diff options
| author | Feuerfuchs <git@feuerfuchs.dev> | 2020-11-01 20:55:14 +0100 | 
|---|---|---|
| committer | Feuerfuchs <git@feuerfuchs.dev> | 2020-11-01 20:55:14 +0100 | 
| commit | d07f664450ddaaebb44127a4bd057763d13d3f82 (patch) | |
| tree | 234cfd673ac527869a8dda4f32afbec48c87b512 /src/bem | |
| download | iro-sass-d07f664450ddaaebb44127a4bd057763d13d3f82.tar.gz iro-sass-d07f664450ddaaebb44127a4bd057763d13d3f82.tar.bz2 iro-sass-d07f664450ddaaebb44127a4bd057763d13d3f82.zip  | |
Init
Diffstat (limited to 'src/bem')
| -rw-r--r-- | src/bem/_block.scss | 392 | ||||
| -rw-r--r-- | src/bem/_debug.scss | 16 | ||||
| -rw-r--r-- | src/bem/_element.scss | 622 | ||||
| -rw-r--r-- | src/bem/_functions.scss | 26 | ||||
| -rw-r--r-- | src/bem/_modifier.scss | 246 | ||||
| -rw-r--r-- | src/bem/_multi.scss | 131 | ||||
| -rw-r--r-- | src/bem/_state.scss | 146 | ||||
| -rw-r--r-- | src/bem/_suffix.scss | 118 | ||||
| -rw-r--r-- | src/bem/_theme.scss | 61 | ||||
| -rw-r--r-- | src/bem/_validators.scss | 176 | ||||
| -rw-r--r-- | src/bem/_vars.scss | 108 | 
11 files changed, 2042 insertions, 0 deletions
diff --git a/src/bem/_block.scss b/src/bem/_block.scss new file mode 100644 index 0000000..d065891 --- /dev/null +++ b/src/bem/_block.scss  | |||
| @@ -0,0 +1,392 @@ | |||
| 1 | //// | ||
| 2 | /// @group BEM | ||
| 3 | /// | ||
| 4 | /// @access public | ||
| 5 | //// | ||
| 6 | |||
| 7 | /// | ||
| 8 | /// Generate a new block. | ||
| 9 | /// | ||
| 10 | /// This mixin simply creates a new block with the name {namespace}_{name}, | ||
| 11 | /// where {namespace} is the prefix assigned to $type and {name} is the | ||
| 12 | /// block's name. | ||
| 13 | /// | ||
| 14 | /// @param {string} $name - Block name | ||
| 15 | /// @param {string} $type [null] - BEMIT namespace of the block | ||
| 16 | /// | ||
| 17 | /// @content | ||
| 18 | /// | ||
| 19 | /// @throw If $type is invalid | ||
| 20 | /// @throw If the block is preceded by another block, element, modifier or suffix | ||
| 21 | /// | ||
| 22 | /// @example scss - Creating a new block | ||
| 23 | /// @include iro-bem-block('something', 'component') { | ||
| 24 | /// /* some definitions */ | ||
| 25 | /// } | ||
| 26 | /// | ||
| 27 | /// // Generates: | ||
| 28 | /// | ||
| 29 | /// .c-something { | ||
| 30 | /// /* some definitions */ | ||
| 31 | /// } | ||
| 32 | /// | ||
| 33 | @mixin iro-bem-block($name, $type: null) { | ||
| 34 | $result: iro-bem-block($name, $type); | ||
| 35 | $selector: nth($result, 1); | ||
| 36 | $context: nth($result, 2); | ||
| 37 | |||
| 38 | @include iro-bem-validate( | ||
| 39 | 'block', | ||
| 40 | (name: $name, type: $type), | ||
| 41 | $selector, | ||
| 42 | $context | ||
| 43 | ); | ||
| 44 | |||
| 45 | @if $type != null { | ||
| 46 | $iro-bem-blocks: append($iro-bem-blocks, $name + '_' + $type) !global; | ||
| 47 | } @else { | ||
| 48 | $iro-bem-blocks: append($iro-bem-blocks, $name) !global; | ||
| 49 | } | ||
| 50 | |||
| 51 | @include iro-context-push($iro-bem-context-id, $context...); | ||
| 52 | @at-root #{$selector} { | ||
| 53 | @content; | ||
| 54 | } | ||
| 55 | @include iro-context-pop($iro-bem-context-id); | ||
| 56 | } | ||
| 57 | |||
| 58 | /// | ||
| 59 | /// Generate a new block. Check the respective mixin documentation for more information. | ||
| 60 | /// | ||
| 61 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 62 | /// | ||
| 63 | /// @see {mixin} iro-bem-block | ||
| 64 | /// | ||
| 65 | @function iro-bem-block($name, $type: null) { | ||
| 66 | // | ||
| 67 | // Possible outcomes: | ||
| 68 | // - ({b,e,m,s}) block | ||
| 69 | // | ||
| 70 | |||
| 71 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
| 72 | |||
| 73 | $selector: null; | ||
| 74 | $base-selector: null; | ||
| 75 | |||
| 76 | @if $type != null { | ||
| 77 | $namespace: map-get($iro-bem-namespaces, $type); | ||
| 78 | |||
| 79 | @if not $namespace { | ||
| 80 | @error '"#{$type}" is not a valid type.'; | ||
| 81 | } | ||
| 82 | |||
| 83 | $base-selector: selector-parse('.' + $namespace + '-' + $name); | ||
| 84 | |||
| 85 | @if $type != 'theme' or & { | ||
| 86 | $selector: $base-selector; | ||
| 87 | } @else if not & { | ||
| 88 | $selector: iro-bem-theme-selector($name); | ||
| 89 | } | ||
| 90 | } @else { | ||
| 91 | $base-selector: selector-parse('.' + $name); | ||
| 92 | $selector: $base-selector; | ||
| 93 | } | ||
| 94 | |||
| 95 | @if & { | ||
| 96 | $selector: selector-nest(&, $selector); | ||
| 97 | } | ||
| 98 | |||
| 99 | $context: 'block', ( | ||
| 100 | 'name': $name, | ||
| 101 | 'type': $type, | ||
| 102 | 'selector': $selector, | ||
| 103 | 'base-selector': $base-selector | ||
| 104 | ); | ||
| 105 | |||
| 106 | @return $selector $context; | ||
| 107 | } | ||
| 108 | |||
| 109 | /// | ||
| 110 | /// Generate a new object block. It's a shorthand for iro-bem-block($name, 'object'). | ||
| 111 | /// | ||
| 112 | /// @param {string} $name - Object block name | ||
| 113 | /// | ||
| 114 | /// @content | ||
| 115 | /// | ||
| 116 | @mixin iro-bem-object($name) { | ||
| 117 | @include iro-bem-block($name, 'object') { | ||
| 118 | @content; | ||
| 119 | } | ||
| 120 | } | ||
| 121 | |||
| 122 | /// | ||
| 123 | /// Generate a new object block. Check the respective mixin documentation for more information. | ||
| 124 | /// | ||
| 125 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 126 | /// | ||
| 127 | /// @see {mixin} iro-bem-object | ||
| 128 | /// | ||
| 129 | @function iro-bem-object($name) { | ||
| 130 | @return iro-bem-block($name, 'object'); | ||
| 131 | } | ||
| 132 | |||
| 133 | /// | ||
| 134 | /// Generate a new component block. It's a shorthand for iro-bem-block($name, 'component'). | ||
| 135 | /// | ||
| 136 | /// @param {string} $name - Component block name | ||
| 137 | /// | ||
| 138 | /// @content | ||
| 139 | /// | ||
| 140 | @mixin iro-bem-component($name) { | ||
| 141 | @include iro-bem-block($name, 'component') { | ||
| 142 | @content; | ||
| 143 | } | ||
| 144 | } | ||
| 145 | |||
| 146 | /// | ||
| 147 | /// Generate a new component block. Check the respective mixin documentation for more information. | ||
| 148 | /// | ||
| 149 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 150 | /// | ||
| 151 | /// @see {mixin} iro-bem-component | ||
| 152 | /// | ||
| 153 | @function iro-bem-component($name) { | ||
| 154 | @return iro-bem-block($name, 'component'); | ||
| 155 | } | ||
| 156 | |||
| 157 | /// | ||
| 158 | /// Generate a new layout block. It's a shorthand for iro-bem-block($name, 'layout'). | ||
| 159 | /// | ||
| 160 | /// @param {string} $name - Layout block name | ||
| 161 | /// | ||
| 162 | /// @content | ||
| 163 | /// | ||
| 164 | @mixin iro-bem-layout($name) { | ||
| 165 | @include iro-bem-block($name, 'layout') { | ||
| 166 | @content; | ||
| 167 | } | ||
| 168 | } | ||
| 169 | |||
| 170 | /// | ||
| 171 | /// Generate a new layout block. Check the respective mixin documentation for more information. | ||
| 172 | /// | ||
| 173 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 174 | /// | ||
| 175 | /// @see {mixin} iro-bem-layout | ||
| 176 | /// | ||
| 177 | @function iro-bem-layout($name) { | ||
| 178 | @return iro-bem-block($name, 'layout'); | ||
| 179 | } | ||
| 180 | |||
| 181 | /// | ||
| 182 | /// Generate a new utility block. It's a shorthand for iro-bem-block($name, 'utility'). | ||
| 183 | /// | ||
| 184 | /// @param {string} $name - Utility block name | ||
| 185 | /// | ||
| 186 | /// @content | ||
| 187 | /// | ||
| 188 | @mixin iro-bem-utility($name) { | ||
| 189 | @include iro-bem-block($name, 'utility') { | ||
| 190 | @content; | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 194 | /// | ||
| 195 | /// Generate a new utility block. Check the respective mixin documentation for more information. | ||
| 196 | /// | ||
| 197 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 198 | /// | ||
| 199 | /// @see {mixin} iro-bem-utility | ||
| 200 | /// | ||
| 201 | @function iro-bem-utility($name) { | ||
| 202 | @return iro-bem-block($name, 'utility'); | ||
| 203 | } | ||
| 204 | |||
| 205 | /// | ||
| 206 | /// Generate a new scope block. It's a shorthand for iro-bem-block($name, 'scope'). | ||
| 207 | /// | ||
| 208 | /// @param {string} $name - Scope block name | ||
| 209 | /// | ||
| 210 | /// @content | ||
| 211 | /// | ||
| 212 | @mixin iro-bem-scope($name) { | ||
| 213 | @include iro-bem-block($name, 'scope') { | ||
| 214 | @content; | ||
| 215 | } | ||
| 216 | } | ||
| 217 | |||
| 218 | /// | ||
| 219 | /// Generate a new scope block. Check the respective mixin documentation for more information. | ||
| 220 | /// | ||
| 221 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 222 | /// | ||
| 223 | /// @see {mixin} iro-bem-scope | ||
| 224 | /// | ||
| 225 | @function iro-bem-scope($name) { | ||
| 226 | @return iro-bem-block($name, 'scope'); | ||
| 227 | } | ||
| 228 | |||
| 229 | /// | ||
| 230 | /// Generate a new theme block. It's a shorthand for iro-bem-block($name, 'theme'). | ||
| 231 | /// | ||
| 232 | /// @param {string} $name - Theme block name | ||
| 233 | /// | ||
| 234 | /// @content | ||
| 235 | /// | ||
| 236 | @mixin iro-bem-theme($name) { | ||
| 237 | @include iro-bem-block($name, 'theme') { | ||
| 238 | @content; | ||
| 239 | } | ||
| 240 | } | ||
| 241 | |||
| 242 | /// | ||
| 243 | /// Generate a new theme block. Check the respective mixin documentation for more information. | ||
| 244 | /// | ||
| 245 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 246 | /// | ||
| 247 | /// @see {mixin} iro-bem-theme | ||
| 248 | /// | ||
| 249 | @function iro-bem-theme($name) { | ||
| 250 | @return iro-bem-block($name, 'theme'); | ||
| 251 | } | ||
| 252 | |||
| 253 | /// | ||
| 254 | /// Generate a new JS block. It's a shorthand for iro-bem-block($name, 'js'). | ||
| 255 | /// | ||
| 256 | /// @param {string} $name - JS block name | ||
| 257 | /// | ||
| 258 | /// @content | ||
| 259 | /// | ||
| 260 | @mixin iro-bem-js($name) { | ||
| 261 | @include iro-bem-block($name, 'js') { | ||
| 262 | @content; | ||
| 263 | } | ||
| 264 | } | ||
| 265 | |||
| 266 | /// | ||
| 267 | /// Generate a new JS block. Check the respective mixin documentation for more information. | ||
| 268 | /// | ||
| 269 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 270 | /// | ||
| 271 | /// @see {mixin} iro-bem-js | ||
| 272 | /// | ||
| 273 | @function iro-bem-js($name) { | ||
| 274 | @return iro-bem-block($name, 'js'); | ||
| 275 | } | ||
| 276 | |||
| 277 | /// | ||
| 278 | /// Generate a new QA block. It's a shorthand for iro-bem-block($name, 'qa'). | ||
| 279 | /// | ||
| 280 | /// @param {string} $name - QA block name | ||
| 281 | /// | ||
| 282 | /// @content | ||
| 283 | /// | ||
| 284 | @mixin iro-bem-qa($name) { | ||
| 285 | @include iro-bem-block($name, 'qa') { | ||
| 286 | @content; | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | /// | ||
| 291 | /// Generate a new QA block. Check the respective mixin documentation for more information. | ||
| 292 | /// | ||
| 293 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 294 | /// | ||
| 295 | /// @see {mixin} iro-bem-qa | ||
| 296 | /// | ||
| 297 | @function iro-bem-qa($name) { | ||
| 298 | @return iro-bem-block($name, 'qa'); | ||
| 299 | } | ||
| 300 | |||
| 301 | /// | ||
| 302 | /// Generate a new hack block. It's a shorthand for iro-bem-block($name, 'hack'). | ||
| 303 | /// | ||
| 304 | /// @param {string} $name - Hack block name | ||
| 305 | /// | ||
| 306 | /// @content | ||
| 307 | /// | ||
| 308 | @mixin iro-bem-hack($name) { | ||
| 309 | @include iro-bem-block($name, 'hack') { | ||
| 310 | @content; | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | /// | ||
| 315 | /// Generate a new hack block. Check the respective mixin documentation for more information. | ||
| 316 | /// | ||
| 317 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 318 | /// | ||
| 319 | /// @see {mixin} iro-bem-hack | ||
| 320 | /// | ||
| 321 | @function iro-bem-hack($name) { | ||
| 322 | @return iro-bem-block($name, 'hack'); | ||
| 323 | } | ||
| 324 | |||
| 325 | /// | ||
| 326 | /// Assert that a block or element is composed of another block. In BEM, such a relationship is referred to | ||
| 327 | /// as a "mix": https://en.bem.info/methodology/key-concepts/#mix | ||
| 328 | /// | ||
| 329 | /// Compilation will fail if the foreign block doesn't exist. This way, you can ensure that blocks are | ||
| 330 | /// defined in the right order so that composed blocks/elements will actually override the foreign | ||
| 331 | /// declarations without having to artificially increase the specificity. | ||
| 332 | /// | ||
| 333 | /// @param {string | list} $block - Either first block name, or list with two items: 1. block name, 2. block type | ||
| 334 | /// @param {string | list} $blocks - Either other block names, or list with two items: 1. block name, 2. block type | ||
| 335 | /// | ||
| 336 | /// @throw If a block type is invalid | ||
| 337 | /// @throw If a block doesn't exist | ||
| 338 | /// | ||
| 339 | /// @example scss - Successful assertion | ||
| 340 | /// @include iro-bem-component('someBlock') { | ||
| 341 | /// /* some definitions */ | ||
| 342 | /// } | ||
| 343 | /// | ||
| 344 | /// @include iro-bem-component('anotherBlock') { | ||
| 345 | /// /* some definitions */ | ||
| 346 | /// | ||
| 347 | /// @include iro-bem-element('elem') { | ||
| 348 | /// @include iro-bem-composed-of('someBlock' 'component'); | ||
| 349 | /// | ||
| 350 | /// /* some definitions */ | ||
| 351 | /// } | ||
| 352 | /// } | ||
| 353 | /// | ||
| 354 | /// // Intended use: <div class="c-anotherBlock__elem c-someBlock">...</div> | ||
| 355 | /// | ||
| 356 | /// @example scss - Failing assertion | ||
| 357 | /// @include iro-bem-component('anotherBlock') { | ||
| 358 | /// /* some definitions */ | ||
| 359 | /// | ||
| 360 | /// @include iro-bem-element('elem') { | ||
| 361 | /// @include iro-bem-composed-of('someBlock' 'component'); | ||
| 362 | /// | ||
| 363 | /// /* some definitions */ | ||
| 364 | /// } | ||
| 365 | /// } | ||
| 366 | /// | ||
| 367 | /// @include iro-bem-component('someBlock') { | ||
| 368 | /// /* some definitions */ | ||
| 369 | /// } | ||
| 370 | /// | ||
| 371 | /// // Compilation will fail because c-someBlock is defined after c-anotherBlock__elem | ||
| 372 | /// | ||
| 373 | @mixin iro-bem-composed-of($block, $blocks...) { | ||
| 374 | @each $block in iro-list-prepend($blocks, $block) { | ||
| 375 | @if type-of($block) == string { | ||
| 376 | @if not index($iro-bem-blocks, $block) { | ||
| 377 | @error 'Block "#{$block}" does not exist.'; | ||
| 378 | } | ||
| 379 | } @else { | ||
| 380 | $name: nth($block, 1); | ||
| 381 | $type: nth($block, 2); | ||
| 382 | |||
| 383 | @if not map-get($iro-bem-namespaces, $type) { | ||
| 384 | @error '"#{$type}" is not a valid type.'; | ||
| 385 | } | ||
| 386 | |||
| 387 | @if not index($iro-bem-blocks, $name + '_' + $type) { | ||
| 388 | @error 'Block "#{$name}" does not exist.'; | ||
| 389 | } | ||
| 390 | } | ||
| 391 | } | ||
| 392 | } | ||
diff --git a/src/bem/_debug.scss b/src/bem/_debug.scss new file mode 100644 index 0000000..e69083c --- /dev/null +++ b/src/bem/_debug.scss  | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | //// | ||
| 2 | /// @group BEM | ||
| 3 | /// | ||
| 4 | /// @access public | ||
| 5 | //// | ||
| 6 | |||
| 7 | @if $iro-bem-debug { | ||
| 8 | @each $type, $color in $iro-bem-debug-colors { | ||
| 9 | $namespace: map-get($iro-bem-namespaces, $type); | ||
| 10 | |||
| 11 | [class^='#{$namespace}-'], | ||
| 12 | [class*=' #{$namespace}-'] { | ||
| 13 | outline: 5px solid $color; | ||
| 14 | } | ||
| 15 | } | ||
| 16 | } | ||
diff --git a/src/bem/_element.scss b/src/bem/_element.scss new file mode 100644 index 0000000..b3d2fee --- /dev/null +++ b/src/bem/_element.scss  | |||
| @@ -0,0 +1,622 @@ | |||
| 1 | //// | ||
| 2 | /// @group BEM | ||
| 3 | /// | ||
| 4 | /// @access public | ||
| 5 | //// | ||
| 6 | |||
| 7 | /// | ||
| 8 | /// Generate a new BEM element. | ||
| 9 | /// | ||
| 10 | /// The element will be generated according to the BEM naming convention. | ||
| 11 | /// If the parent selector doesn't match the block selector, the element will be | ||
| 12 | /// nested inside the parent selector. This means, you may nest elements inside | ||
| 13 | /// other elements, modifiers or any kind of selector such as &:hover. | ||
| 14 | /// | ||
| 15 | /// @param {string} $name - First element name | ||
| 16 | /// @param {string} $names - More element names | ||
| 17 | /// | ||
| 18 | /// @content | ||
| 19 | /// | ||
| 20 | /// @throw If the element is not preceded by a block, element, modifier or suffix. | ||
| 21 | /// | ||
| 22 | /// @example scss - Element for a block | ||
| 23 | /// @include iro-bem-component('block') { | ||
| 24 | /// /* some block definitions */ | ||
| 25 | /// | ||
| 26 | /// @include iro-bem-element('elem') { | ||
| 27 | /// /* some element definitions */ | ||
| 28 | /// } | ||
| 29 | /// } | ||
| 30 | /// | ||
| 31 | /// // Generates: | ||
| 32 | /// | ||
| 33 | /// .c-block { | ||
| 34 | /// /* some block definitions */ | ||
| 35 | /// } | ||
| 36 | /// | ||
| 37 | /// .c-block__elem { | ||
| 38 | /// /* some element definitions */ | ||
| 39 | /// } | ||
| 40 | /// | ||
| 41 | /// @example scss - Element that is affected by the user hovering the block | ||
| 42 | /// @include iro-bem-component('block') { | ||
| 43 | /// /* some block definitions */ | ||
| 44 | /// | ||
| 45 | /// @include iro-bem-element('elem') { | ||
| 46 | /// background-color: #eee; | ||
| 47 | /// } | ||
| 48 | /// | ||
| 49 | /// &:hover { | ||
| 50 | /// @include iro-bem-element('elem') { | ||
| 51 | /// background-color: #000; | ||
| 52 | /// } | ||
| 53 | /// } | ||
| 54 | /// } | ||
| 55 | /// | ||
| 56 | /// // Generates: | ||
| 57 | /// | ||
| 58 | /// .c-block { | ||
| 59 | /// /* some block definitions */ | ||
| 60 | /// } | ||
| 61 | /// | ||
| 62 | /// .c-block__elem { | ||
| 63 | /// background-color: #eee; | ||
| 64 | /// } | ||
| 65 | /// | ||
| 66 | /// .c-block:hover .c-block__elem { | ||
| 67 | /// background-color: #000; | ||
| 68 | /// } | ||
| 69 | /// | ||
| 70 | /// @example scss - Multiple elements | ||
| 71 | /// @include iro-bem-component('block') { | ||
| 72 | /// /* some block definitions */ | ||
| 73 | /// | ||
| 74 | /// @include iro-bem-element('elem1', 'elem2') { | ||
| 75 | /// /* some element definitions */ | ||
| 76 | /// } | ||
| 77 | /// } | ||
| 78 | /// | ||
| 79 | /// // Generates: | ||
| 80 | /// | ||
| 81 | /// .c-block { | ||
| 82 | /// /* some block definitions */ | ||
| 83 | /// } | ||
| 84 | /// | ||
| 85 | /// .c-block__elem1, .c-block__elem2 { | ||
| 86 | /// /* some element definitions */ | ||
| 87 | /// } | ||
| 88 | /// | ||
| 89 | @mixin iro-bem-element($name, $names...) { | ||
| 90 | $result: iro-bem-element($name, $names...); | ||
| 91 | $selector: nth($result, 1); | ||
| 92 | $context: nth($result, 2); | ||
| 93 | |||
| 94 | @include iro-bem-validate( | ||
| 95 | 'element', | ||
| 96 | (name: $name, names: $names), | ||
| 97 | $selector, | ||
| 98 | $context | ||
| 99 | ); | ||
| 100 | |||
| 101 | @include iro-context-push($iro-bem-context-id, $context...); | ||
| 102 | @at-root #{$selector} { | ||
| 103 | @content; | ||
| 104 | } | ||
| 105 | @include iro-context-pop($iro-bem-context-id); | ||
| 106 | } | ||
| 107 | |||
| 108 | /// | ||
| 109 | /// Generate a new BEM element. Check the respective mixin documentation for more information. | ||
| 110 | /// | ||
| 111 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 112 | /// | ||
| 113 | /// @see {mixin} iro-bem-element | ||
| 114 | /// | ||
| 115 | @function iro-bem-element($name, $names...) { | ||
| 116 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
| 117 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); | ||
| 118 | |||
| 119 | $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element'); | ||
| 120 | |||
| 121 | $selector: (); | ||
| 122 | $parts-data: (); | ||
| 123 | |||
| 124 | @if nth($parent-context, 1) == 'element' { | ||
| 125 | @if $iro-bem-element-nesting-policy == 'disallow' { | ||
| 126 | @error 'Element nesting is forbidden.'; | ||
| 127 | } | ||
| 128 | |||
| 129 | @if $iro-bem-element-nesting-policy == 'append' { | ||
| 130 | $element-selector: map-get(nth($parent-context, 2), 'selector'); | ||
| 131 | |||
| 132 | @if not iro-selector-suffix-match(&, $element-selector) { | ||
| 133 | @error 'A nested element must be an immediate children of the parent element.'; | ||
| 134 | } | ||
| 135 | |||
| 136 | // | ||
| 137 | // Possible outcomes: | ||
| 138 | // - {e}__element | ||
| 139 | // - [manual selector] {e}__element | ||
| 140 | // | ||
| 141 | |||
| 142 | @each $name in join($name, $names) { | ||
| 143 | $sel: selector-append(&, $iro-bem-element-separator + $name); | ||
| 144 | $selector: join($selector, $sel, comma); | ||
| 145 | $parts-data: append($parts-data, ( | ||
| 146 | 'name': $name, | ||
| 147 | 'selector': $sel | ||
| 148 | )); | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | $parent-context: iro-context-get($iro-bem-context-id, 'block'); | ||
| 153 | } | ||
| 154 | |||
| 155 | @if length($selector) == 0 { | ||
| 156 | $parent-selector: map-get(nth($parent-context, 2), 'selector'); | ||
| 157 | |||
| 158 | @if iro-selector-suffix-match(&, $parent-selector) { | ||
| 159 | // | ||
| 160 | // Possible outcomes: | ||
| 161 | // - {b}__element | ||
| 162 | // - [manual selector] {b}__element | ||
| 163 | // | ||
| 164 | |||
| 165 | @each $name in join($name, $names) { | ||
| 166 | $sel: selector-append(&, $iro-bem-element-separator + $name); | ||
| 167 | $selector: join($selector, $sel, comma); | ||
| 168 | $parts-data: append($parts-data, ( | ||
| 169 | 'name': $name, | ||
| 170 | 'selector': $sel | ||
| 171 | )); | ||
| 172 | } | ||
| 173 | } @else { | ||
| 174 | // | ||
| 175 | // Possible outcomes: | ||
| 176 | // - {b} [manual selector] {b}__element | ||
| 177 | // - {e,m,s} ([manual selector]) {b}__element | ||
| 178 | // | ||
| 179 | |||
| 180 | @if nth($parent-context, 1) != 'block' { | ||
| 181 | $parent-context: iro-context-get($iro-bem-context-id, 'block'); | ||
| 182 | } | ||
| 183 | |||
| 184 | $block-base-selector: map-get(nth($parent-context, 2), 'base-selector'); | ||
| 185 | |||
| 186 | @each $name in join($name, $names) { | ||
| 187 | $sel: selector-nest(&, selector-append($block-base-selector, $iro-bem-element-separator + $name)); | ||
| 188 | $selector: join($selector, $sel, comma); | ||
| 189 | $parts-data: append($parts-data, ( | ||
| 190 | 'name': $name, | ||
| 191 | 'selector': $sel | ||
| 192 | )); | ||
| 193 | } | ||
| 194 | } | ||
| 195 | } | ||
| 196 | |||
| 197 | $context: 'element', ( | ||
| 198 | 'parts': $parts-data, | ||
| 199 | 'selector': $selector | ||
| 200 | ); | ||
| 201 | |||
| 202 | @return $selector $context; | ||
| 203 | } | ||
| 204 | |||
| 205 | /// | ||
| 206 | /// Generate a BEM element that is related to the current element. | ||
| 207 | /// | ||
| 208 | /// The generated element selector is appended to the current element selector. The $sign | ||
| 209 | /// determines the relationship. | ||
| 210 | /// | ||
| 211 | /// @param {string} $sign - Relationshop sign, either '+' or '~' | ||
| 212 | /// @param {string} $name - First element name | ||
| 213 | /// @param {string} $names - More element names | ||
| 214 | /// | ||
| 215 | /// @content | ||
| 216 | /// | ||
| 217 | /// @throw If the element is not preceded by an element. | ||
| 218 | /// | ||
| 219 | /// @example scss - A sibling element to a single element | ||
| 220 | /// @include iro-bem-component('block') { | ||
| 221 | /// @include iro-bem-element('elem') { | ||
| 222 | /// /* some element definitions */ | ||
| 223 | /// | ||
| 224 | /// @include iro-bem-related-element('~', 'sibling') { | ||
| 225 | /// /* some sibling element definitions */ | ||
| 226 | /// } | ||
| 227 | /// } | ||
| 228 | /// } | ||
| 229 | /// | ||
| 230 | /// // Generates: | ||
| 231 | /// | ||
| 232 | /// .c-block__elem { | ||
| 233 | /// /* some element definitions */ | ||
| 234 | /// } | ||
| 235 | /// | ||
| 236 | /// .c-block__elem ~ .c-block__sibling { | ||
| 237 | /// /* some sibling element definitions */ | ||
| 238 | /// } | ||
| 239 | /// | ||
| 240 | /// @example scss - A successor element to a single element | ||
| 241 | /// @include iro-bem-component('block') { | ||
| 242 | /// @include iro-bem-element('elem') { | ||
| 243 | /// /* some element definitions */ | ||
| 244 | /// | ||
| 245 | /// @include iro-bem-related-element('+', 'successor') { | ||
| 246 | /// /* some successor element definitions */ | ||
| 247 | /// } | ||
| 248 | /// } | ||
| 249 | /// } | ||
| 250 | /// | ||
| 251 | /// // Generates: | ||
| 252 | /// | ||
| 253 | /// .c-block__elem { | ||
| 254 | /// /* some element definitions */ | ||
| 255 | /// } | ||
| 256 | /// | ||
| 257 | /// .c-block__elem + .c-block__successor { | ||
| 258 | /// /* some successor element definitions */ | ||
| 259 | /// } | ||
| 260 | /// | ||
| 261 | /// @example scss - A successor element to multiple elements | ||
| 262 | /// @include iro-bem-component('block') { | ||
| 263 | /// @include iro-bem-element('elem1', 'elem2') { | ||
| 264 | /// /* some element definitions */ | ||
| 265 | /// | ||
| 266 | /// @include iro-bem-related-element('+', 'successor') { | ||
| 267 | /// /* some successor element definitions */ | ||
| 268 | /// } | ||
| 269 | /// } | ||
| 270 | /// } | ||
| 271 | /// | ||
| 272 | /// // Generates: | ||
| 273 | /// | ||
| 274 | /// .c-block__elem1, .c-block__elem2 { | ||
| 275 | /// /* some element definitions */ | ||
| 276 | /// } | ||
| 277 | /// | ||
| 278 | /// .c-block__elem1 + .c-block__successor, .c-block__elem2 + .c-block__successor { | ||
| 279 | /// /* some successor element definitions */ | ||
| 280 | /// } | ||
| 281 | /// | ||
| 282 | @mixin iro-bem-related-element($sign, $name, $names...) { | ||
| 283 | $result: iro-bem-related-element($sign, $name, $names...); | ||
| 284 | $selector: nth($result, 1); | ||
| 285 | $context: nth($result, 2); | ||
| 286 | |||
| 287 | @include iro-bem-validate( | ||
| 288 | 'related-element', | ||
| 289 | (sign: $sign, name: $name, names: $names), | ||
| 290 | $selector, | ||
| 291 | $context | ||
| 292 | ); | ||
| 293 | |||
| 294 | @include iro-context-push($iro-bem-context-id, $context...); | ||
| 295 | @at-root #{$selector} { | ||
| 296 | @content; | ||
| 297 | } | ||
| 298 | @include iro-context-pop($iro-bem-context-id); | ||
| 299 | } | ||
| 300 | |||
| 301 | /// | ||
| 302 | /// Generate a new BEM element that is related to the current element. | ||
| 303 | /// Check the respective mixin documentation for more information. | ||
| 304 | /// | ||
| 305 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 306 | /// | ||
| 307 | /// @see {mixin} iro-bem-related-element | ||
| 308 | /// | ||
| 309 | @function iro-bem-related-element($sign, $name, $names...) { | ||
| 310 | // | ||
| 311 | // Generating this selector is simple: Take the latest block context, use it | ||
| 312 | // to generate the element part, and insert it at the end of the current selector. | ||
| 313 | // Possible outcomes: | ||
| 314 | // - {e} ({m,s}) ([manual selector]) + {e} | ||
| 315 | // - {e} ({m,s}) ([manual selector]) ~ {e} | ||
| 316 | // | ||
| 317 | |||
| 318 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
| 319 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'element'); | ||
| 320 | |||
| 321 | @if $sign != '+' and $sign != '~' { | ||
| 322 | @error 'Invalid relationship sign #{inspect($sign)}.'; | ||
| 323 | } | ||
| 324 | |||
| 325 | $block-context: iro-context-get($iro-bem-context-id, 'block'); | ||
| 326 | $block-base-selector: map-get(nth($block-context, 2), 'base-selector'); | ||
| 327 | |||
| 328 | $selector: (); | ||
| 329 | $parts-data: (); | ||
| 330 | |||
| 331 | @each $name in join($name, $names) { | ||
| 332 | $sel: selector-nest(&, $sign, selector-append($block-base-selector, $iro-bem-element-separator + $name)); | ||
| 333 | $selector: join($selector, $sel, comma); | ||
| 334 | $parts-data: append($parts-data, ( | ||
| 335 | 'name': $name, | ||
| 336 | 'selector': $sel | ||
| 337 | )); | ||
| 338 | } | ||
| 339 | |||
| 340 | $context: 'element', ( | ||
| 341 | 'parts': $parts-data, | ||
| 342 | 'selector': $selector | ||
| 343 | ); | ||
| 344 | |||
| 345 | @return $selector $context; | ||
| 346 | } | ||
| 347 | |||
| 348 | /// | ||
| 349 | /// Generate a BEM element that is a sibling of the current element. | ||
| 350 | /// | ||
| 351 | /// It's a shorthand for iro-bem-related-element('~', $name). | ||
| 352 | /// | ||
| 353 | /// @param {string} $name - First element name | ||
| 354 | /// @param {list} $names - List of more element names | ||
| 355 | /// | ||
| 356 | /// @content | ||
| 357 | /// | ||
| 358 | @mixin iro-bem-sibling-element($name, $names...) { | ||
| 359 | @include iro-bem-related-element('~', $name, $names...) { | ||
| 360 | @content; | ||
| 361 | } | ||
| 362 | } | ||
| 363 | |||
| 364 | /// | ||
| 365 | /// Generate a new BEM element that is a sibling of the current element. | ||
| 366 | /// Check the respective mixin documentation for more information. | ||
| 367 | /// | ||
| 368 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 369 | /// | ||
| 370 | /// @see {mixin} iro-bem-sibling-element | ||
| 371 | /// | ||
| 372 | @function iro-bem-sibling-element($name, $names...) { | ||
| 373 | @return iro-bem-related-element('~', $name, $names...); | ||
| 374 | } | ||
| 375 | |||
| 376 | /// | ||
| 377 | /// Generate a BEM element that is the successor of the current element. | ||
| 378 | /// | ||
| 379 | /// It's a shorthand for iro-bem-related-element('+', $name). | ||
| 380 | /// | ||
| 381 | /// @param {string} $name - First element name | ||
| 382 | /// @param {string} $names - More element names | ||
| 383 | /// | ||
| 384 | /// @content | ||
| 385 | /// | ||
| 386 | @mixin iro-bem-next-element($name, $names...) { | ||
| 387 | @include iro-bem-related-element('+', $name, $names...) { | ||
| 388 | @content; | ||
| 389 | } | ||
| 390 | } | ||
| 391 | |||
| 392 | /// | ||
| 393 | /// Generate a new BEM element that is the successor of the current element. | ||
| 394 | /// Check the respective mixin documentation for more information. | ||
| 395 | /// | ||
| 396 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 397 | /// | ||
| 398 | /// @see {mixin} iro-bem-next-element | ||
| 399 | /// | ||
| 400 | @function iro-bem-next-element($name, $names...) { | ||
| 401 | @return iro-bem-related-element('+', $name, $names...); | ||
| 402 | } | ||
| 403 | |||
| 404 | /// | ||
| 405 | /// Generate the current BEM element as a successor of itself. | ||
| 406 | /// | ||
| 407 | /// If this is applied to a single element, it behaves exactly the same as | ||
| 408 | /// iro-bem-related-element('+', name); | ||
| 409 | /// However, if it is applied to multiple elements, each twin element only will influence | ||
| 410 | /// their other twin, which is not replicable with iro-bem-related-element. | ||
| 411 | /// | ||
| 412 | /// @content | ||
| 413 | /// | ||
| 414 | /// @example scss - Two twin elements | ||
| 415 | /// @include iro-bem-component('block') { | ||
| 416 | /// @include iro-bem-element('elem') { | ||
| 417 | /// /* some element definitions */ | ||
| 418 | /// | ||
| 419 | /// @include iro-bem-next-twin-element { | ||
| 420 | /// /* some twin element definitions */ | ||
| 421 | /// } | ||
| 422 | /// } | ||
| 423 | /// } | ||
| 424 | /// | ||
| 425 | /// // Generates: | ||
| 426 | /// | ||
| 427 | /// .c-block__elem { | ||
| 428 | /// /* some element definitions */ | ||
| 429 | /// } | ||
| 430 | /// | ||
| 431 | /// .c-block__elem + .c-block__elem { | ||
| 432 | /// /* some twin element definitions */ | ||
| 433 | /// } | ||
| 434 | /// | ||
| 435 | /// @example scss - Multiple twin elements | ||
| 436 | /// @include iro-bem-component('block') { | ||
| 437 | /// @include iro-bem-element('elem1', 'elem2') { | ||
| 438 | /// /* some element definitions */ | ||
| 439 | /// | ||
| 440 | /// @include iro-bem-next-twin-element { | ||
| 441 | /// /* some twin element definitions */ | ||
| 442 | /// } | ||
| 443 | /// } | ||
| 444 | /// } | ||
| 445 | /// | ||
| 446 | /// // Generates: | ||
| 447 | /// | ||
| 448 | /// .c-block__elem1, .c-block__elem2 { | ||
| 449 | /// /* some element definitions */ | ||
| 450 | /// } | ||
| 451 | /// | ||
| 452 | /// .c-block__elem1 + .c-block__elem1, .c-block__elem2 + .c-block__elem2 { | ||
| 453 | /// /* some twin element definitions */ | ||
| 454 | /// } | ||
| 455 | /// | ||
| 456 | @mixin iro-bem-related-twin-element($sign) { | ||
| 457 | $result: iro-bem-related-twin-element($sign); | ||
| 458 | $selector: nth($result, 1); | ||
| 459 | $context: nth($result, 2); | ||
| 460 | |||
| 461 | @include iro-bem-validate( | ||
| 462 | 'next-twin-element', | ||
| 463 | (), | ||
| 464 | $selector, | ||
| 465 | $context | ||
| 466 | ); | ||
| 467 | |||
| 468 | @include iro-context-push($iro-bem-context-id, $context...); | ||
| 469 | @at-root #{$selector} { | ||
| 470 | @content; | ||
| 471 | } | ||
| 472 | @include iro-context-pop($iro-bem-context-id); | ||
| 473 | } | ||
| 474 | |||
| 475 | /// | ||
| 476 | /// Generate the current BEM element as a successor of itself. | ||
| 477 | /// Check the respective mixin documentation for more information. | ||
| 478 | /// | ||
| 479 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 480 | /// | ||
| 481 | /// @see {mixin} iro-bem-next-twin-element | ||
| 482 | /// | ||
| 483 | @function iro-bem-related-twin-element($sign) { | ||
| 484 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
| 485 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'element'); | ||
| 486 | |||
| 487 | $element-context: iro-context-get($iro-bem-context-id, 'element'); | ||
| 488 | $element-selector: map-get(nth($element-context, 2), 'selector'); | ||
| 489 | |||
| 490 | $block-context: iro-context-get($iro-bem-context-id, 'block'); | ||
| 491 | $block-base-selector: map-get(nth($block-context, 2), 'base-selector'); | ||
| 492 | |||
| 493 | $selector: (); | ||
| 494 | $parts-data: (); | ||
| 495 | |||
| 496 | // | ||
| 497 | // To determine the twin for each element, iterate the sub-selectors from the current selector | ||
| 498 | // and check if it contains the currently inspected element. This has to be done with string | ||
| 499 | // comparison since none of Sass selector functions is of use here. | ||
| 500 | // Finally, the current twin will be appended to the extracted sub-selector as a successor | ||
| 501 | // element. | ||
| 502 | // | ||
| 503 | @each $part-data in map-get(nth($element-context, 2), 'parts') { | ||
| 504 | $part-selector: map-get($part-data, 'selector'); | ||
| 505 | $part-name: map-get($part-data, 'name'); | ||
| 506 | |||
| 507 | $sel: (); | ||
| 508 | @if iro-selector-suffix-match(&, $element-selector) { | ||
| 509 | // | ||
| 510 | // This mixin is included in the selector the last element mixin created. | ||
| 511 | // Possible outcomes: | ||
| 512 | // - {e} + {e} | ||
| 513 | // - [manual selector] {e} + {e} | ||
| 514 | // | ||
| 515 | |||
| 516 | @each $s in & { | ||
| 517 | @each $ps in $part-selector { | ||
| 518 | @if nth($s, -1) == nth($ps, -1) { | ||
| 519 | $sel-ent: selector-nest($s, $sign, selector-append($block-base-selector, $iro-bem-element-separator + $part-name)); | ||
| 520 | $sel: join($sel, $sel-ent, comma); | ||
| 521 | } | ||
| 522 | } | ||
| 523 | } | ||
| 524 | } @else { | ||
| 525 | // | ||
| 526 | // This mixin is NOT included in the selector the last element mixin created. | ||
| 527 | // Possible outcomes: | ||
| 528 | // - {e} {m,s} + {e} | ||
| 529 | // - {e} [manual selector] + {e} | ||
| 530 | // - {e} {m,s} [manual selector] + {e} | ||
| 531 | // | ||
| 532 | |||
| 533 | @each $s in & { | ||
| 534 | @each $ps in $part-selector { | ||
| 535 | @if str-index(inspect($s), inspect($ps)) { | ||
| 536 | $char-index: str-length(inspect($ps)) + 1; | ||
| 537 | $match: index(' ' ':' ',', str-slice(inspect($s), $char-index, $char-index)) != null; | ||
| 538 | |||
| 539 | @if not $match { | ||
| 540 | @each $separator in $iro-bem-element-separator $iro-bem-modifier-separator $iro-bem-suffix-separator { | ||
| 541 | @if str-slice(inspect($s), $char-index, $char-index + str-length($separator) - 1) == $separator { | ||
| 542 | $match: true; | ||
| 543 | } | ||
| 544 | } | ||
| 545 | } | ||
| 546 | |||
| 547 | @if $match { | ||
| 548 | $sel-ent: selector-nest($s, '+', selector-append($block-base-selector, $iro-bem-element-separator + $part-name)); | ||
| 549 | $sel: join($sel, $sel-ent, comma); | ||
| 550 | } | ||
| 551 | } | ||
| 552 | } | ||
| 553 | } | ||
| 554 | } | ||
| 555 | @if length($sel) != length($part-selector) { | ||
| 556 | @error 'Could not generate twin element selector.'; | ||
| 557 | } | ||
| 558 | |||
| 559 | $selector: join($selector, $sel, comma); | ||
| 560 | $parts-data: append($parts-data, ( | ||
| 561 | 'name': $part-name, | ||
| 562 | 'selector': $sel | ||
| 563 | )); | ||
| 564 | } | ||
| 565 | |||
| 566 | $context: 'element', ( | ||
| 567 | 'parts': $parts-data, | ||
| 568 | 'selector': $selector | ||
| 569 | ); | ||
| 570 | |||
| 571 | @return $selector $context; | ||
| 572 | } | ||
| 573 | |||
| 574 | /// | ||
| 575 | /// Generate the current BEM element as a sibling of itself. | ||
| 576 | /// | ||
| 577 | /// It's a shorthand for iro-bem-related-twin-element('~'). | ||
| 578 | /// | ||
| 579 | /// @content | ||
| 580 | /// | ||
| 581 | @mixin iro-bem-sibling-twin-element { | ||
| 582 | @include iro-bem-related-twin-element('~') { | ||
| 583 | @content; | ||
| 584 | } | ||
| 585 | } | ||
| 586 | |||
| 587 | /// | ||
| 588 | /// Generate the current BEM element as a sibling of itself. | ||
| 589 | /// Check the respective mixin documentation for more information. | ||
| 590 | /// | ||
| 591 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 592 | /// | ||
| 593 | /// @see {mixin} iro-bem-sibling-twin-element | ||
| 594 | /// | ||
| 595 | @function iro-bem-sibling-twin-element() { | ||
| 596 | @return iro-bem-related-twin-element('~'); | ||
| 597 | } | ||
| 598 | |||
| 599 | /// | ||
| 600 | /// Generate the current BEM element as a next sibling of itself. | ||
| 601 | /// | ||
| 602 | /// It's a shorthand for iro-bem-related-twin-element('+', $name). | ||
| 603 | /// | ||
| 604 | /// @content | ||
| 605 | /// | ||
| 606 | @mixin iro-bem-next-twin-element { | ||
| 607 | @include iro-bem-related-twin-element('+') { | ||
| 608 | @content; | ||
| 609 | } | ||
| 610 | } | ||
| 611 | |||
| 612 | /// | ||
| 613 | /// Generate the current BEM element as a next sibling of itself. | ||
| 614 | /// Check the respective mixin documentation for more information. | ||
| 615 | /// | ||
| 616 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 617 | /// | ||
| 618 | /// @see {mixin} iro-bem-next-twin-element | ||
| 619 | /// | ||
| 620 | @function iro-bem-next-twin-element() { | ||
| 621 | @return iro-bem-related-twin-element('+'); | ||
| 622 | } | ||
diff --git a/src/bem/_functions.scss b/src/bem/_functions.scss new file mode 100644 index 0000000..4bb95c4 --- /dev/null +++ b/src/bem/_functions.scss  | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | //// | ||
| 2 | /// @group BEM | ||
| 3 | /// | ||
| 4 | /// @access public | ||
| 5 | //// | ||
| 6 | |||
| 7 | /// | ||
| 8 | /// @access private | ||
| 9 | /// | ||
| 10 | @function iro-bem-theme-selector($name, $names...) { | ||
| 11 | $namespace: map-get($iro-bem-namespaces, 'theme'); | ||
| 12 | $selector: null; | ||
| 13 | |||
| 14 | @each $name in join($name, $names) { | ||
| 15 | $sel: '.' + $namespace + '-' + $name; | ||
| 16 | |||
| 17 | @if $selector == null { | ||
| 18 | $selector: join(selector-parse($sel), selector-parse('[class*=\' t-\'] ' + $sel), comma); | ||
| 19 | $selector: join($selector, selector-parse('[class^=\'t-\'] ' + $sel), comma); | ||
| 20 | } @else { | ||
| 21 | $selector: selector-nest($selector, $sel); | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | @return $selector; | ||
| 26 | } | ||
diff --git a/src/bem/_modifier.scss b/src/bem/_modifier.scss new file mode 100644 index 0000000..e1f9507 --- /dev/null +++ b/src/bem/_modifier.scss  | |||
| @@ -0,0 +1,246 @@ | |||
| 1 | //// | ||
| 2 | /// @group BEM | ||
| 3 | /// | ||
| 4 | /// @access public | ||
| 5 | //// | ||
| 6 | |||
| 7 | /// | ||
| 8 | /// Generate a new BEM modifier. | ||
| 9 | /// | ||
| 10 | /// If the parent context is block or element, the modifier will modify said block or element according | ||
| 11 | /// to the BEM naming convention. | ||
| 12 | /// | ||
| 13 | /// If the parent context is a modifier or suffix, then the modifier will depend on said modifier or suffix. | ||
| 14 | /// Depending on $extend, the meaning of this dependency (and the resulting selector) varies: | ||
| 15 | /// If it's false (default), you signalize that the modifier also exists by itself, but it changes its | ||
| 16 | /// behavior when the parent modifier or suffix is set. | ||
| 17 | /// If it's true, you signalize that the modifier extends the parent modifier or suffix and can only be | ||
| 18 | /// used in conjunction with it. | ||
| 19 | /// | ||
| 20 | /// @param {string | list} $name - First element name or list with two items: 1. first element name, 2. bool indicating if the modifier is extending | ||
| 21 | /// @param {string | list} $names - More element names or lists with two items: 1. element name, 2. bool indicating if the modifier is extending | ||
| 22 | /// | ||
| 23 | /// @content | ||
| 24 | /// | ||
| 25 | /// @throw If the element is not preceded by a block, element, modifier or suffix. | ||
| 26 | /// | ||
| 27 | /// @example scss - Modifier that modifies a block or element | ||
| 28 | /// @include iro-bem-component('block') { | ||
| 29 | /// @include iro-bem-modifier('mod') { | ||
| 30 | /// background-color: #eee; | ||
| 31 | /// } | ||
| 32 | /// | ||
| 33 | /// @include iro-bem-element('elem') { | ||
| 34 | /// @include iro-bem-modifier('mod') { | ||
| 35 | /// background-color: #222; | ||
| 36 | /// } | ||
| 37 | /// } | ||
| 38 | /// } | ||
| 39 | /// | ||
| 40 | /// // Generates: | ||
| 41 | /// | ||
| 42 | /// .c-block--mod { | ||
| 43 | /// background-color: #eee; | ||
| 44 | /// } | ||
| 45 | /// | ||
| 46 | /// .c-block__elem--mod { | ||
| 47 | /// background-color: #222; | ||
| 48 | /// } | ||
| 49 | /// | ||
| 50 | /// @example scss - Modifier nested in another modifier, not extending | ||
| 51 | /// @include iro-bem-component('block') { | ||
| 52 | /// @include iro-bem-modifier('mod') { | ||
| 53 | /// background-color: #eee; | ||
| 54 | /// } | ||
| 55 | /// | ||
| 56 | /// @include iro-bem-modifier('dark') { | ||
| 57 | /// /* some definitions */ | ||
| 58 | /// | ||
| 59 | /// @include iro-bem-modifier('mod') { | ||
| 60 | /// background-color: #222; | ||
| 61 | /// } | ||
| 62 | /// } | ||
| 63 | /// } | ||
| 64 | /// | ||
| 65 | /// // Generates: | ||
| 66 | /// | ||
| 67 | /// .c-block--mod { | ||
| 68 | /// background-color: #eee; | ||
| 69 | /// } | ||
| 70 | /// | ||
| 71 | /// .c-block--dark { | ||
| 72 | /// /* some definitions */ | ||
| 73 | /// } | ||
| 74 | /// | ||
| 75 | /// .c-block--dark.c-block--mod { | ||
| 76 | /// background-color: #222; | ||
| 77 | /// } | ||
| 78 | /// | ||
| 79 | /// @example scss - Modifier nested in another modifier, extending | ||
| 80 | /// @include iro-bem-component('block') { | ||
| 81 | /// @include iro-bem-modifier('mod') { | ||
| 82 | /// background-color: #eee; | ||
| 83 | /// } | ||
| 84 | /// | ||
| 85 | /// @include iro-bem-modifier('dark') { | ||
| 86 | /// /* some definitions */ | ||
| 87 | /// | ||
| 88 | /// @include iro-bem-modifier('mod' true) { | ||
| 89 | /// background-color: #222; | ||
| 90 | /// } | ||
| 91 | /// } | ||
| 92 | /// } | ||
| 93 | /// | ||
| 94 | /// // Generates: | ||
| 95 | /// | ||
| 96 | /// .c-block--mod { | ||
| 97 | /// background-color: #eee; | ||
| 98 | /// } | ||
| 99 | /// | ||
| 100 | /// .c-block--dark { | ||
| 101 | /// /* some definitions */ | ||
| 102 | /// } | ||
| 103 | /// | ||
| 104 | /// .c-block--dark--mod { | ||
| 105 | /// background-color: #222; | ||
| 106 | /// } | ||
| 107 | /// | ||
| 108 | @mixin iro-bem-modifier($name, $names...) { | ||
| 109 | $result: iro-bem-modifier($name, $names...); | ||
| 110 | $selector: nth($result, 1); | ||
| 111 | $context: nth($result, 2); | ||
| 112 | |||
| 113 | @include iro-bem-validate( | ||
| 114 | 'modifier', | ||
| 115 | (name: $name, names: $names), | ||
| 116 | $selector, | ||
| 117 | $context | ||
| 118 | ); | ||
| 119 | |||
| 120 | @include iro-context-push($iro-bem-context-id, $context...); | ||
| 121 | @at-root #{$selector} { | ||
| 122 | @content; | ||
| 123 | } | ||
| 124 | @include iro-context-pop($iro-bem-context-id); | ||
| 125 | } | ||
| 126 | |||
| 127 | /// | ||
| 128 | /// Generate a new BEM modifier. Check the respective mixin documentation for more information. | ||
| 129 | /// | ||
| 130 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 131 | /// | ||
| 132 | /// @see {mixin} iro-bem-modifier | ||
| 133 | /// | ||
| 134 | @function iro-bem-modifier($name, $names...) { | ||
| 135 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
| 136 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); | ||
| 137 | |||
| 138 | $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element' 'modifier' 'suffix'); | ||
| 139 | $parent-selector: map-get(nth($parent-context, 2), 'selector'); | ||
| 140 | $selector: (); | ||
| 141 | $parts-data: (); | ||
| 142 | |||
| 143 | @if not iro-selector-suffix-match(&, $parent-selector) { | ||
| 144 | // | ||
| 145 | // The current selector doesn't match the parent selector. | ||
| 146 | // The user manually added a selector between parent context and this modifier call. | ||
| 147 | // This case is forbidden because any outcome semantically wouldn't make sense: | ||
| 148 | // - {b,e,m,s} [manual selector] {b,e,m,s}--modifier | ||
| 149 | // - {b,e,m,s}--modifier [manual selector] | ||
| 150 | // The first case would make the modifier behave like an element. | ||
| 151 | // The second case is unintuitive, the code would be more clear by nesting the manual | ||
| 152 | // selector in the modifier instead. | ||
| 153 | // | ||
| 154 | |||
| 155 | @error 'A modifier must be an immediate child of the parent context'; | ||
| 156 | } | ||
| 157 | |||
| 158 | @each $name in iro-list-prepend($names, $name) { | ||
| 159 | $extend: false; | ||
| 160 | @if type-of($name) == list { | ||
| 161 | $extend: nth($name, 2); | ||
| 162 | $name: nth($name, 1); | ||
| 163 | } | ||
| 164 | |||
| 165 | @if index('block' 'element', nth($parent-context, 1)) or $extend == true { | ||
| 166 | // | ||
| 167 | // Either the parent context is block or element, or a modifier or suffix | ||
| 168 | // is to be extended. The modifier part can simply be appended. | ||
| 169 | // Possible outcomes: | ||
| 170 | // - {b,e,m,s}--modifier | ||
| 171 | // | ||
| 172 | |||
| 173 | $sel: selector-append(&, $iro-bem-modifier-separator + $name); | ||
| 174 | $selector: join($selector, $sel, comma); | ||
| 175 | $parts-data: append($parts-data, ( | ||
| 176 | 'name': $name, | ||
| 177 | 'selector': $sel | ||
| 178 | )); | ||
| 179 | } @else { | ||
| 180 | // | ||
| 181 | // Parent context is modifier, suffix or state and $extend is false. | ||
| 182 | // | ||
| 183 | |||
| 184 | $be-context: iro-context-get($iro-bem-context-id, 'block' 'element'); | ||
| 185 | |||
| 186 | @if nth($be-context, 1) == 'element' { | ||
| 187 | // | ||
| 188 | // Latest context is element. Since element contexts can consist of multiple single | ||
| 189 | // elements, inspect all elements and append its selector with the suffix "--$name". | ||
| 190 | // This has to be done with string comparison since none of Sass selector functions | ||
| 191 | // is of use here. | ||
| 192 | // Possible outcomes: | ||
| 193 | // - {m,s}.{e}--modifier | ||
| 194 | // | ||
| 195 | |||
| 196 | $nsel: (); | ||
| 197 | |||
| 198 | @each $elem-part-data in map-get(nth($be-context, 2), 'parts') { | ||
| 199 | $elem-part-selector: map-get($elem-part-data, 'selector'); | ||
| 200 | |||
| 201 | $sel: (); | ||
| 202 | @each $s in & { | ||
| 203 | @each $ps in $elem-part-selector { | ||
| 204 | @if str-index(inspect($s), inspect($ps) + $iro-bem-modifier-separator) or str-index(inspect($s), inspect($ps) + $iro-bem-suffix-separator) { | ||
| 205 | $sel: join($sel, selector-unify($s, selector-append($ps, $iro-bem-modifier-separator + $name)), comma); | ||
| 206 | } | ||
| 207 | } | ||
| 208 | } | ||
| 209 | @if length($sel) == 0 { | ||
| 210 | @error 'Could not generate modifier selector.'; | ||
| 211 | } | ||
| 212 | |||
| 213 | $nsel: join($nsel, $sel, comma); | ||
| 214 | } | ||
| 215 | |||
| 216 | $selector: join($selector, $nsel, comma); | ||
| 217 | $parts-data: append($parts-data, ( | ||
| 218 | 'name': $name, | ||
| 219 | 'selector': $nsel | ||
| 220 | )); | ||
| 221 | } @else { | ||
| 222 | // | ||
| 223 | // Latest context is block. Just append the modifier part. | ||
| 224 | // Possible outcomes: | ||
| 225 | // - {m,s}.{b}--modifier | ||
| 226 | // | ||
| 227 | |||
| 228 | $block-base-selector: map-get(nth($be-context, 2), 'base-selector'); | ||
| 229 | |||
| 230 | $sel: selector-append(&, $block-base-selector, $iro-bem-modifier-separator + $name); | ||
| 231 | $selector: join($selector, $sel, comma); | ||
| 232 | $parts-data: append($parts-data, ( | ||
| 233 | 'name': $name, | ||
| 234 | 'selector': $sel | ||
| 235 | )); | ||
| 236 | } | ||
| 237 | } | ||
| 238 | } | ||
| 239 | |||
| 240 | $context: 'modifier', ( | ||
| 241 | 'parts': $parts-data, | ||
| 242 | 'selector': $selector | ||
| 243 | ); | ||
| 244 | |||
| 245 | @return $selector $context; | ||
| 246 | } | ||
diff --git a/src/bem/_multi.scss b/src/bem/_multi.scss new file mode 100644 index 0000000..9e47ce4 --- /dev/null +++ b/src/bem/_multi.scss  | |||
| @@ -0,0 +1,131 @@ | |||
| 1 | //// | ||
| 2 | /// @group BEM | ||
| 3 | /// | ||
| 4 | /// @access public | ||
| 5 | //// | ||
| 6 | |||
| 7 | /// | ||
| 8 | /// Generate multiple entities (BEM or not) at once. | ||
| 9 | /// | ||
| 10 | /// NOTE: This mixin does not generate perfectly optimized selectors in order to keep track of contexts. | ||
| 11 | /// | ||
| 12 | /// @param {string | list} $first - First selector. Either a string for a manual selector, or a list with the first items standing for a BEM selector function (optionally suffixed by a colon) and other items being passed as arguments to said function. | ||
| 13 | /// @param {string | list} $others - Other selectors. Either a string for a manual selector, or a list with the first items standing for a BEM selector function (optionally suffixed by a colon) and other items being passed as arguments to said function. | ||
| 14 | /// | ||
| 15 | /// @content | ||
| 16 | /// | ||
| 17 | /// @example scss - Creating multiple elements, a modifier and an anchor | ||
| 18 | /// @include iro-bem-object('buttonstrip') { | ||
| 19 | /// display: none; | ||
| 20 | /// | ||
| 21 | /// @include iro-bem-multi('modifier' 'mod', 'element' 'button' 'separator', '> a') { | ||
| 22 | /// display: block; | ||
| 23 | /// } | ||
| 24 | /// } | ||
| 25 | /// | ||
| 26 | /// // Generates: | ||
| 27 | /// | ||
| 28 | /// .o-buttonstrip { | ||
| 29 | /// display: none; | ||
| 30 | /// } | ||
| 31 | /// | ||
| 32 | /// .o-buttonstrip--mod { | ||
| 33 | /// display: block; | ||
| 34 | /// } | ||
| 35 | /// | ||
| 36 | /// .o-buttonstrip__button, { | ||
| 37 | /// .o-buttonstrip__separator { | ||
| 38 | /// display: block; | ||
| 39 | /// } | ||
| 40 | /// | ||
| 41 | /// .o-buttonstrip > a { | ||
| 42 | /// display: block; | ||
| 43 | /// } | ||
| 44 | /// | ||
| 45 | /// @example scss - Creating multiple elements, a modifier and an anchor - optional colons included | ||
| 46 | /// @include iro-bem-object('buttonstrip') { | ||
| 47 | /// display: none; | ||
| 48 | /// | ||
| 49 | /// @include iro-bem-multi('modifier:' 'mod', 'element:' 'button' 'separator', '> a') { | ||
| 50 | /// display: block; | ||
| 51 | /// } | ||
| 52 | /// } | ||
| 53 | /// | ||
| 54 | /// // Generates: | ||
| 55 | /// | ||
| 56 | /// .o-buttonstrip { | ||
| 57 | /// display: none; | ||
| 58 | /// } | ||
| 59 | /// | ||
| 60 | /// .o-buttonstrip--mod { | ||
| 61 | /// display: block; | ||
| 62 | /// } | ||
| 63 | /// | ||
| 64 | /// .o-buttonstrip__button, { | ||
| 65 | /// .o-buttonstrip__separator { | ||
| 66 | /// display: block; | ||
| 67 | /// } | ||
| 68 | /// | ||
| 69 | /// .o-buttonstrip > a { | ||
| 70 | /// display: block; | ||
| 71 | /// } | ||
| 72 | /// | ||
| 73 | @mixin iro-bem-multi($first, $others...) { | ||
| 74 | @include iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
| 75 | |||
| 76 | @each $entity in iro-list-prepend($others, $first) { | ||
| 77 | $is-manual-selector: false; | ||
| 78 | |||
| 79 | @if type-of($entity) == string and not function-exists('iro-bem-' + $entity) { | ||
| 80 | $is-manual-selector: true; | ||
| 81 | } | ||
| 82 | |||
| 83 | @if $is-manual-selector { | ||
| 84 | $sel: if(&, selector-nest(&, $entity), selector-parse($entity)); | ||
| 85 | |||
| 86 | @at-root #{$sel} { | ||
| 87 | @content; | ||
| 88 | } | ||
| 89 | } @else { | ||
| 90 | $entity-func-id: null; | ||
| 91 | |||
| 92 | @if type-of($entity) == list { | ||
| 93 | $entity-func-id: nth($entity, 1); | ||
| 94 | $entity: iro-list-slice($entity, 2); | ||
| 95 | } @else { | ||
| 96 | $entity-func-id: $entity; | ||
| 97 | $entity: (); | ||
| 98 | } | ||
| 99 | |||
| 100 | @if str-slice($entity-func-id, str-length($entity-func-id)) == ':' { | ||
| 101 | $entity-func-id: str-slice($entity-func-id, 1, str-length($entity-func-id) - 1); | ||
| 102 | } | ||
| 103 | |||
| 104 | $sel-func: null; | ||
| 105 | |||
| 106 | @if function-exists('iro-bem-' + $entity-func-id) { | ||
| 107 | $sel-func: get-function('iro-bem-' + $entity-func-id); | ||
| 108 | } @else if function-exists($entity-func-id) { | ||
| 109 | $sel-func: get-function($entity-func-id); | ||
| 110 | } | ||
| 111 | |||
| 112 | @if $sel-func == null { | ||
| 113 | @error 'Function "#{inspect($entity-func-id)}" was not found.'; | ||
| 114 | } | ||
| 115 | |||
| 116 | $entity-result: call($sel-func, $entity...); | ||
| 117 | $entity-result-selector: nth($entity-result, 1); | ||
| 118 | $entity-result-context: nth($entity-result, 2); | ||
| 119 | |||
| 120 | @if $entity-result-context != null { | ||
| 121 | @include iro-context-push($iro-bem-context-id, $entity-result-context...); | ||
| 122 | } | ||
| 123 | @at-root #{$entity-result-selector} { | ||
| 124 | @content; | ||
| 125 | } | ||
| 126 | @if $entity-result-context != null { | ||
| 127 | @include iro-context-pop($iro-bem-context-id); | ||
| 128 | } | ||
| 129 | } | ||
| 130 | } | ||
| 131 | } | ||
diff --git a/src/bem/_state.scss b/src/bem/_state.scss new file mode 100644 index 0000000..4a85bbb --- /dev/null +++ b/src/bem/_state.scss  | |||
| @@ -0,0 +1,146 @@ | |||
| 1 | //// | ||
| 2 | /// @group BEM | ||
| 3 | /// | ||
| 4 | /// @access public | ||
| 5 | //// | ||
| 6 | |||
| 7 | /// | ||
| 8 | /// Create a new state rule. | ||
| 9 | /// | ||
| 10 | /// @param {string} $state - First state name | ||
| 11 | /// @param {list} $states - List of more state names | ||
| 12 | /// | ||
| 13 | /// @content | ||
| 14 | /// | ||
| 15 | /// @example scss - Using single is-state | ||
| 16 | /// @include iro-bem-object('menu') { | ||
| 17 | /// display: none; | ||
| 18 | /// | ||
| 19 | /// @include iro-bem-state('is', open') { | ||
| 20 | /// display: block; | ||
| 21 | /// } | ||
| 22 | /// } | ||
| 23 | /// | ||
| 24 | /// // Generates: | ||
| 25 | /// | ||
| 26 | /// .o-menu { | ||
| 27 | /// display: none; | ||
| 28 | /// } | ||
| 29 | /// | ||
| 30 | /// .o-menu.is-open { | ||
| 31 | /// display: block; | ||
| 32 | /// } | ||
| 33 | /// | ||
| 34 | /// @example scss - Using multiple is-states | ||
| 35 | /// @include iro-bem-object('menu') { | ||
| 36 | /// display: none; | ||
| 37 | /// | ||
| 38 | /// @include iro-bem-state('is', open', 'visible') { | ||
| 39 | /// display: block; | ||
| 40 | /// } | ||
| 41 | /// } | ||
| 42 | /// | ||
| 43 | /// // Generates: | ||
| 44 | /// | ||
| 45 | /// .o-menu { | ||
| 46 | /// display: none; | ||
| 47 | /// } | ||
| 48 | /// | ||
| 49 | /// .o-menu.is-open, | ||
| 50 | /// .o-menu.is-visible { | ||
| 51 | /// display: block; | ||
| 52 | /// } | ||
| 53 | /// | ||
| 54 | @mixin iro-bem-state($prefix, $state, $states...) { | ||
| 55 | $result: iro-bem-state($prefix, $state, $states...); | ||
| 56 | $selector: nth($result, 1); | ||
| 57 | $context: nth($result, 2); | ||
| 58 | |||
| 59 | @include iro-bem-validate( | ||
| 60 | 'state', | ||
| 61 | (prefix: $prefix, state: $state, states: $states), | ||
| 62 | $selector, | ||
| 63 | $context | ||
| 64 | ); | ||
| 65 | |||
| 66 | @include iro-context-push($iro-bem-context-id, $context...); | ||
| 67 | @at-root #{$selector} { | ||
| 68 | @content; | ||
| 69 | } | ||
| 70 | @include iro-context-pop($iro-bem-context-id); | ||
| 71 | } | ||
| 72 | |||
| 73 | /// | ||
| 74 | /// Generate a new state. | ||
| 75 | /// | ||
| 76 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 77 | /// | ||
| 78 | /// @see {mixin} iro-bem-has | ||
| 79 | /// | ||
| 80 | @function iro-bem-state($prefix, $state, $states...) { | ||
| 81 | $selector: (); | ||
| 82 | $parts-data: (); | ||
| 83 | |||
| 84 | @each $state in join($state, $states) { | ||
| 85 | $sel: selector-parse('.#{$prefix}-#{$state}'); | ||
| 86 | @if & { | ||
| 87 | $sel: selector-append(&, $sel); | ||
| 88 | } | ||
| 89 | $selector: join($selector, $sel, comma); | ||
| 90 | $parts-data: append($parts-data, ( | ||
| 91 | 'name': $state, | ||
| 92 | 'selector': $sel | ||
| 93 | )); | ||
| 94 | } | ||
| 95 | |||
| 96 | $context: 'state', ( | ||
| 97 | 'parts': $parts-data, | ||
| 98 | 'selector': $selector | ||
| 99 | ); | ||
| 100 | |||
| 101 | @return $selector $context; | ||
| 102 | } | ||
| 103 | |||
| 104 | /// | ||
| 105 | /// Create a new has-state modifier. | ||
| 106 | /// | ||
| 107 | /// It's a shorthand for iro-bem-state('is', $state, $states...). | ||
| 108 | /// | ||
| 109 | @mixin iro-bem-is($state, $states...) { | ||
| 110 | @include iro-bem-state('is', $state, $states...) { | ||
| 111 | @content; | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | /// | ||
| 116 | /// Generate a new is-state modifier. Check the respective mixin documentation for more information. | ||
| 117 | /// | ||
| 118 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 119 | /// | ||
| 120 | /// @see {mixin} iro-bem-is | ||
| 121 | /// | ||
| 122 | @function iro-bem-is($state, $states...) { | ||
| 123 | @return iro-bem-state('is', $state, $states...); | ||
| 124 | } | ||
| 125 | |||
| 126 | /// | ||
| 127 | /// Create a new has-state modifier. | ||
| 128 | /// | ||
| 129 | /// It's a shorthand for iro-bem-state('has', $state, $states...). | ||
| 130 | /// | ||
| 131 | @mixin iro-bem-has($state, $states...) { | ||
| 132 | @include iro-bem-state('has', $state, $states...) { | ||
| 133 | @content; | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 | /// | ||
| 138 | /// Generate a new has-state modifier. Check the respective mixin documentation for more information. | ||
| 139 | /// | ||
| 140 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 141 | /// | ||
| 142 | /// @see {mixin} iro-bem-has | ||
| 143 | /// | ||
| 144 | @function iro-bem-has($state, $states...) { | ||
| 145 | @return iro-bem-state('has', $state, $states...); | ||
| 146 | } | ||
diff --git a/src/bem/_suffix.scss b/src/bem/_suffix.scss new file mode 100644 index 0000000..b103c9f --- /dev/null +++ b/src/bem/_suffix.scss  | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | //// | ||
| 2 | /// @group BEM | ||
| 3 | /// | ||
| 4 | /// @access public | ||
| 5 | //// | ||
| 6 | |||
| 7 | /// | ||
| 8 | /// Generate a new suffix. | ||
| 9 | /// | ||
| 10 | /// @param {string} $name - Suffix name | ||
| 11 | /// | ||
| 12 | /// @content | ||
| 13 | /// | ||
| 14 | /// @throw If the element is not preceded by a block or modifier. | ||
| 15 | /// | ||
| 16 | /// @example scss - Using a suffix | ||
| 17 | /// @include iro-bem-utility('hidden') { | ||
| 18 | /// display: none; | ||
| 19 | /// | ||
| 20 | /// @media (max-width: 320px) { | ||
| 21 | /// @include iro-bem-suffix('phone') { | ||
| 22 | /// display: none; | ||
| 23 | /// } | ||
| 24 | /// } | ||
| 25 | /// | ||
| 26 | /// @media (max-width: 768px) { | ||
| 27 | /// @include iro-bem-suffix('tablet') { | ||
| 28 | /// display: none; | ||
| 29 | /// } | ||
| 30 | /// } | ||
| 31 | /// } | ||
| 32 | /// | ||
| 33 | /// // Generates: | ||
| 34 | /// | ||
| 35 | /// .u-hidden { | ||
| 36 | /// display: none; | ||
| 37 | /// } | ||
| 38 | /// | ||
| 39 | /// @media (max-width: 320px) { | ||
| 40 | /// .u-hidden@phone { | ||
| 41 | /// display: none; | ||
| 42 | /// } | ||
| 43 | /// } | ||
| 44 | /// | ||
| 45 | /// @media (max-width: 768px) { | ||
| 46 | /// .u-hidden@tablet { | ||
| 47 | /// display: none; | ||
| 48 | /// } | ||
| 49 | /// } | ||
| 50 | /// | ||
| 51 | @mixin iro-bem-suffix($name) { | ||
| 52 | $result: iro-bem-suffix($name); | ||
| 53 | $selector: nth($result, 1); | ||
| 54 | $context: nth($result, 2); | ||
| 55 | |||
| 56 | @include iro-bem-validate( | ||
| 57 | 'suffix', | ||
| 58 | (name: $name), | ||
| 59 | $selector, | ||
| 60 | $context | ||
| 61 | ); | ||
| 62 | |||
| 63 | @include iro-context-push($iro-bem-context-id, $context...); | ||
| 64 | @at-root #{$selector} { | ||
| 65 | @content; | ||
| 66 | } | ||
| 67 | @include iro-context-pop($iro-bem-context-id); | ||
| 68 | } | ||
| 69 | |||
| 70 | /// | ||
| 71 | /// Generate a new suffix. Check the respective mixin documentation for more information. | ||
| 72 | /// | ||
| 73 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 74 | /// | ||
| 75 | /// @see {mixin} iro-bem-suffix | ||
| 76 | /// | ||
| 77 | @function iro-bem-suffix($name) { | ||
| 78 | // | ||
| 79 | // Suffixes can be used on block, element and modifier. | ||
| 80 | // | ||
| 81 | |||
| 82 | $noop: iro-context-assert-stack-count($iro-bem-context-id, $iro-bem-max-depth); | ||
| 83 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); | ||
| 84 | $noop: iro-context-assert-stack-must-not-contain($iro-bem-context-id, 'suffix'); | ||
| 85 | |||
| 86 | $parent-context: iro-context-get($iro-bem-context-id, 'block' 'element' 'modifier'); | ||
| 87 | $parent-selector: map-get(nth($parent-context, 2), 'selector'); | ||
| 88 | |||
| 89 | @if not iro-selector-suffix-match(&, $parent-selector) { | ||
| 90 | // | ||
| 91 | // The current selector doesn't match the parent selector. | ||
| 92 | // The user manually added a selector between parent context and this suffix call. | ||
| 93 | // This case is forbidden because any outcome semantically wouldn't make sense: | ||
| 94 | // - {b,e,m} [manual selector] {b,e,m}@suffix | ||
| 95 | // - {b,e,m}@suffix [manual selector] | ||
| 96 | // The first case would make the modifier behave like an element. | ||
| 97 | // The second case is unintuitive, the code would be more clear by nesting the manual | ||
| 98 | // selector in the suffix instead. | ||
| 99 | // | ||
| 100 | |||
| 101 | @error 'A suffix must be an immediate child of the parent context'; | ||
| 102 | } | ||
| 103 | |||
| 104 | // | ||
| 105 | // The suffix part can simply be appended. | ||
| 106 | // Possible outcomes: | ||
| 107 | // - {b,e,m}@suffix | ||
| 108 | // | ||
| 109 | |||
| 110 | $selector: selector-append(&, $iro-bem-suffix-separator + $name); | ||
| 111 | |||
| 112 | $context: 'suffix', ( | ||
| 113 | 'name': $name, | ||
| 114 | 'selector': $selector | ||
| 115 | ); | ||
| 116 | |||
| 117 | @return $selector $context; | ||
| 118 | } | ||
diff --git a/src/bem/_theme.scss b/src/bem/_theme.scss new file mode 100644 index 0000000..b4bcc76 --- /dev/null +++ b/src/bem/_theme.scss  | |||
| @@ -0,0 +1,61 @@ | |||
| 1 | //// | ||
| 2 | /// @group BEM | ||
| 3 | /// | ||
| 4 | /// @access public | ||
| 5 | //// | ||
| 6 | |||
| 7 | /// | ||
| 8 | /// Declare new rules for the current block for when this theme is active. | ||
| 9 | /// | ||
| 10 | /// @param {string} $name - First theme block name | ||
| 11 | /// @param {string} $names - More theme block names | ||
| 12 | /// | ||
| 13 | /// @content | ||
| 14 | /// | ||
| 15 | @mixin iro-bem-at-theme($name, $names...) { | ||
| 16 | $result: iro-bem-at-theme($name, $names...); | ||
| 17 | $selector: nth($result, 1); | ||
| 18 | $context: nth($result, 2); | ||
| 19 | |||
| 20 | @include iro-bem-validate( | ||
| 21 | 'at-theme', | ||
| 22 | (name: $name, names: $names), | ||
| 23 | $selector, | ||
| 24 | $context | ||
| 25 | ); | ||
| 26 | |||
| 27 | @include iro-context-push($iro-bem-context-id, $context...); | ||
| 28 | @at-root #{$selector} { | ||
| 29 | @content; | ||
| 30 | } | ||
| 31 | @include iro-context-pop($iro-bem-context-id); | ||
| 32 | } | ||
| 33 | |||
| 34 | /// | ||
| 35 | /// Declare new rules for the current block for when this theme is active. | ||
| 36 | /// Check the respective mixin documentation for more information. | ||
| 37 | /// | ||
| 38 | /// @return {list} A list with two items: 1. selector, 2. context or `null` | ||
| 39 | /// | ||
| 40 | /// @see {mixin} iro-bem-at-theme | ||
| 41 | /// | ||
| 42 | @function iro-bem-at-theme($name, $names...) { | ||
| 43 | $noop: iro-context-assert-stack-must-contain($iro-bem-context-id, 'block'); | ||
| 44 | |||
| 45 | $parent-context: iro-context-get($iro-bem-context-id, 'block'); | ||
| 46 | $parent-selector: map-get(nth($parent-context, 2), 'selector'); | ||
| 47 | |||
| 48 | @if not iro-selector-suffix-match(&, $parent-selector) { | ||
| 49 | @error 'An at-theme rule must be an immediate child of a block'; | ||
| 50 | } | ||
| 51 | |||
| 52 | $selector: iro-bem-theme-selector($name, $names...); | ||
| 53 | $selector: selector-nest($selector, &); | ||
| 54 | |||
| 55 | $context: 'at-theme', ( | ||
| 56 | 'name': join($name, $names), | ||
| 57 | 'selector': $selector | ||
| 58 | ); | ||
| 59 | |||
| 60 | @return $selector $context; | ||
| 61 | } | ||
diff --git a/src/bem/_validators.scss b/src/bem/_validators.scss new file mode 100644 index 0000000..eb09a60 --- /dev/null +++ b/src/bem/_validators.scss  | |||
| @@ -0,0 +1,176 @@ | |||
| 1 | //// | ||
| 2 | /// Validators are custom functions that will be called before a BEM entity is created. | ||
| 3 | /// They check if the current mixin usage is valid or not and thus they are a flexible way to | ||
| 4 | /// let you implement your own rules. | ||
| 5 | /// | ||
| 6 | /// Validator functions receive the following information: | ||
| 7 | /// - BEM entity type | ||
| 8 | /// - Arguments passed to the mixin | ||
| 9 | /// - The generated selector | ||
| 10 | /// - The generated context, if any | ||
| 11 | /// | ||
| 12 | /// Additionally, the context stack used by the BEM system can be examined. | ||
| 13 | /// | ||
| 14 | /// @group BEM | ||
| 15 | /// | ||
| 16 | /// @access public | ||
| 17 | //// | ||
| 18 | |||
| 19 | /// | ||
| 20 | /// A list of validator functions. | ||
| 21 | /// | ||
| 22 | /// @type list | ||
| 23 | /// | ||
| 24 | /// @access private | ||
| 25 | /// | ||
| 26 | $iro-bem-validators: (); | ||
| 27 | |||
| 28 | /// | ||
| 29 | /// Register one or multiple validator functions. | ||
| 30 | /// | ||
| 31 | /// A validator function is a function that accepts 4 arguments: | ||
| 32 | /// 1. BEM entity type (string) | ||
| 33 | /// 2. Arguments passed to the mixin (map) | ||
| 34 | /// 3. The generated selector (selector) | ||
| 35 | /// 4. The generated context (list, may be null) | ||
| 36 | /// | ||
| 37 | /// The function must return a list with two items: | ||
| 38 | /// 1. `true` if the mixin usage is valid, otherwise `false`, | ||
| 39 | /// 2. a string with a rejection reason (empty if the usage is valid). | ||
| 40 | /// | ||
| 41 | /// @param {string} $func-name - First function name. | ||
| 42 | /// @param {string} $func-names - Other function names. | ||
| 43 | /// | ||
| 44 | @mixin iro-bem-add-validator($func-name, $func-names...) { | ||
| 45 | $noop: iro-bem-add-validator($func-name, $func-names...); | ||
| 46 | } | ||
| 47 | |||
| 48 | /// | ||
| 49 | /// Register one or multiple validator functions. Check the respective mixin documentation for more information. | ||
| 50 | /// | ||
| 51 | /// @see {mixin} iro-bem-add-validator | ||
| 52 | /// | ||
| 53 | @function iro-bem-add-validator($func-name, $func-names...) { | ||
| 54 | @each $fn-name in join($func-name, $func-names) { | ||
| 55 | $fn: get-function($fn-name); | ||
| 56 | $iro-bem-validators: map-merge($iro-bem-validators, ($fn-name: $fn)) !global; | ||
| 57 | } | ||
| 58 | @return null; | ||
| 59 | } | ||
| 60 | |||
| 61 | /// | ||
| 62 | /// Unregister one or multiple validator functions. | ||
| 63 | /// | ||
| 64 | /// @param {string} $func-name - First function name. | ||
| 65 | /// @param {string} $func-names - Other function names. | ||
| 66 | /// | ||
| 67 | @mixin iro-bem-remove-validator($func-name, $func-names...) { | ||
| 68 | $noop: iro-bem-remove-validator($func-name, $func-names...); | ||
| 69 | } | ||
| 70 | |||
| 71 | /// | ||
| 72 | /// Unregister one or multiple validator functions. Check the respective mixin documentation for more information. | ||
| 73 | /// | ||
| 74 | /// @see {mixin} iro-bem-remove-validator | ||
| 75 | /// | ||
| 76 | @function iro-bem-remove-validator($func-name, $func-names...) { | ||
| 77 | $iro-bem-validators: map-remove($iro-bem-validators, $func-name, $func-names...) !global; | ||
| 78 | @return null; | ||
| 79 | } | ||
| 80 | |||
| 81 | /// | ||
| 82 | /// @access private | ||
| 83 | /// | ||
| 84 | @mixin iro-bem-validate($type, $args, $selector, $context) { | ||
| 85 | @each $id, $fn in $iro-bem-validators { | ||
| 86 | $result: call($fn, $type, $args, $selector, $context); | ||
| 87 | @if not nth($result, 1) { | ||
| 88 | @error 'A BEM validator rejected this mixin usage due to the following reason: #{nth($result, 2)}'; | ||
| 89 | } | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | // | ||
| 94 | // --------------------------------------------------------------------------------------------------------- | ||
| 95 | // Built-in validators | ||
| 96 | // --------------------------------------------------------------------------------------------------------- | ||
| 97 | // | ||
| 98 | |||
| 99 | /// | ||
| 100 | /// A validator that makes sure blocks are declared in the right order, determined by the | ||
| 101 | /// namespace used. | ||
| 102 | /// | ||
| 103 | @function iro-bem-validator--enforce-namespace-order($type, $args, $selector, $context) { | ||
| 104 | @if not global-variable-exists(iro-bem-namespace-order) { | ||
| 105 | $iro-bem-namespace-order: map-keys($iro-bem-namespaces) !global; | ||
| 106 | } | ||
| 107 | @if not global-variable-exists(iro-bem-cur-namespace-index) { | ||
| 108 | $iro-bem-cur-namespace-index: 1 !global; | ||
| 109 | } | ||
| 110 | |||
| 111 | @if $type == 'block' { | ||
| 112 | $block-type: map-get($args, 'type'); | ||
| 113 | |||
| 114 | @if $block-type != null { | ||
| 115 | $ns-index: index($iro-bem-namespace-order, $block-type); | ||
| 116 | |||
| 117 | @if $ns-index != null { | ||
| 118 | @if $ns-index < $iro-bem-cur-namespace-index { | ||
| 119 | @return false 'Namespace "#{$block-type}" comes before current namespace #{nth($iro-bem-namespace-order, $iro-bem-cur-namespace-index)}'; | ||
| 120 | } | ||
| 121 | |||
| 122 | $iro-bem-cur-namespace-index: $ns-index !global; | ||
| 123 | } | ||
| 124 | } | ||
| 125 | } | ||
| 126 | |||
| 127 | @return true ''; | ||
| 128 | } | ||
| 129 | |||
| 130 | /// | ||
| 131 | /// A validator that makes all BEM entities immutable. | ||
| 132 | /// | ||
| 133 | @function iro-bem-validator--immutable-entities($type, $args, $selector, $context) { | ||
| 134 | @if not global-variable-exists(iro-bem-generated-selectors) { | ||
| 135 | $iro-bem-generated-selectors: () !global; | ||
| 136 | } | ||
| 137 | |||
| 138 | $block-name: null; | ||
| 139 | $block-type: null; | ||
| 140 | $block-id: null; | ||
| 141 | |||
| 142 | @if $type == 'block' { | ||
| 143 | $block-name: map-get($args, 'name'); | ||
| 144 | $block-type: map-get($args, 'type'); | ||
| 145 | } @else { | ||
| 146 | $block-context: iro-context-get($iro-bem-context-id, 'block'); | ||
| 147 | $block-name: map-get(nth($block-context, 2), 'name'); | ||
| 148 | $block-type: map-get(nth($block-context, 2), 'type'); | ||
| 149 | } | ||
| 150 | |||
| 151 | @if $block-type != null { | ||
| 152 | $block-id: $block-name + '_' + $block-type; | ||
| 153 | } @else { | ||
| 154 | $block-id: $block-name; | ||
| 155 | } | ||
| 156 | |||
| 157 | @if $type == 'block' { | ||
| 158 | @if map-has-key($iro-bem-generated-selectors, $block-id) { | ||
| 159 | @return false 'Entity "#{$type}" with arguments [ #{iro-map-print($args)} ] was already defined.'; | ||
| 160 | } | ||
| 161 | |||
| 162 | $iro-bem-generated-selectors: map-merge($iro-bem-generated-selectors, ($block-id: ())) !global; | ||
| 163 | } @else { | ||
| 164 | $selectors: map-get($iro-bem-generated-selectors, $block-id); | ||
| 165 | |||
| 166 | @if index($selectors, $selector) { | ||
| 167 | @return false 'Entity "#{$type}" with arguments [ #{iro-map-print($args)} ] was already defined.'; | ||
| 168 | } | ||
| 169 | |||
| 170 | $selectors: append($selectors, $selector); | ||
| 171 | |||
| 172 | $iro-bem-generated-selectors: map-merge($iro-bem-generated-selectors, ($block-id: $selectors)) !global; | ||
| 173 | } | ||
| 174 | |||
| 175 | @return true ''; | ||
| 176 | } | ||
diff --git a/src/bem/_vars.scss b/src/bem/_vars.scss new file mode 100644 index 0000000..5942d4f --- /dev/null +++ b/src/bem/_vars.scss  | |||
| @@ -0,0 +1,108 @@ | |||
| 1 | //// | ||
| 2 | /// @group BEM | ||
| 3 | /// | ||
| 4 | /// @access public | ||
| 5 | //// | ||
| 6 | |||
| 7 | /// | ||
| 8 | /// Separating character sequence for elements. | ||
| 9 | /// | ||
| 10 | /// @type string | ||
| 11 | /// | ||
| 12 | $iro-bem-element-separator: '__' !default; | ||
| 13 | |||
| 14 | /// | ||
| 15 | /// Separating character sequence for modifiers. | ||
| 16 | /// | ||
| 17 | /// @type string | ||
| 18 | /// | ||
| 19 | $iro-bem-modifier-separator: '--' !default; | ||
| 20 | |||
| 21 | /// | ||
| 22 | /// Separating character sequence for BEMIT suffixes. | ||
| 23 | /// | ||
| 24 | /// @type string | ||
| 25 | /// | ||
| 26 | $iro-bem-suffix-separator: '\\@' !default; | ||
| 27 | |||
| 28 | /// | ||
| 29 | /// Prefixes for all BEMIT namespaces. | ||
| 30 | /// | ||
| 31 | /// @prop {string} utility ['u'] - Utility namespace | ||
| 32 | /// @prop {string} object ['o'] - Object namespace | ||
| 33 | /// @prop {string} component ['c'] - Component namespace | ||
| 34 | /// @prop {string} layout ['l'] - Layout namespace | ||
| 35 | /// @prop {string} scope ['s'] - Scope namespace | ||
| 36 | /// @prop {string} theme ['t'] - Theme namespace | ||
| 37 | /// @prop {string} js ['js'] - JS namespace | ||
| 38 | /// @prop {string} qa ['qa'] - QA namespace | ||
| 39 | /// @prop {string} hack ['_'] - Hack namespace | ||
| 40 | /// | ||
| 41 | /// @type map | ||
| 42 | /// | ||
| 43 | $iro-bem-namespaces: ( | ||
| 44 | object: 'o', | ||
| 45 | component: 'c', | ||
| 46 | layout: 'l', | ||
| 47 | scope: 's', | ||
| 48 | theme: 't', | ||
| 49 | utility: 'u', | ||
| 50 | js: 'js', | ||
| 51 | qa: 'qa', | ||
| 52 | hack: '_' | ||
| 53 | ) !default; | ||
| 54 | |||
| 55 | /// | ||
| 56 | /// A list of all generated blocks. | ||
| 57 | /// | ||
| 58 | /// @type list | ||
| 59 | /// | ||
| 60 | /// @access private | ||
| 61 | /// | ||
| 62 | $iro-bem-blocks: (); | ||
| 63 | |||
| 64 | /// | ||
| 65 | /// Maximum nesting depth of BEM mixins. The large default value means there is no effective limit. | ||
| 66 | /// | ||
| 67 | /// @type number | ||
| 68 | /// | ||
| 69 | $iro-bem-max-depth: 99 !default; | ||
| 70 | |||
| 71 | /// | ||
| 72 | /// Indicates how nested elements should be handled. | ||
| 73 | /// | ||
| 74 | /// 'allow' means elements will be nested, i.e. the result will be {e} {b}__element. | ||
| 75 | /// 'disallow' means an error will be emitted. | ||
| 76 | /// 'append' means the element name will be appended to the parent element, i.e. the result will be {e}__element. | ||
| 77 | /// Any other value is treated as 'allow'. | ||
| 78 | /// | ||
| 79 | /// @type string | ||
| 80 | /// | ||
| 81 | $iro-bem-element-nesting-policy: 'allow' !default; | ||
| 82 | |||
| 83 | /// | ||
| 84 | /// Context ID used for all BEM-related mixins. | ||
| 85 | /// | ||
| 86 | /// @type string | ||
| 87 | /// | ||
| 88 | $iro-bem-context-id: 'bem' !default; | ||
| 89 | |||
| 90 | /// | ||
| 91 | /// Debug mode. | ||
| 92 | /// | ||
| 93 | /// @type bool | ||
| 94 | /// | ||
| 95 | $iro-bem-debug: false !default; | ||
| 96 | |||
| 97 | /// | ||
| 98 | /// Colors assigned to namespaces. | ||
| 99 | /// | ||
| 100 | /// @type map | ||
| 101 | /// | ||
| 102 | $iro-bem-debug-colors: ( | ||
| 103 | object: #ffa500, | ||
| 104 | component: #00f, | ||
| 105 | layout: #ff0, | ||
| 106 | utility: #008000, | ||
| 107 | hack: #f00 | ||
| 108 | ) !default; | ||
