CSS rules merging - css

I am working to implement RTL support on a large framework. For that, I replaced all of the rules that are directional with mixins like:
a {
#include padding(0, 1px, 0, 0);
#include margin(0, 1px, 0, 0);
}
What this does, is to add the relevant padding/margin depending on the direction of the document.
Every mixin creates a case for ltr and a case for rtl.
This is the result from those mixins:
[dir="ltr"] a {
padding: 0 1px 0 0;
}
[dir="rtl"] a {
padding: 0 0 0 1px;
}
[dir="ltr"] a {
margin: 0 1px 0 0;
}
[dir="rtl"] a {
margin: 0 0 0 1px;
}
Which works, and is fine, but creates a lot of duplicate selectors (2 per mixin), so the css bundle size increases by 100kb (20%), and a large part of it is because of this duplication.
Expected result:
[dir="ltr"] a {
padding: 0 1px 0 0;
margin: 0 1px 0 0;
}
[dir="rtl"] a {
padding: 0 0 0 1px;
margin: 0 0 0 1px;
}
What post-processing I can do to merge relevant duplicate selectors, without hurting the order of css execution?
Undesired case:
Lets say I have this code:
b.s1 {
padding-left: 1px;
margin: 0;
}
b.s2 {
padding-left: 0;
margin: 1px;
}
b.s1 {
padding-left: 1px;
}
If I merge b.s1 upwards, then s2's padding-left can override it.
If I merge b.s1 downwards, then s2's margin is overriden.
Is there any solution to this problem?
EDIT: Original code
// Add property for all sides
// #param {string} $prop
// #param {string} $top
// #param {string} $end
// #param {string} $bottom
// #param {string} $start
// #param {boolean} $content include content or use default
// ----------------------------------------------------------
#mixin property($prop, $top, $end: $top, $bottom: $top, $start: $end, $content: false) {
#if $top == $end and $top == $bottom and $top == $start {
#include multi-dir() {
#{$prop}: $top;
}
} #else if $top == $bottom and $end == $start and $top != null and $end != null {
#include multi-dir() {
#{$prop}: $top $end;
}
} #else if $end == $start and $top != null and $end != null and $bottom != null {
#include multi-dir() {
#{$prop}: $top $end $bottom;
}
} #else if $top != null and $end != null and $bottom != null and $start != null {
#include ltr() {
#{$prop}: $top $end $bottom $start;
}
#include rtl() {
#{$prop}: $top $start $bottom $end;
}
} #else {
#if $content == true { // TODO check if #content exists instead
#content;
} #else {
#include property-horizontal($prop, $start, $end);
#include multi-dir() {
#{$prop}-top: $top;
#{$prop}-bottom: $bottom;
}
}
}
}
// Add padding for all sides
// #param {string} $top
// #param {string} $end
// #param {string} $bottom
// #param {string} $start
// ----------------------------------------------------------
#mixin padding($top, $end: $top, $bottom: $top, $start: $end) {
#include property(padding, $top, $end, $bottom, $start);
}
// Add margin for all sides
// #param {string} $top
// #param {string} $end
// #param {string} $bottom
// #param {string} $start
// ----------------------------------------------------------
#mixin margin($top, $end: $top, $bottom: $top, $start: $end) {
#include property(margin, $top, $end, $bottom, $start);
}

I wrote a specific fix for dir, as it is my main problem, not other duplications which are a tiny proportion of my bundle.
Pretty simple, and does the job, 0 seconds. It does not merge up or down, but instead removes the code and adds a chunk with the merged directional code at the end. (it doesn't matter for directional, as everything maintains its order and specificity)
function joinDirections(contents) {
// This includes multi directional selectors, like `[dir="ltr"] sel, [dir="rtl"] sel`,
// Which go into the ltr pile, but it is ok as the rest (`sel, [dir="rtl"] sel`) is still good.
const dirExp = /\[dir="(.*?)"\](.*?){\s*([^}]*?)\s*}/gm;
let directions = {};
let matches;
while (matches = dirExp.exec(contents)) {
if (!(matches[1] in directions))
directions[matches[1]] = {};
if (!(matches[2] in directions[matches[1]]))
directions[matches[1]][matches[2]] = '';
directions[matches[1]][matches[2]] += matches[3];
}
contents = contents.replace(dirExp, '');
let directionalContents = '';
Object.keys(directions).forEach(dir => {
Object.keys(directions[dir]).forEach(selector => {
directionalContents += `[dir="${dir}"]${selector}{${directions[dir][selector]}}\n`;
});
});
return contents + directionalContents;
}

Related

SASS loop on map, get next iteration

I have a color array like this :
$colors: (
'primary': '#aaa',
'secondary': '#bbb',
'color-3': '#ccc',
'color-4': '#ddd',
'color-5': '#eee',
);
and I want to automate class creation in loop like this :
#each $col in map-keys($theme-colors){
&.btn-#{$col} {
background-color: map-get($theme-colors, $col);
&:hover{
background-color: map-get($theme-colors, $col + 1); // <= my problem is here to get my "$col + 1"
}
}
}
My idea is to create a class btn-primary with primary color background but, on hover, secondary color at background.
A class btn-secondary secondary color background but, on hover, color-3 color at background, etc.
How can I do this?
Thanks ;)
Here you can find your solution: https://github.com/elcheio/sass-map-get-next-prev
Using that function (i.e. map-get-next function) you can resolve your problem.
So, first of all copy and paste that function... very very simple (^_^;)
#function map-get-next($map, $key, $fallback: false, $return: value) {
// Check if map is valid
#if type-of($map) == map {
// Check if key exists in map
#if map-has-key($map, $key) {
// Init index counter variable
$i: 0;
// Init key index
$key-index: false;
// Traverse map for key
#each $map-key, $map-value in $map {
// Update index
$i: $i + 1;
// If map key found, set key index
#if $map-key == $key {
$key-index: $i;
}
// If next index return next value or key based on $return
#if $i == $key-index + 1 {
#if $return == key {
#return $map-key;
} #else {
#return $map-value;
}
}
// If last entry return false
#if $i == length($map) {
#return $fallback;
}
}
#warn 'No next map item for key #{$key}';
#return $fallback;
}
#warn 'No valid key #{$key} in map';
#return $fallback;
}
#warn 'No valid map';
#return $fallback;
}
Then you can add your map:
$colors: (
'primary': #aaa,
'secondary': #bbb,
'color-3': #ccc,
'color-4': #ddd,
'color-5': #eee,
);
In the end, you have to create an #each loop to estract every pair name/value of your map:
#each $name, $value in $colors{
.btn-#{$name} {
background-color: $value;
&:hover{
background-color: map-get-next($colors, $name, #ffffff); // <== here you have to write the color for your last item (i.e. 'color-5'; in this example is white)
}
}
}
That's it! Your output will be:
.btn-primary {
background-color: #aaa;
}
.btn-primary:hover {
background-color: #bbb;
}
.btn-secondary {
background-color: #bbb;
}
.btn-secondary:hover {
background-color: #ccc;
}
.btn-color-3 {
background-color: #ccc;
}
.btn-color-3:hover {
background-color: #ddd;
}
.btn-color-4 {
background-color: #ddd;
}
.btn-color-4:hover {
background-color: #eee;
}
.btn-color-5 {
background-color: #eee;
}
.btn-color-5:hover {
background-color: #ffffff;
}

SASS offset position mixin troubles

I've been trying to make an offset positioning mixin for the past couple of hours. I've been struggling with lots of errors, I cant understand what is wrong.
Here is the latest version,
#function is-offset-prop-valid($value) {
$values: auto initial inherit 0;
#return (type-of($value) == number and not unitless($value))
and (index($values, $value) != false);
}
#mixin position($position, $args) {
$offsets: top right bottom left;
#each $offset in $offsets {
$i: index($args, $offset);
#if $i == length($args) {
#error "Empty offset values not allowed";
} else {
#if is-offset-prop-valid(nth($args, $i+1)) {
#{$offset}: nth($args, $i+1);
} else {
#error "Set value #{nth($args, $i+1)} not a valid property for #{$offset}";
}
}
}
positon: $position;
}
Normally I would have the nth($args, $i + 1) set as variable, but for the sake of this example, I left it like that.
When I use the mixin
.abs {
#include position(absolute, top 10px);
}
I get this error from the inner if statement:
Set value 10px not a valid property for top
I fixed your code and rewrited it a bit. Sassmeister demo.
At first, is-offset-prop-valid function is now more readable.
Secondly, mixin position does loop through arguments ($args), not through $offsets. And I added more argument checks (look at comments). And you need to write # symbol before else: #else.
#function is-offset-prop-valid($value) {
$values: auto initial inherit 0;
$is-numder: type-of($value) == number;
$is-unitless: unitless($value);
$match-default-values: index($values, $value) != null;
#return ($is-numder and not $is-unitless) or $match-default-values;
}
#mixin position($position, $args...) {
$offsets: top right bottom left;
#if length($args) == 0 {
#error "Empty offset values not allowed.";
}
#each $arg in $args {
// Check offset key-value pair
#if length($arg) != 2 {
#error "Invalid pair offset-name: offset-value.";
}
$offset: nth($arg, 1);
$value: nth($arg, 2);
// Check offset name parameter
#if index($offsets, $offset) == null {
#error "Invalid offset name: `#{$offset}`.";
}
// Check offset value parameter
#if not is-offset-prop-valid($value) {
#error "Set value `#{$value}` not a valid property for `#{$offset}`.";
}
#{$offset}: $value;
}
position: $position;
}
.abs {
#include position(absolute, top 10px, left 23px);
}
But it seems to me that a simple set position is much simpler and more understandable:
.abs {
top: 10px;
left: 23px;
position: absolute;
}

SASS and data attribute multiple

I have a problem with the nesting of SAS to make multiple selections, nose much about it, I hope you can help me and understand (because I do not write very good English).
SASS mixin:
#mixin data($x) {
$sel: &;
$collector: ();
#for $i from 1 through length($sel) {
$s: nth($sel, $i);
$last: nth($s, -1);
#if str-slice($last, -1) == "]" {
// if is just the bare attribute with no value, $offset will be -1, otherwise it will be -2
$offset: -1;
$current-x: $x;
#if str-slice($last, -2) == '"]' {
// this attribute already has a value, so we need to adjust the offset
$offset: -2;
} #else {
// no attribute value, so add the equals and quotes
$current-x: '="' + $x + '"';
}
$last: str-slice($last, 1, $offset - 1) + $current-x + str-slice($last, $offset);
$collector: append($collector, set-nth($s, -1, $last), comma);
} #else {
// following line will append $x to your non-attribute selector
$collector: append($collector, selector-append($s, $x), comma);
// the following line will not change your non-attribute selector at all
//$collector: append($collector, $s, comma);
}
}
#at-root #{$collector} {
#content;
}
}
SASS:
[data-content] {
#include data("content") {
background: black;
}
}
Output:
[data-content="content"] {
background: black;
}
The problem is I can not nest more than one item, for example does not work:
[data-content] {
#include data("content", "menu") {
background: black;
}
}
Output:
[data-content="content"],
[data-content="menu"] {
background: black;
}
Any way to solve?
You can always do something like this if you don't mind having to specify your selectors instead of passing them through as variables.
[data-content="content"], [data-content="menu"]{
#include data() {
background: black;
}
}

Create a recursion mixin scss

I am trying to create a recursion #mixin. The mixin has the task to append a selector from a given list and apply a CSS rules. The result of this has to look like this:
[data-tag]:not([conref]) {
border: dashed 2px #2999d1;
}
[data-tag]:not([conref])[nodeid] {
border: dashed 2px #2999d1;
}
[data-tag]:not([conref])[nodeid][draggable] {
border: dashed 2px #2999d1;
}
[data-tag]:not([conref])[nodeid][draggable][class] {
border: dashed 2px #2999d1;
}
The #mixin which I have created looks like this:
#mixin set-border-to-selectors($list, $item){
#if false == (index($list, $item)){
#error "Fail: #{$item} not in list: #{$list}";
}
#else{
$index: index($list, $item);
#debug "index: #{$index}";
$item: nth($list, $index);
&[#{$item}]{
#include set-data-tag-border;
#if($index + 1 <= length($list)){
$item: nth($list, $index + 1);
#include set-border-to-selectors($list, $item);
}
}
}
}
The first time I use the function it works pretty well.
#mixin show-data-tag-border{
$data-tag-list: (#{$id}, draggable, class);
$data-tag-list-two: (#{$id}, #{$conref}, class);
&[data-tag]:not([#{$conref}]){
#include set-data-tag-border;
#include set-border-to-selectors($data-tag-list, #{$id});
}
&[data-tag]{
#include set-border-to-selectors($data-tag-list-two, #{$id});
}
But the second call of the set-border-to-selector function throws an error, because the $index in the recusion #mixin does not contain a number. It is empty.
I call the function from p{#include show-data-tag-border;} and the border will be set in #mixin set-data-tag-border{border: solid 1px black}. And this two variables:
$id: id;
$conref: conref;
Is there something wrong what I do not see or is this approach wrong to create an recursion in SCSS? I won't use this function only one time and write one or more similar function below to accomplish this.
I'm not gonna try to repair your code, as it's too big. Recursion should work just fine like this:
#mixin set-borders ($list, $index: 1) {
#if $index <= length($list) {
#if $index == 1 {
&#{nth($list, $index)} {
#include set-borders($list, $index + 1);
}
} #else {
&,
&#{nth($list, $index)} {
#include set-borders($list, $index + 1);
}
}
} #else {
// ... (set the required properties)
}
}
The problem was the declaration of the list elements. I have to make tick marks.
So instead of this:
$data-tag-list: (#{$id}, draggable, class);
$data-tag-list-two: (#{$id}, #{$conref}, class);
I have to enclose every element in the list with tick marks:
$data-tag-list: ('#{$id}', 'draggable', 'class');
$data-tag-list-two: ('#{$id}', '#{$conref}', 'class');
and it works!
Thank you hon2a for your help!
Here is the full code for check up:
$id: id;
$conref: conref;
#mixin set-data-tag-border{border: solid 1px black}
#mixin set-border-to-selectors($list, $item){
#if false == (index($list, $item)){
#error "Fail: #{$item} not in list: #{$list}";
}
#else{
$index: index($list, $item);
#debug "index: #{$index}";
$item: nth($list, $index);
&[#{$item}]{
#include set-data-tag-border;
#if($index + 1 <= length($list)){
$item: nth($list, $index + 1);
#include set-border-to-selectors($list, $item);
}
}
}
}
#mixin show-data-tag-border{
$data-tag-list: ('#{$id}', 'draggable', 'class');
$data-tag-list-two: ('#{$id}', '#{$conref}', 'class');
&[data-tag]:not([#{$conref}]){
#include set-data-tag-border;
#include set-border-to-selectors($data-tag-list, #{$id});
}
&[data-tag]{
#include set-border-to-selectors($data-tag-list-two, #{$id});
}
}
p{#include show-data-tag-border;}

How do you check for unitless variables with multiple values SASS

I try to create a flexible mixin where you can set the space for padding or margin from the same mixin
I based it on a bourbon for positioning
mixin setSpace($setSpace: padding, $setSpaceValues: 0 0 0 0){
#if type-of($setSpace) == list{
$setSpaceValues :$setSpace;
$setSpace: padding;
}
$top: nth($setSpaceValues, 1);
$right: nth($setSpaceValues, 2);
$bottom: nth($setSpaceValues, 3);
$left: nth($setSpaceValues, 4);
#if unitless($top and $right and $bottom and $left){
#{$setSpace}: $top+px $right+px $bottom+px $left+px ;
}
}
But I try to get the flexibility to be able to add individual units to it as well so that I can do also
.test{
#include setSpace(margin, 10% 0 5 5);
}
You could use the Sass if() function on each value to check for unitless ... maybe make define a function that does this - something in this direction perhaps:
#function setUnit($val){
#return if(unitless($val), $val * 1px, $val);
}
And then you can use it in your mixin:
#mixin setSpace($setSpace: padding, $setSpaceValues: 0 0 0 0){
#if type-of($setSpace) == list{
$setSpaceValues: $setSpace;
$setSpace: padding;
}
$top: nth($setSpaceValues, 1);
$right: nth($setSpaceValues, 2);
$bottom: nth($setSpaceValues, 3);
$left: nth($setSpaceValues, 4);
#{$setSpace}: setUnit($top) setUnit($right) setUnit($bottom) setUnit($left) ;
}
DEMO
in addition you could also just set the values in a loop ( - a bit more flexible and shorter):
#mixin setSpace($setSpace: padding, $setSpaceValues: 0 0 0 0){
#if type-of($setSpace) == list{
$setSpaceValues: $setSpace;
$setSpace: padding;
}
$out: ();
#each $val in $setSpaceValues{
$out: append($out, if(unitless($val), $val * 1px, $val));
}
#{$setSpace}: $out;
}
DEMO
Hope this gives you some ideas.
Your mixin just might be better suited as a function:
#function spacing($values: 0) {
$collector: ();
#each $v in $values {
$collector: append($collector, if(unitless($v) and $v != 0, $v * 1px, $v));
}
#return $collector;
}
.test{
margin: spacing(10% 0 5 5);
}
Output:
.test {
margin: 10% 0 5px 5px;
}
If all you're doing is transforming a single value, functions make it a little more clear that's all that's happening when you come back to read the code later.

Resources