Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to only show half of a SVG icon?

I currently have a rating system that produces 5 stars that can show half values. I did this by using FontAwesome's Half Stars and did some CSS tricks to make them look like one star. But I was thinking of increasing my React and CSS knowledge by coming up with a way to show only half of an SVG icon. So instead of using the Half Stars, I could use whatever Icon the user wanted, and it would only show 50% of the Icon for example if you wanted to give a 3.5 rating.

Q: Can you show only half of an Icon and somehow know if the user is clicking on one side or the other?

Here is the code I have currently that uses the HalfStars for a little bit of reference

import React, { useState } from 'react'
import { FaRegStarHalf, FaStarHalf } from 'react-icons/fa'

import './styles/Rater.css'

const Rater = ({ iconCount }) => {
    const [rating, setRating] = useState(null)
    const [hover, setHover] = useState(null)
    // check if user has set a rating by clicking a star and use rating to determine icons
    const Star = rating ? FaStarHalf : FaRegStarHalf

    return (
        <div>
            {[...Array(iconCount), ...Array(iconCount)].map((icon, i) => {
                const value = (i + 1) / 2

                return (
                    <label>
                        <input
                            type='radio'
                            name='rating'
                            value={value}
                            onClick={() => {
                                console.log(`value => `, value)
                                return setRating(value)
                            }}
                        />
                        <div className='star-container'>
                            <div>
                                <Star
                                    className={i % 2 ? 'star-left' : 'star'}
                                    color={value <= (hover || rating) ? '#ffc107' : '#e4e5e9'}
                                    onMouseEnter={() => setHover(value)}
                                    onMouseLeave={() => setHover(null)}
                                />
                            </div>
                        </div>
                    </label>
                )
            })}
        </div>
    )
}

export default Rater


like image 715
4156 Avatar asked Dec 19 '20 02:12

4156


Video Answer


2 Answers

I have written a code to get you the idea; If you click on the right side of the star, its color changes to blue and if you click on the left side, its color changes to gold. Also, it's better to not use stopPropagation and check e.target of the event.

const starIcon = document.getElementById("star");
const icon = document.getElementById("icon");
starIcon.onclick = e => {
  starIcon.style.color = "blue";
  e.stopPropagation();
};
icon.onclick = e => {
  starIcon.style.color = "gold";
}
i {
  clip-path: inset(0 0 0 50%);
  color: gold;
}
  <!DOCTYPE html>
  <html lang="en">

    <head>
      <meta charset="UTF-8">
      <title>Document</title>
      <link href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" rel="stylesheet">
    </head>

    <body>
      <span id="icon"><i id="star", class="fas fa-star"></i></span>
    </body>

  </html>
like image 54
Reza Avatar answered Oct 20 '22 01:10

Reza


You can do it in ONE SVG

  • I ditched the Font-Awesome icon
  • searched for "star" in the 7000+ icons on https://iconmeister.github.io/ (first load takes a minute)
  • Picked the star icon with the best d-path (Clarity Iconset: cl-social-star-solid)
  • copied only the d-path
  • Edited the d-path in https://yqnn.github.io/svg-path-editor/ to a 100x100 viewBox/grid
  • made it an inverse star by prepending M0 0h100v100h-100v-100 to the path
  • Created a new SVG file in a 0 0 300 100 viewBox to fit 3 stars.. see below
  • Added a background rectangle setting gold color rating with width="50%"
  • Used 3 inverse stars, each at an x-offset
  • added 6 rectangles covering all half-stars
  • set inline events on every "half-star"
    (the click works in this SO snippet, but SO adds a long delay)

Proof of Concept

<svg viewBox="0 0 300 100" width="500px">
  <rect id="rating" width="50%" fill="gold" height="100%" />

  <path id="star" fill="green" 
        d="M0 0h100v100h-100v-100m91 42a6 6 90 00-4-10l-22-1a1 1 90 01-1 
           0l-8-21a6 6 90 00-11 0l-8 21a1 1 90 01-1 1l-22 1a6 6 90 00-4 
           10l18 14a1 1 90 010 1l-6 22a6 6 90 008 6l19-13a1 1 90 011 0l19
           13a6 6 90 006 0a6 6 90 002-6l-6-22a1 1 90 010-1z"/>
    <use href="#star" x="100" />
    <use href="#star" x="200" />

  <rect id="c" width="16.66%" height="100%" fill="transparent" stroke="red" 
        onclick="console.log(this)" />
    <use href="#c" x="50" />
    <use href="#c" x="100" />
    <use href="#c" x="150" />
    <use href="#c" x="200" />
    <use href="#c" x="250" />
</svg>

A Star Rating Component <star-rating stars=N >

You don't want to create all this SVG by hand... couple lines of JavaScript can create the SVG, for any number of stars

Using a W3C standard Web Component here, because it runs in this page and is not as complex as a React Component.

https://developer.mozilla.org/en-US/docs/Web/Web_Components

  • Not using <use>, just duplicate all paths and rects with a x offset
  • mouseover events set the background % color
  • click shows the index of the clicked halfstar (0+)
  • Rating can be set with values or percentage; document.querySelector('[stars="5"]').rating="90%" (4.5 stars)
  • needs extra work for your use case

All required HTML & JavaScript:

<star-rating stars=5 rating="3.5"
             bgcolor="green" nocolor="grey" color="gold"></star-rating>
<star-rating stars=7 rating="50%"
             bgcolor="rebeccapurple" nocolor="beige" color="goldenrod"></star-rating>
<script>
  document.addEventListener("click", (evt) => console.log(evt.target.getAttribute("n")))

  customElements.define("star-rating", class extends HTMLElement {
    set rating( rate ) {
      if (!String(rate).includes("%")) rate = Number(rate) / this.stars * 100 + "%";
      this.querySelector("#rating").setAttribute("width", rate);
    }
    connectedCallback() {
      let { bgcolor, stars, nocolor, color, rating } = this.attributes;
      this.stars = ~~stars.value || 5;
      this.innerHTML = 
        `<svg viewBox="0 0 ${this.stars*100} 100" style="cursor:pointer;width:300px">`
      + `<rect width="100%" height="100" fill="${nocolor.value}"/>`
      + `<rect id="rating"  height="100" fill="${color.value}"  />`
        + Array(  this.stars     ).fill()
               .map((i, n) => `<path fill="${bgcolor.value}" d="M${ n*100 } 0h102v100h-102v-100m91 42a6 6 90 00-4-10l-22-1a1 1 90 01-1 0l-8-21a6 6 90 00-11 0l-8 21a1 1 90 01-1 1l-22 1a6 6 90 00-4 10l18 14a1 1 90 010 1l-6 22a6 6 90 008 6l19-13a1 1 90 011 0l19 13a6 6 90 006 0a6 6 90 002-6l-6-22a1 1 90 010-1z"/>`)
               .join("")
        + Array(  this.stars * 2 ).fill()
               .map((i, n) => `<rect x="${ n*50 }" n="${n}" opacity="0" width="50" height="100"`
                  + ` onclick="dispatchEvent(new Event('click'))" `
                  + ` onmouseover="this.closest('star-rating').rating = ${(n+1)/2}"/>`)
              .join("") 
      + "</svg>";

      this.rating = rating.value;
    }
  });
</script>

Notes:

  • This native <star-rating> Component (also called Custom Element because NO shadowDOM is involved) has ZERO dependencies
    • no libraries
    • no external SVG
  • native components are not self-closing tags and must contain a hyphen, so notation is: <star-rating></star-rating>
  • Changed the star to M0 0h102v100h-102v-100 (2 pixel overlap) to cover SVG rounding issues

Supported in all Frameworks... except...

React doesn't support this modern W3C Web Components Standard yet.

React scores just 71% on https://custom-elements-everywhere.com/

All other Frameworks (Angular, Vue, Svelte) have 100% support

You have to do some extra work to handle native DOM Elements and Events in React; but the Custom Element isn't complex.. it creates SVG; should be easy to replicate as a React Component.

like image 26
Danny '365CSI' Engelman Avatar answered Oct 20 '22 00:10

Danny '365CSI' Engelman