У меня есть рейтинг для трех разных продуктов,
и я пытаюсь отобразить звездный SVG на основе их рейтинга.
Например, у продукта-А рейтинг 3, я хочу отобразить звездное изображение 3 раза.
Максимальное количество звезд, которое они могут иметь, — 5.
Я написал такой код:
const ratingHandler = (productRate) => {
for (let i = 0; i < productRate + 1; i++) {
console.info(productRate);
return `<img src = "assets/star.svg" alt = "rating-star" />`;
}
};
Оценка продукта правильно показывает номер рейтинга в моем console.info
, но рендеринг в цикле не выполняется на основе этого числа.
Я пытаюсь визуализировать теги img на основе количества ставок, которые имеет каждый продукт. Если у продукта рейтинг 3, я хочу, чтобы тег img отображался 3 раза.
Возможно, после чего-то вроде -> const ratingHandler = (productRate) => new Array(productRate).fill('<img src = "assets/star.svg" alt = "rating-star" />').join('')
@Кейт, а может быть проще str.repeat(productRate)
@VLAZ Старые привычки :), хотя они уже давно доступны..
Реальная проблема ОП гораздо глубже. Звездный рейтинг следует рассматривать как компонент, абстракцию проблемы, которая в общих чертах реализуется один раз. Таким образом, мы стремимся к многоразовому компоненту, который можно легко адаптировать путем изменения значений атрибутов и изменений CSS. Его разметку можно использовать гибко, хотя она семантически более правильна, чем последовательность звездных изображений OP.
@PeterSeliger Не совсем, вы, конечно, можете продвигать лучшие практики и возможность повторного использования компонентов, но ответы на SO должны быть прямыми и простыми. Сказав это, ваш ответ хорошо представлен, и я вижу много усилий, и я вижу, что другие пользователи, натыкающиеся на SO, могут найти это полезным, и по этой причине я буду голосовать... :)
Ваша функция завершается, как только она впервые достигает return
. Перед возвратом вам необходимо создать полную строку:
const ratingHandler = (productRate) => {
let stars = '';
for (let i = 0; i < productRate + 1; i++) {
stars += `<img src = "assets/star.svg" alt = "rating-star" />`;
}
return stars;
};
удивительный! Спасибо! это сработало! У меня есть еще один вопрос: для ставок меньше 5 я хочу добавить имя класса «.inactiveStar» к этому тегу изображения. Обычно, если у продукта рейтинг 4, img отображается 5 раз, но последний имеет имя класса .inactiveStar
@Афра, это совсем другая проблема. Я бы посоветовал вам задать новый вопрос об этом вместо того, чтобы расширять этот.
@Афра, может быть, что-то вроде этого - вместо изображений используется Юникод, но вы поймете идею, просто добавьте свой класс в часть else
.
@Энди, это было очень полезно, спасибо!
Для интереса: возможно, альтернативный (без цикла, используйте фон CSS для изображений) способ создания звезд из рейтинга:
[Редактирование]
const toBody = html => document.body.insertAdjacentHTML(`beforeend`, html);
document.addEventListener(`click`, handle);
[0, 4, 3, 1, 5, 2].forEach(v => toBody(rate(v)));
document.body.insertAdjacentHTML(
`beforeend`,
`<div class = "rateMe" data-maxrate = "10">
${rate(0, 10, true)}
<div><button data-reset='1'>reset</button></div>
</div> `
);
function rate(rating, n = 5) {
return `<div data-nstars = "${n}" data-rating = "${rating}">
${`<span class = "star active"></span>`.repeat(rating)}${
`<span class = "star inactive"></span>`.repeat(n-rating)}</div>`;
}
function handle(evt) {
const rateElement = evt.target.closest(`.rateMe`);
if (rateElement) {
const maxRate = +rateElement.dataset.maxrate;
const stars = rateElement.querySelectorAll(`.star`);
rateElement.querySelector(`[data-rating]`).remove();
const score = evt.target.dataset.reset ? 0 :
[...stars].reduce((acc, s, i) => s === evt.target ? acc + i : acc, 1);
return rateElement.insertAdjacentHTML(`afterBegin`, rate(score, maxRate));
}
}
[data-rating]:before {
content: attr(data-rating)'/'attr(data-nstars);
color: green;
}
.star {
background-repeat: no-repeat;
background-size: cover;
width: 16px;
height: 16px;
display: inline-block;
margin-left: 3px;
}
.rateMe:before {
content: 'Click to rate';
display: block;
margin: 1rem 0 0.2rem 0;
font-weight: bold;
}
.rateMe div {
cursor: pointer;
display: inline-block;
}
.inactive {
background-image: url('data:image/svg+xml,%3Csvg%20height%3D%2250px%22%20width%3D%2250px%22%20version%3D%221.1%22%20id%3D%22Capa_1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20viewBox%3D%220%200%2047.94%2047.94%22%20xml%3Aspace%3D%22preserve%22%20fill%3D%22%23000000%22%20stroke%3D%22%23000000%22%20stroke-width%3D%220.62322%22%3E%3Cg%20id%3D%22SVGRepo_bgCarrier%22%20stroke-width%3D%220%22%3E%3C%2Fg%3E%3Cg%20id%3D%22SVGRepo_tracerCarrier%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3C%2Fg%3E%3Cg%20id%3D%22SVGRepo_iconCarrier%22%3E%20%3Cpath%20style%3D%22fill%3A%23FFFFFF%3B%22%20d%3D%22M26.285%2C2.486l5.407%2C10.956c0.376%2C0.762%2C1.103%2C1.29%2C1.944%2C1.412l12.091%2C1.757%20c2.118%2C0.308%2C2.963%2C2.91%2C1.431%2C4.403l-8.749%2C8.528c-0.608%2C0.593-0.886%2C1.448-0.742%2C2.285l2.065%2C12.042%20c0.362%2C2.109-1.852%2C3.717-3.746%2C2.722l-10.814-5.685c-0.752-0.395-1.651-0.395-2.403%2C0l-10.814%2C5.685%20c-1.894%2C0.996-4.108-0.613-3.746-2.722l2.065-12.042c0.144-0.837-0.134-1.692-0.742-2.285l-8.749-8.528%20c-1.532-1.494-0.687-4.096%2C1.431-4.403l12.091-1.757c0.841-0.122%2C1.568-0.65%2C1.944-1.412l5.407-10.956%20C22.602%2C0.567%2C25.338%2C0.567%2C26.285%2C2.486z%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
}
.active, .rateMe .star:hover {
background-image: url('data:image/svg+xml,%3Csvg%20height%3D%2250px%22%20width%3D%2250px%22%20version%3D%221.1%22%20id%3D%22Capa_1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20viewBox%3D%220%200%2047.94%2047.94%22%20xml%3Aspace%3D%22preserve%22%20fill%3D%22%23000000%22%20stroke%3D%22%23000000%22%20stroke-width%3D%220.62322%22%3E%3Cg%20id%3D%22SVGRepo_bgCarrier%22%20stroke-width%3D%220%22%3E%3C%2Fg%3E%3Cg%20id%3D%22SVGRepo_tracerCarrier%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3C%2Fg%3E%3Cg%20id%3D%22SVGRepo_iconCarrier%22%3E%20%3Cpath%20style%3D%22fill%3A%23ED8A19%3B%22%20d%3D%22M26.285%2C2.486l5.407%2C10.956c0.376%2C0.762%2C1.103%2C1.29%2C1.944%2C1.412l12.091%2C1.757%20c2.118%2C0.308%2C2.963%2C2.91%2C1.431%2C4.403l-8.749%2C8.528c-0.608%2C0.593-0.886%2C1.448-0.742%2C2.285l2.065%2C12.042%20c0.362%2C2.109-1.852%2C3.717-3.746%2C2.722l-10.814-5.685c-0.752-0.395-1.651-0.395-2.403%2C0l-10.814%2C5.685%20c-1.894%2C0.996-4.108-0.613-3.746-2.722l2.065-12.042c0.144-0.837-0.134-1.692-0.742-2.285l-8.749-8.528%20c-1.532-1.494-0.687-4.096%2C1.431-4.403l12.091-1.757c0.841-0.122%2C1.568-0.65%2C1.944-1.412l5.407-10.956%20C22.602%2C0.567%2C25.338%2C0.567%2C26.285%2C2.486z%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
}
Я тоже рискнул реализовать активный компонент Star-Rating, чтобы продемонстрировать, как, по моему мнению, может выглядеть подход к достаточно общему, доступному, настраиваемому, поддерживаемому и, следовательно, повторно используемому (веб-)компоненту.
Обратите внимание на отличные комментарии @VLAZ по вопросу:
const ratingHandler = productRate =>
'<img src = "assets/star.svg" alt = "rating-star" />'.repeat(productRate+1)
Если вы хотите быть крутым парнем в классе, и никто не должен понимать ваш код, но он все равно должен выполнять свою работу, это ваш вариант:
const ratingHandler = (productRate) => [undefined].concat(productRate ?? []).reduce((all) => all += '<img src = "assets/star.svg" alt = "rating-star" />', "");
Отблагодаришь позже!
Из моего комментария выше...
Реальная проблема ОП гораздо глубже. Звездный рейтинг следует рассматривать как компонент, абстракцию проблемы, которая в общих чертах реализуется один раз. Таким образом, мы стремимся к многоразовому компоненту, который можно легко адаптировать путем изменения значений атрибутов и изменений CSS. Его разметку можно использовать гибко, хотя она семантически более правильна, чем последовательность звездных изображений OP.
Первый шаг — придумать осмысленную разметку и немного CSS… пока без использования JavaScript.
[data-star-rating] {
position: relative;
/* display: inline-block; */
display: block;
/* margin: 0; */
margin: 8px 0;
> p,
> span {
display: inline-block;
margin: 0 4px 0 0;
&:after {
content: ':'
}
}
> output,
> figcaption {
position: relative;
display: inline-block;
margin: 0;
color: transparent;
&:after,
&:before {
position: absolute;
left: 0;
top: -8px;
content: '';
font-size: 1.3em;
letter-spacing: 2px;
-webkit-text-stroke: 1px black;
}
&:after {
color: white;
}
&:before {
z-index: 1;
color: gold;
}
&[data-max-amount = "3"] {
&:after {
content: '★★★';
}
}
&[data-max-amount = "4"] {
&:after {
content: '★★★★';
}
}
&[data-max-amount = "5"] {
&:after {
content: '★★★★★';
}
}
&[data-max-amount = "6"] {
&:after {
content: '★★★★★★';
}
}
&[data-max-amount = "7"] {
&:after {
content: '★★★★★★★';
}
}
&[data-amount = "1"] {
&:before {
content: '★';
}
}
&[data-amount = "2"] {
&:before {
content: '★★';
}
}
&[data-amount = "3"] {
&:before {
content: '★★★';
}
}
&[data-amount = "4"] {
&:before {
content: '★★★★';
}
}
&[data-amount = "5"] {
&:before {
content: '★★★★★';
}
}
&[data-amount = "6"] {
&:before {
content: '★★★★★★';
}
}
&[data-amount = "7"] {
&:before {
content: '★★★★★★★';
}
}
}
}
<figure data-star-rating>
<p>Rating</p>
<figcaption data-amount = "4" data-max-amount = "7" title = "4 of 7 stars">
4 of 7 stars
</figcaption>
</figure>
<label data-star-rating>
<span>Rating</span>
<output data-amount = "2" data-max-amount = "5" title = "2 of 5 stars">
2 of 5 stars
</output>
</label>
<figure data-star-rating>
<p>Rating</p>
<figcaption data-amount = "1" data-max-amount = "4" title = "1 of 4 stars">
1 of 4 stars
</figcaption>
</figure>
<label data-star-rating>
<span>Rating</span>
<output data-amount = "3" data-max-amount = "3" title = "3 of 3 stars">
3 of 3 stars
</output>
</label>
<figure data-star-rating>
<p>Rating</p>
<figcaption data-amount = "5" data-max-amount = "6" title = "5 of 6 stars">
5 of 6 stars
</figcaption>
</figure>
Следующая итерация обеспечивает еще лучшую разметку с точки зрения использования (копию рейтинга можно опустить/показать), семантики и доступности. Правила показанного выше CSS были адаптированы в соответствии с этой новой структурой HTML. Сверху находится JavaScript, который реализует star-rating
веб-компонент.
Реализация представляет собой автономный пользовательский элемент, который должен наследовать от базового класса HTML-элемента HTMLElement
. Это связано с отсутствием поддержки Apple настраиваемых встроенных элементов, которые могут наследовать от стандартных элементов HTML.
Семантический разрыв, связанный с невозможностью использования <figure is = "star-rating"/>
, можно обойти, используя подобную роль арии <star-rating aria-role = "figure" />
.
В следующем примере кода представлен дополнительный скрипт, который позволяет манипулировать каждым компонентом с рейтингом, чтобы доказать концепцию компонента и правильную реализацию.
Каждый компонент звездного рейтинга можно использовать путем предоставления и/или мутации/изменения двух значений пользовательских данных-* атрибута. Это так просто.
function parseDataKey(attrName) {
return attrName
.replace(/^data-/, '')
.replace(/-(\p{Ll})/gu, (_, char) => char.toUpperCase());
}
function getApprovedAmountValue(initialData, dataKey, latestValue) {
const initialValue = initialData[dataKey];
latestValue = Number(latestValue);
latestValue = Number.isFinite(latestValue) ? latestValue : initialValue;
return String(
approvedValue = (dataKey === 'maxAmount')
&& Math.max(3, Math.min(latestValue, initialValue))
|| Math.max(0, Math.min(latestValue, initialData['maxAmount'] ?? latestValue))
);
}
class StarRating extends HTMLElement {
static observedAttributes = ['data-amount', 'data-max-amount'];
#initialData = {};
#captionNode;
constructor() {
super();
Object.assign(this.#initialData, {
amount: null,
maxAmount: null,
})
this.#captionNode = this.querySelector('[data-template-target]');
}
renderCaption() {
const { dataset: { template, amount, maxAmount, } } = this;
const ratingCopy = template
.replace(/\${\samount\s\}/g, amount)
.replace(/\${\smaxAmount\s\}/g, maxAmount);
this.#captionNode.textContent = ratingCopy;
this.title = ratingCopy;
}
attributeChangedCallback(attrName, recentValue, currentValue) {
const dataKey = parseDataKey(attrName);
if (recentValue === null) {
this.#initialData[dataKey] = currentValue;
}
const approvedValue =
getApprovedAmountValue(this.#initialData, dataKey, currentValue);
if (approvedValue !== currentValue) {
// set correct value and indirectly trigger method execution again .
this.dataset[dataKey] = approvedValue;
} else {
// rendering of valid/sanitized value changes only.
this.renderCaption();
}
}
}
customElements.define("star-rating", StarRating);
star-rating/*, [is = "star-rating"]*/ {
position: relative;
display: block;
margin: 12px 0;
> figcaption:has(> [data-template-target]) {
position: relative;
float: left;
margin: 0;
padding: 1px 4px 0 0;
> [data-template-target] {
overflow: hidden;
position: absolute;
width: 0;
&.visible {
overflow: unset;
position: unset;
width: unset;
}
}
}
> [data-figure] {
display: inline-block;
position: relative;
top: -1.3em;
&:after,
&:before {
position: absolute;
left: 0;
top: 0;
content: '';
font-size: 1.3em;
letter-spacing: 2px;
-webkit-text-stroke: 1px black;
}
&:after {
color: white;
}
&:before {
z-index: 1;
color: gold;
}
}
&[data-max-amount = "3"] {
> [data-figure] {
&:after {
content: '★★★';
}
}
}
&[data-max-amount = "4"] {
> [data-figure] {
&:after {
content: '★★★★';
}
}
}
&[data-max-amount = "5"] {
> [data-figure] {
&:after {
content: '★★★★★';
}
}
}
&[data-max-amount = "6"] {
> [data-figure] {
&:after {
content: '★★★★★★';
}
}
}
&[data-max-amount = "7"] {
> [data-figure] {
&:after {
content: '★★★★★★★';
}
}
}
&[data-amount = "1"] {
> [data-figure] {
&:before {
content: '★';
}
}
}
&[data-amount = "2"] {
> [data-figure] {
&:before {
content: '★★';
}
}
}
&[data-amount = "3"] {
> [data-figure] {
&:before {
content: '★★★';
}
}
}
&[data-amount = "4"] {
> [data-figure] {
&:before {
content: '★★★★';
}
}
}
&[data-amount = "5"] {
> [data-figure] {
&:before {
content: '★★★★★';
}
}
}
&[data-amount = "6"] {
> [data-figure] {
&:before {
content: '★★★★★★';
}
}
}
&[data-amount = "7"] {
> [data-figure] {
&:before {
content: '★★★★★★★';
}
}
}
}
fieldset { margin: 24px 0 0 0; label { margin: 0 8px 0 0; } }
<star-rating aria-role = "figure"
data-amount = "1"
data-max-amount = "4"
data-template = "${ amount } of ${ maxAmount } stars">
<figcaption>
Rating:
<span data-template-target class = "visible">-- of -- stars</span> /
</figcaption>
<div data-figure></div>
</star-rating>
<star-rating aria-role = "figure"
data-amount = "3"
data-max-amount = "3"
data-template = "${ amount } of ${ maxAmount } stars">
<figcaption>
Rating
... <span data-template-target class = "visible">-- of -- stars</span> ...
</figcaption>
<div data-figure></div>
</star-rating>
<star-rating aria-role = "figure"
data-amount = "5"
data-max-amount = "7"
data-template = "${ amount } of ${ maxAmount } stars">
<figcaption>
Rating:
<span data-template-target>-- of -- stars</span>
</figcaption>
<div data-figure></div>
</star-rating>
<fieldset>
<legend>Change Star Ratings</legend>
<label>
<span>1st Rating</span>
<select data-idx = "0">
<option value = "0">none</option>
<option value = "1" selected>★</option>
<option value = "2">★★</option>
<option value = "3">★★★</option>
<option value = "4">★★★★</option>
</select>
</label>
<label>
<span>2nd Rating</span>
<select data-idx = "1">
<option value = "0">none</option>
<option value = "1">★</option>
<option value = "2">★★</option>
<option value = "3" selected>★★★</option>
</select>
</label>
<label>
<span>3rd Rating</span>
<select data-idx = "2">
<option value = "0">none</option>
<option value = "1">★</option>
<option value = "2">★★</option>
<option value = "3">★★★</option>
<option value = "4">★★★★</option>
<option value = "5" selected>★★★★★</option>
<option value = "6">★★★★★★</option>
<option value = "7">★★★★★★★</option>
</select>
</label>
</fieldset>
<script>
document
.querySelector('fieldset')
.addEventListener('change', ({ target }) => {
document
.querySelectorAll('star-rating')
.item(target.dataset.idx)
.dataset.amount = target.value;
});
</script>
Редактировать... особенно ориентируясь на поздние вклады @KooiInc, как его последний предоставленный пример кода в его собственном сообщении, так и его комментарий к этому моему сообщению...
@KooiInc ... да, приведенный выше пример реализует исключительно пассивный рейтинг. А поскольку он служит цели «только для отображения», можно сохранить семантически простую разметку, например, базовую
<figure/>
. Активный звездный компонент должен семантически следовать за<fieldset/>
, который окружает радиогруппу. — Питер СелигерЯ создал версию веб-компонента , реализующую активный рейтинг . Он использует (синтаксис, не относящийся к классу) пользовательский модуль веб-компонента, который я создал ранее. – КоойИнк
А что касается последней правки @KooiInc, где он заявляет...
Вдохновленный ответом Питера Селигера, я создал (на мой взгляд, менее запутанный) веб-компонент для «ранжирования звезд».
... Я тоже рискнул привести еще один пример кода, где реализация активного компонента звездного рейтинга должна продемонстрировать то, что я имею в виду...
разметка компонента по умолчанию должна быть максимально семантически корректной и доступной.
разметка компонента может даже быть взаимозаменяемой (как показано на примере пассивного звездного рейтинга «только для отображения»).
Исходное состояние компонентов, данные и отображение, должно настраиваться с помощью атрибутов data-*
и/или пользовательских атрибутов компонента.
JavaScript используется исключительно для управления инициализацией компонента, его взаимодействием с пользовательским интерфейсом и изменениями его состояния (но не для управления его макетом/дизайном).
CSS должен охватывать все остальные аспекты компонента, связанные с пользовательским интерфейсом.
В следующем примере показаны 3 различных компонента Star-Rating с навигацией по вкладкам. А четвертая радиогруппа без стиля и без JS, включающая набор полей, доказывает, что разметка HTML-структуры компонента может быть написана пригодным для использования и повторного использования способом, даже при отсутствии стилей и сценариев, особенно когда речь идет о с (основанными на) элементами управления формой.
function getValidatedInitialData(component) {
const maxScoreUpperLimit = 13;
const maxScoreLowerLimit = 3;
const maxScoreDefault = 5;
const scoreDefault = 0;
let maxScore = Number(component.getAttribute('max-score') ?? maxScoreDefault);
let score = Number(component.getAttribute('score') ?? scoreDefault);
maxScore = Number.isFinite(maxScore)
? Math.max(maxScoreLowerLimit, Math.min(maxScoreUpperLimit, maxScore))
: maxScoreDefault;
return {
maxScore,
score: Number.isFinite(score)
? Math.max(0, Math.min(score, maxScore))
: scoreDefault,
};
}
function handleScoreItemStateChange(evt, nodes, state) {
const { currentTarget: component, target: inputControl } = evt;
const { scoreTarget, summaryTarget } = nodes;
const { maxScore, summary } = state;
// console.info('handleStateChange');
const controlList = [
...scoreTarget.querySelectorAll(`[type = "${ inputControl.type }"]`)
];
const selectedIndex = controlList.indexOf(inputControl);
state.selectedIndex = selectedIndex;
state.score = selectedIndex + 1;
inputControl.value = state.score;
if (summaryTarget) {
summaryTarget[summary.attrName] = summary.value = summary.literal
.replace(/\${\sscore\s\}/g, state.score)
.replace(/\${\smaxScore\s\}/g, maxScore);
}
}
function renderSelector(component, nodes, state) {
const { scoreTemplate, scoreTarget, summaryTarget } = nodes;
const { maxScore, score, summary } = state;
const fragment = document.createDocumentFragment();
Array
.from(
{ length: maxScore }, () => scoreTemplate.content.cloneNode(true),
)
.forEach((node, idx) => {
const scoreValue = idx + 1;
const labelTarget = node.querySelector('[data-label][data-literal]');
const inputControl = node.querySelector('[type = "radio"], [type = "checkbox"]');
if (labelTarget) {
labelTarget.textContent = (labelTarget.dataset.literal ?? '')
.replace(/\${\sscore\s\}/g, scoreValue)
.replace(/\${\smaxScore\s\}/g, maxScore);
Reflect.deleteProperty(labelTarget.dataset, 'literal');
}
inputControl.value = scoreValue;
if (scoreValue === score) {
state.selectedIndex = idx;
state.score = scoreValue;
inputControl.checked = true;
}
fragment.appendChild(node);
});
if (summaryTarget) {
summaryTarget[summary.attrName] = summary.value = summary.literal
.replace(/\${\sscore\s\}/g, state.score)
.replace(/\${\smaxScore\s\}/g, maxScore);
}
Reflect.deleteProperty(nodes, 'scoreTemplate');
scoreTemplate.remove();
scoreTarget.appendChild(fragment);
}
function initializeSelector(component, nodes, state) {
const { maxScore, score } = getValidatedInitialData(component);
const { summaryTarget } = nodes;
state.maxScore = maxScore;
state.score = score;
component.removeAttribute('max-score');
component.removeAttribute('score');
if (summaryTarget) {
const { summary } = state;
summary.attrName = ('value' in summaryTarget) && 'value' || 'textContent';
summary.literal = summaryTarget.dataset.literal ?? '';
Reflect.deleteProperty(summaryTarget.dataset, 'literal');
}
component.addEventListener('change', evt => handleScoreItemStateChange(evt, nodes, state));
renderSelector(component, nodes, state);
}
class StarRatingSelector extends HTMLElement {
#state = {
maxScore: null,
score: null,
summary: {
attrName: null,
literal: null,
value: null,
},
selectedIndex: -1,
};
#nodes = {};
constructor() {
super();
Object.assign(this.#nodes, {
scoreTemplate: this.querySelector('template[data-score-item]'),
scoreTarget: this.querySelector('[data-score-target]'),
summaryTarget: this.querySelector('[data-summary][data-literal]'),
});
}
get selectedIndex() {
return this.#state.selectedIndex;
}
get score() {
return this.#state.score;
}
get maxScore() {
return this.#state.maxScore;
}
get summary() {
return this.#state.summary.value;
}
connectedCallback() {
initializeSelector(this, this.#nodes, this.#state);
}
}
customElements.define('star-rating-selector', StarRatingSelector);
star-rating-selector/*,
[is = "star-rating-selector"]*/ {
position: relative;
display: block;
margin: 12px 0;
&:has(> fieldset) {
margin-top: 16px;
padding: 0 0 4px 0;
border: 1px solid black;
border-top: none;
}
> fieldset {
position: relative;
top: -.55em;
margin: 0 0 -1.4em 0;
border: none;
border-top: 1px solid black;
}
> [data-summary] {
overflow: hidden;
display: block;
width: 0;
height: 0;
margin: 0;
padding: 0;
&.visible {
overflow: unset;
width: unset;
height: unset;
margin: 0 0 6px 0;
padding: 0 16px;
}
}
[data-score-target] {
display: inline-block;
margin: 0 0 10px 0;
padding: 0;
[data-score-item] {
position: relative;
display: inline-block;
margin: 0 -2px;
> [data-label] {
overflow: hidden;
float: left;
width: 0;
height: 0;
}
> input {
position: absolute;
left: 8px;
top: 10px;
width: 0;
height: 0;
}
&::after {
overflow: hidden;
position: relative;
left: 0;
top: 1px;
padding: 1px 2px 0 2px;
content: '★';
font-size: 1.8em;
line-height: 1em;
color: white;
-webkit-text-stroke: 1px black;
}
&:focus-within {
&::after {
-webkit-text-stroke: 3px blue;
}
}
&:hover {
&::after {
-webkit-text-stroke: 3px purple;
}
cursor: pointer;
}
&:has(:checked) {
&::after {
color: gold;
}
}
}
&:has(:checked) {
[data-score-item] {
&::after {
color: gold;
}
&:has(:checked) ~ [data-score-item] {
&::after {
color: white;
}
}
}
}
&:hover {
[data-score-item] {
&::after {
color: yellow!important;
}
&:hover ~ [data-score-item] {
&::after {
color: white!important;
}
}
}
}
}
}
details { padding: 4px 0 8px 0; }
summary { padding: 0 0 4px 0; }
<star-rating-selector aria-label = "Star Rating Selector" role = "group" max-score = "4">
<template data-score-item>
<label data-score-item>
<span data-label data-literal = "Rate with ${ score } of ${ maxScore } stars.">
rate -- of -- stars
</span>
<input type = "radio" value = "" name = "out-of-four" />
</label>
</template>
<fieldset>
<legend>Please provide a score</legend>
<div data-score-target></div>
</fieldset>
<output data-summary data-literal = "The current rating is ${ score } of ${ maxScore } stars." class = "visible">
-- of -- stars selected
</output>
</star-rating-selector>
<star-rating-selector aria-label = "Star Rating Selector" role = "group" score = "3" max-score = "3">
<template data-score-item>
<label data-score-item>
<span data-label data-literal = "Rate with ${ score } of ${ maxScore } stars.">
rate -- of -- stars
</span>
<input type = "radio" value = "" name = "out-of-three" />
</label>
</template>
<fieldset>
<legend>Please provide your rating</legend>
<div data-score-target></div>
</fieldset>
<output data-summary data-literal = "${ score } of ${ maxScore } stars selected." class = "visible">
-- of -- stars selected
</output>
</star-rating-selector>
<star-rating-selector aria-label = "Star Rating Selector" role = "group" score = "5" max-score = "7">
<template data-score-item>
<label data-score-item>
<span data-label data-literal = "Rate with ${ score } of ${ maxScore } stars.">
rate -- of -- stars
</span>
<input type = "radio" value = "" name = "out-of-seven" />
</label>
</template>
<fieldset>
<legend>Give some stars</legend>
<div data-score-target></div>
</fieldset>
<output data-summary data-literal = "${ score } of ${ maxScore } stars selected.">
-- of -- stars selected
</output>
</star-rating-selector>
<details>
<summary>For Comparison Reason ...</summary>
... the CSS and JavaScript free base structure of any of the above "Star Rating" Components:
</details>
<fieldset aria-label = "Star Rating Selector">
<legend>Rate with ...</legend>
<label>
<span>
1 of 5 stars
</span>
<input type = "radio" value = "" name = "out-of-five" />
</label>
<label>
<span>
2 of 5 stars
</span>
<input type = "radio" value = "" name = "out-of-five" />
</label>
<label>
<span>
3 of 5 stars
</span>
<input type = "radio" value = "" name = "out-of-five" />
</label>
<label>
<span>
4 of 5 stars
</span>
<input type = "radio" value = "" name = "out-of-five" checked />
</label>
<label>
<span>
5 of 5 stars
</span>
<input type = "radio" value = "" name = "out-of-five" />
</label>
</fieldset>
Хорошая идея, хотя и немного запутанная, имхо. Недостатком здесь может быть то, что пользователь не может оценить, просто щелкнув одну из звездочек.
@KooiInc ... да, приведенный выше пример реализует исключительно пассивный рейтинг. А поскольку он служит цели «только для отображения», можно сохранить семантически простую разметку, например, базовую <figure/>
. Активный звездный компонент должен семантически следовать за <fieldset/>
, который окружает радиогруппу.
Я создал версию веб-компонента , реализующую активный рейтинг . Он использует (синтаксис, не относящийся к классу) пользовательский модуль веб-компонента, который я создал ранее.
@KooiInc ... интересно, мне это нравится. Преимущества подхода «без класса», который абстрагирует начальную загрузку / шаблонный код компонента класса, могут заключаться в том, что разработка будет направлена на императивный стиль, основанный на функциях, где необходимо передавать все необходимые ссылки, данные и события. . Таким образом, это не позволяет разработчикам, не имеющим опыта объектно-ориентированного проектирования, возиться с кодом из-за неправильного выбора дизайна и даже худших реализаций.
См. Останавливает ли возврат цикл?. Но вам следует подробно описать, что вы пытаетесь сделать, потому что это не
return
. Хотите создать массив? Строка? Добавить в DOM?