Date

(Alternate titles: make the facebook like thing, make the twitter fave thing, make the intsa heart thing, All this webshit is the same and I'm spending my weekends codifying boolean emotions for corporations to package and sell to Ted Cruz: why I threw my laptop into a barbeque pit)

If you've been making webshit long enough you've felt this request appear in your inbox, you probably didn't have to check your email -- it was there in the morning and when you woke up you knew. And for good reason, this widget is everywhere. If you're using chrome it's IN the chrome of your browser (check the right side of your url bar). It's in slack. It's probably somewhere in my god damned terminal but I just haven't found it yet.

Sadly there aren't any tutorials for making it. And because I have NOT been doing web development that long, this feels like a good opportunity for some beginner CSS. So there are basically two characters we're gonna use today: black star which is the most metal unicode character, and white star which is the outline of the black star (and isn't YET the name of a neo-nazi fetlife discussion group, so keep your fingers crossed).

These look like ☆ and ★ respectively. (By the way, if you wanna type these in vim, you just need to ctrl+V in insert mode, then type u for unicode, then the 4-hexchar unicode sequence. So those last two characters were ctrl+v+u+2606 and ctrl+v+u+2605.Lean back a little bit to avoid drooling on the keyboard.)

So check this out:

<div id="star_{{post.id}}" class="star-div">
    <span class="star">&star;</span>
</div>

with this css

.star {
    font-size:120%;
    color: #F0F0F0;
}

.star-div {
    cursor: default;
}

.star-div > span:hover:before {
    cursor: default;
    content: "\2606";
    position: absolute;
    color: grey;
}

renders like this:

Hover over it. What we're doing is using CSS positioning to overlay a slightly darker star glyph over the original one on hover. The position: absolute property means that the positioning is computed relative to the container block (in this case the div with the star-div class), and we're simply not specifying any top, bottom, left, or right offsets.

All we really need to do to the star-div class is prevent it from messing with the cursor, because for something unintrusive like a star that hasn't even been activated yet, that's just obnoxious. In reality you'll probably want it next to something, and so display: inline-block might be worth having close by.

Next we want to figure out a way to display the "starred" whatever-it-is-you-are-starring. This is simple -- just use the filled in star and color it yellow!

<div id="star_{{post.id}}" class="star-div-active">
    <span class="star-active">&starf;</span>
</div>

and use the following css

.star-active {
    font-size:120%;
    color: yellow;
}

.star-div-active > span:hover {
    cursor: pointer;
}

which renders like this:

Here we want to mess with the cursor -- everyone disagrees with me here but I feel like it's better than adding another distracting hover effect. Feel free to leave a comment below. This is sort of what the gmail star looks like (actually they use the pointer for unstarred, as well as for the starred rows) but I feel like this is less intrusive.

Only problem is it's kind of hard to see. So what we can do is overlay the outline star to apply a kind of "border". Looks like this in the css:

.star-div-active > span:before {
    content: "\2606";
    position: absolute;
    color: lightgrey;
}

and this on the page:

Now all we have left is to make it interactive. This is pretty easy.

In the javascript we just check to see if the star is, uh, starred or not, and make it the other one. And then probably bust off an ajax to tell the server what you did.

$('.star-div,.star-div-active').on('click', function(ev) {
  starEl = $(this).find('span')
  starred = starEl.hasClass('star-active')
  // change the DOM first, THEN make the ajax call
  if (starred) {
    starEl.removeClass('star-active').addClass('star').text('\u2606')
    starEl.parent().removeClass('star-div-active').addClass('star-div')
  } else {
    starEl.removeClass('star').addClass('star-active').text('\u2605')
    starEl.parent().removeClass('star-div').addClass('star-div-active')
  }
  $.ajax({
    type: 'POST',
    url: '/api/whatever',
    data: JSON.stringify({
      id: element_id,
      starred: !starred,
      }),
    contentType: 'application/json; charset=utf-8',
    dataType: 'json',
    success: function (data) {
      // whatever you wanna do, go nuts!            
    },
    error: function(xhr) {
      $('nav').next().prepend(`<div class="alert alert-dismissable alert-danger"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> Error ${xhr.statusCode().status}: ${xhr.statusCode().statusText} ${xhr.responseText} </div>`)
    }
    })
})

So now we have this:

Click on it. It's that simple.

One last thing. Once you complete this you'll probably send an email to your boss and he'll try it out right there from his iPhone in the shower. He'll notice that it takes him two taps to actually get the star to change, and the first tap just applies the hover effect. You may have noticed this too.

You've discovered (and, of course, I mean I discovered) The Annoying Mobile Double-Tap Link issue, documented extensively by Nicholas C. Zakas back in 2012.

To save you a click (or a tap, heh -- joke works on two levels) there are a couple solutions. You can use a media query to only apply the hover pseudoclasses on devices with screens wider than 600 pixels

@media only screen and (min-width: 600px) {...}

(you can look up the breakpoints, but the iPhone 5S and iPhone 6+ won't hit this at 320px and 414px).

Or you can use the pointer media query

@media (pointer: fine) {...}

This is arguably the best solution as it only applies the styles to devices identified as having highly accurate pointing devices. Your greasy fingers are classified as "coarse" according to the spec, but don't reach for the lubriderm, there are other options.

There's also the hover media feature, which appears to be exactly what we need.

@media (hover) {...}

but according to the link it didn't work on firefox in 2016. Since nobody used firefox in 2016 this claim is impossible to verify but I'll take their word for it.

So our final css will look like this:

.star {
    font-size:120%;
    color: #F0F0F0;
}

.star-active {
    font-size:120%;
    color: yellow;
}

.star-div-active > span:before {
    content: "\2606";
    position: absolute;
    color: lightgrey;
}

.star-div {
    cursor: default;
}

 @media (hover) {
    .star-div > span:hover:before {
        cursor: default;
        content: "\2606";
        position: absolute;
        color: grey;
     }
     .star-div-active > span:hover {
        cursor: pointer;
    }
}

and our product will look like this.

Enjoy.