Skip to content

Instantly share code, notes, and snippets.

@landsman
Last active December 6, 2022 18:06
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save landsman/526d88db01cf5ec50fad257fe2d44574 to your computer and use it in GitHub Desktop.
Save landsman/526d88db01cf5ec50fad257fe2d44574 to your computer and use it in GitHub Desktop.
automatic convert bootstrap v. 3 to bootstrap v. 4 idea ... more on https://github.com/twbs/bootstrap/blob/v4-dev/docs/4.0/migration.md, http://upgrade-bootstrap.bootply.com/
let gulp = require('gulp'),
replace = require('gulp-batch-replace'),
filesExist = require('files-exist');
gulp.task('bt4', () =>
{
let diff = {
'@media (min-width: $screen-xs-min) and (max-width: $screen-sm-max)': '@media (min-width: map-get($grid-breakpoints, xs)) and (max-width: map-get($grid-breakpoints, xs))',
'@media (min-width: $screen-xs) and (max-width: ($screen-md-min - 1))': '@media (min-width: map-get($grid-breakpoints, xs)) and (max-width: map-get($grid-breakpoints, md)-1)',
'@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max)': '@include media-breakpoint-only(md)',
'@media (min-width: $screen-xs-min)': '@include media-breakpoint-up(xs)',
'@media (min-width: $screen-sm)': '@include media-breakpoint-up(md)',
'@media (min-width: $screen-sm-min)': '@include media-breakpoint-up(md)',
'@media (min-width: $screen-md-min)': '@include media-breakpoint-up(md)',
'@media (min-width: $screen-md)': '@include media-breakpoint-up(md)',
'@media (min-width: $screen-md-max)': '@include media-breakpoint-up(md)',
'@media (min-width: $screen-lg-min)': '@include media-breakpoint-up(lg)',
'@media (max-width: ($screen-xs-min - 1))': '@include media-breakpoint-down(xs)',
'@media (max-width: $screen-xs-max)': '@include media-breakpoint-down(xs)',
'@media (max-width: ($screen-sm-min - 1))': '@include media-breakpoint-down(md)',
'@media (max-width: $screen-sm)': '@include media-breakpoint-down(md)',
'@media (max-width: $screen-sm-min)': '@include media-breakpoint-down(md)',
'@media (max-width: $screen-sm-max)': '@include media-breakpoint-down(lg)',
'@media (max-width: $screen-md-max)': '@include media-breakpoint-down(md)',
'@media (max-width: $screen-lg-max)': '@include media-breakpoint-down(lg)',
'@media (max-width: $screen-xs-min - 1)': '@include media-breakpoint-down(xs)',
'@media (max-width: $screen-md-min)': '@include media-breakpoint-down(md)',
// bootstrap 2
'@media (max-width: $screen-xxs)': '@include media-breakpoint-down(xs)',
'.col-*-offset-*': '.offset-*',
'.col-*-push-*': '.order-*-2',
'.col-*-pull-*': '.order-*-1',
'.panel': '.card',
'.panel-heading': '.card-header',
'.panel-title': '.card-title',
'.panel-body': '.card-body',
'.panel-footer': '.card-footer',
'.panel-primary': '.card.bg-primary.text-white',
'.panel-success': '.card.bg-success.text-white',
'.panel-info': '.card.text-white.bg-info',
'.panel-warning': '.card.bg-warning',
'.panel-danger': '.card.bg-danger.text-white',
'.well': '.card.card-body',
'.thumbnail': '.card.card-body',
'.list-inline > li': '.list-inline-item',
'.dropdown-menu > li': '.dropdown-item',
'.nav navbar > li': '.nav-item',
'.nav navbar > li > a': '.nav-link',
'.navbar-right': '.ml-auto',
'.navbar-btn': '.nav-item',
'.navbar-fixed-top': '.fixed-top',
'.nav-stacked': '.flex-column',
'.btn-default': '.btn-secondary',
'.img-responsive': '.img-fluid',
'.img-circle': '.rounded-circle',
'.img-rounded': '.rounded',
//'.form-horizontal': '', // @note: removed
'.radio': '.form-check',
'.checkbox': '.form-check',
'.input-lg': '.form-control-lg',
'.input-sm': '.form-control-sm',
'.control-label': '.form-control-label',
'.table-condensed': '.table-sm',
'.pagination > li': '.page-item',
'.pagination > li > a': '.page-link',
//'.item': '.carousel-item', // @note: this is too much basic word
'.text-help': '.form-control-feedback',
'.pull-right': '.float-right',
'.pull-left': '.float-left',
'.center-block': '.mx-auto',
'.hidden-xs': '.d-none',
'.hidden-sm': '.d-sm-none',
'.hidden-md': '.d-md-none',
'.visible-xs': '.d-block.d-sm-none',
'.visible-sm': '.d-block.d-md-none',
'.visible-md': '.d-block.d-lg-none',
'.visible-lg': '.d-block.d-xl-none',
'.label': '.badge',
'.badge': '.badge.badge-pill',
// twig
'col-xs-': 'col-',
'col-md-': 'col-lg-',
'col-sm-': 'col-md-'
};
let replaceThis = [];
Object.keys(diff).forEach(function(key)
{
replaceThis.push([key, diff[key]]);
});
return gulp
.src(filesExist('./scss/**'))
.pipe(replace(replaceThis))
.pipe(gulp.dest('./build/scss'));
});
@crmpicco
Copy link

This looks a very useful reference. Can you confirm they are all accurate and valid for moving from Bootstrap 3 -> 4?

@weedkiller
Copy link

Any tips on how to run it

@kerryj89
Copy link

Just a heads up to anyone looking at this, the push/pull replacement will only work under the most simplest of cases where there are just two columns using push/pull in a row. That scenario is also the likeliest of cases, but when you do have more than two columns using the push/pull in a row, your ordering will not match Bootstrap 3's.

You can see this by entering the following:

<div class="container-fluid">
  <div class="row">
    <div class="col-sm-3 col-sm-push-3" style="background-color:lavender;">1 .col-sm-3</div>
    <div class="col-sm-3 col-sm-pull-3" style="background-color:lavenderblush;">2 .col-sm-3</div>
    <div class="col-sm-3 col-sm-push-3" style="background-color:lightyellow;">3 .col-sm-3</div>
    <div class="col-sm-3 col-sm-pull-3" style="background-color:lightblue;">4 .col-sm-3</div>
  </div>
</div>

Into http://upgrade-bootstrap.bootply.com/ which I believe uses this script. Render both and Bootstrap 3 will output as 2,1,4,3 while Bootstrap 4 will output as 2,4,1,3.

@kerryj89
Copy link

kerryj89 commented Jun 16, 2020

For anyone landing on this page and wanting to convert the other 1% of changes that require a little more than regex or find and replace, you can use https://github.com/creativetimofficial/bootstrap-converter. It uses cheerio which uses jQuery syntax to allow you to make nested changes that a regex and simple find a place can't do as easily.

As far as push/pull conversion, I coded the following script to calculate the final col position for the columns after push/pull is applied which applies the proper order-* class, useful when you have more than 3 columns using push/pull. Note: Unlike push/pull which uses relative and left/right positioning to start columns anywhere in the grid (even on top of another), order-* uses flex ordering which naturally does not allow that. In my case I do not overlap columns so the following should work:

var colOrigPlacement = 0,
    colOrigPlacementRegex,
    colPushPull,
    colPushPullVal,
    colPushPullRegex,
    isColPush = -1,
    colFinalPlacement,
    col = [],
    mediaQuery = ['xs', 'sm', 'md', 'lg', 'xl'],
    thisClassName;

function matches(text, regex) {
  return [...text.matchAll(regex)][0] || false;
}

mediaQuery.forEach(function (item, index) {
    $('.row').each(function () {
        colOrigPlacement = 0;
        col = [];

        var target = $(this).children('[class*="col-' + item + '"]'),
        childCount = target.length;

        target.each(function (i, val) {
            thisClassName = $(this)[0].attribs.class;

            // Get starting col position without push/pull
            colOrigPlacementRegex = new RegExp('col-' + item + '-(\\d+)', 'g');
            colOrigPlacement += parseInt(matches(thisClassName, colOrigPlacementRegex)[1]);

            // Capture push/pull keyword and the push/pull amount with regex
            colPushPullRegex = new RegExp('col-' + item + '-(push|pull)-(\\d+)', 'g');
            colPushPull = matches(thisClassName, colPushPullRegex);

            // Assign the push or pull amount
            colPushPullVal = parseInt(colPushPull[2]);

            // Are we going to be pushing (adding) or pulling (subtracting) from original col position?
            isColPush = colPushPull[1] === 'push' ? 1 : -1;

            // Get final col position after pushing and pulling
            if (colPushPull) {
                colFinalPlacement = colOrigPlacement + (isColPush * colPushPullVal);
            } else {
                colFinalPlacement = colOrigPlacement;
            }

            // Store jQuery reference to element and relevant data
            col.push({
                elem: $(this),
                colPushPull: colPushPull,
                colFinalPlacement: colFinalPlacement
            });

            // Sort array by final col placement (least to greatest)
            col.sort((a, b) => (a.colFinalPlacement > b.colFinalPlacement) ? 1 : -1);

            // Finally, remove BS3's push/pull class and replace with BS4's order class in the order they should appear
            if (i == childCount - 1 && (colOrigPlacement !== colFinalPlacement)) {
                col.forEach(function (val, i) {
                    val.elem.removeClass(val.colPushPull[0]).addClass('order-' + item + '-' + (i + 1));
                });
           }
        });
    });
});

@landsman
Copy link
Author

@kerryj89 thanks, but why jQuery? It's little bit outdated today.

@kerryj89
Copy link

kerryj89 commented Jun 16, 2020

@kerryj89 thanks, but why jQuery? It's little bit outdated today.

No problem! Thanks for your script, also. As for jQuery syntax, it's what https://github.com/creativetimofficial/bootstrap-converter uses (it uses cheerio which they call jQuery for the server, never heard of it until recently). That will be helpful to programmatically rewrite my HTML in more complex scenarios where elements or classes might have moved around. Honestly as outdated as jQuery might be in customer facing sites, the sizzle engine is still a joy to use with how frictionless it is to traverse up and down the DOM. I think it's very applicable for this one time conversion use case.

@landsman
Copy link
Author

@kerryj89 Doing this on frontend is overkill.
I had to rewrite templates, styles by CLI script, one-time and do new release with updated package of Bootstrap.
Just that.

@kerryj89
Copy link

Just to clarify, I am not having the templates rewritten dynamically every time on the frontend, that would indeed be overkill. It is converting the HTML templates programmatically on my machine once via a gulp task and writes the changes to the files. Your find and replace solution will work for 99% of the upgrade which is just class rename, but the other 1% like when we need to move active class in nav tab to another element or receive more context like number of columns within row to provide order-* values above 2 is when the other solution works better since it understands the nesting nature of HTML which regex and simple find and replace solutions would struggle with. I have hundreds of templates to work through so I am trying to approach updating the HTML as programmatically as possible so I don't miss things.

@landsman
Copy link
Author

@kerryj89 good :)

@devtroll
Copy link

devtroll commented Nov 3, 2020

Conversion is not correct.
Reason: sm in Bootstrap 4 is now md.
Example: @media (min-width: $screen-sm) is now @include media-breakpoint-up(md) for same screen width

@landsman
Copy link
Author

landsman commented Nov 3, 2020

@devtroll updated, thanks

@devtroll
Copy link

devtroll commented Nov 4, 2020

@landsman No probs, but i See another logical issue. "screen-sm-max" means maximum width of 992px. So i think it should be media-breakpoint-down(lg) instead of media-breakpoint-down(md). Do you agree with that?

@weedkiller
Copy link

Yes this is my scenario as well.. replacing templates or updating existing projects

Just to clarify, I am not having the templates rewritten dynamically every time on the frontend, that would indeed be overkill. It is converting the HTML templates programmatically on my machine once via a gulp task and writes the changes to the files....

@Bachstelze
Copy link

Can we adapt the script to Bootstrap 5? https://getbootstrap.com/docs/5.0/migration/

@landsman
Copy link
Author

@Bachstelze nope, you have to write your own for 4 to 5 migration.
I am still surprised that these things are not build-in in Bootstrap.

@HariPrasad-1493
Copy link

HariPrasad-1493 commented Jun 1, 2021

@kerryj89 thanks, but why jQuery? It's little bit outdated today.

No problem! Thanks for your script, also. As for jQuery syntax, it's what https://github.com/creativetimofficial/bootstrap-converter uses (it uses cheerio which they call jQuery for the server, never heard of it until recently). That will be helpful to programmatically rewrite my HTML in more complex scenarios where elements or classes might have moved around. Honestly as outdated as jQuery might be in customer facing sites, the sizzle engine is still a joy to use with how frictionless it is to traverse up and down the DOM. I think it's very applicable for this one time conversion use case.

It doesnt work for angular project though , since angular uses ngTemplates & template binding which are modified by this converter

eg. *ngIf="domains && domains.length>0" converted to *ngif="domains&amp;&amp; domains.length&gt;0" where the conditional clause is altered

@HariPrasad-1493
Copy link

HariPrasad-1493 commented Jun 1, 2021

@landsman how do i run it though on a project structure with files like
Testapp >> home.ts home.html home.css

@landsman
Copy link
Author

landsman commented Jun 1, 2021

@HariPrasad-1493 Really up to you. You can copy paste defined object and write your own replace script. This is just a draft.

@HariPrasad-1493
Copy link

@HariPrasad-1493 Really up to you. You can copy paste defined object and write your own replace script. This is just a draft.

Ohk.. thought this was some gulp task i can run even though i haven't used gulp before :)

@kerryj89
Copy link

kerryj89 commented Jun 4, 2021

It doesnt work for angular project though , since angular uses ngTemplates & template binding which are modified by this converter

eg. *ngIf="domains && domains.length>0" converted to *ngif="domains&amp;&amp; domains.length&gt;0" where the conditional clause is altered

I was able to use it on an AngularJS project and it didn't convert to html entity. It wasn't perfect but got 90% right. Maybe this can help: https://stackoverflow.com/a/31574528/3266845 I still looked through each template to make sure. I noticed it would close my attributes (i.e. <span attr-foo>Test</span> would turn into <span attr-foo="">Test</span> and sometimes would get confused by quotes and close prematurely. The worst is when it just removed a chunk of code sneakily, maybe a memory fault.

Sorry for this off-topic-on-topic reply, landsman.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment