/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.wicket.extensions.rating; import java.util.Optional; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.AjaxFallbackLink; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.list.Loop; import org.apache.wicket.markup.html.list.LoopItem; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler; import org.apache.wicket.request.resource.CssResourceReference; import org.apache.wicket.request.resource.PackageResourceReference; import org.apache.wicket.request.resource.ResourceReference; /** * Rating component that generates a number of stars where a user can click on to rate something. * Subclasses should implement {@link #onRated(int, Optional)} to provide the calculation * of the rating, and {@link #onIsStarActive(int)} to indicate whether to render an active star or * an inactive star. * <p> * Active stars are the stars that show the rating, inactive stars are the left overs. E.G. a rating * of 3.4 on a scale of 5 stars will render 3 active stars, and 2 inactive stars (provided that the * {@link #onIsStarActive(int)} returns <code>true</code> for each of the first three stars). * <p> * Use this component in the following way: * * <pre> * add(new RatingPanel("rating", new PropertyModel(rating, "rating"), 5) * { * protected boolean onIsStarActive(int star) * { * return rating.isActive(star); * } * * protected void onRated(int rating, AjaxRequestTarget target) * { * rating1.addRating(rating); * } * }); * </pre> * * The user of this component is responsible for creating a model that supplies a Double (or Float) * value for the rating message, however the rating panel doesn't necessarily have to contain a * float or number rating value. * <p> * Though not obligatory, you could also supply a value for the number of votes cast, which allows * the component to render a more complete message in the rating label. * * <h2>Customizing the rating value and label</h2> * To customize the rating value, one should override the * {@link #newRatingLabel(String, IModel, IModel)} method and create another label instead, based on * the provided models. If you do so, and use another system of rating than returning a Float or * Double, then you should also customize the rating resource bundle to reflect your message. The * default resource bundle assumes a numeric value for the rating. * * <h2>Resource bundle</h2> * This component uses two types of messages: rating.simple and rating.complete. The first message * is used when no model is given for the number of cast votes. The complete message shows the text * 'Rating xx.yy from zz votes'. * * <pre> * rating.simple=Rated {0,number,#.#} * rating.complete=Rated {0,number,#.#} from {1,number,#} votes * </pre> * * <h2>Customizing the star images</h2> * To customize the images shown, override the {@link #getActiveStarUrl(int)} and * {@link #getInactiveStarUrl(int)} methods. Using the iteration parameter it is possible to use a * different image for each star, creating a fade effect or something similar. * * @author Martijn Dashorst */ public abstract class RatingPanel extends Panel { /** * Renders the stars and the links necessary for rating. */ private final class RatingStarBar extends Loop { /** For serialization. */ private static final long serialVersionUID = 1L; private RatingStarBar(final String id, final IModel<Integer> model) { super(id, model); } @Override protected void populateItem(final LoopItem item) { // Use an AjaxFallbackLink for rating to make voting work even // without Ajax. AjaxFallbackLink<Void> link = new AjaxFallbackLink<Void>("link") { private static final long serialVersionUID = 1L; @Override public void onClick(Optional<AjaxRequestTarget> targetOptional) { LoopItem item = (LoopItem)getParent(); // adjust the rating, and provide the target to the subclass // of our rating component, so other components can also get // updated in case of an AJAX event. onRated(item.getIndex() + 1, targetOptional); // if we process an AJAX event, update this panel targetOptional.ifPresent(target -> target.add(RatingPanel.this.get("rater"))); } @Override public boolean isEnabled() { return !hasVoted.getObject(); } }; int iteration = item.getIndex(); // add the star image, which is either active (highlighted) or // inactive (no star) link.add(new WebMarkupContainer("star").add(AttributeModifier.replace("src", (onIsStarActive(iteration) ? getActiveStarUrl(iteration) : getInactiveStarUrl(iteration))))); item.add(link); } } /** For serialization. */ private static final long serialVersionUID = 1L; /** * Star image for no selected star */ public static final ResourceReference STAR0 = new PackageResourceReference(RatingPanel.class, "star0.gif"); /** * Star image for selected star */ public static final ResourceReference STAR1 = new PackageResourceReference(RatingPanel.class, "star1.gif"); /** * The number of stars that need to be shown, should result in an Integer object. */ private IModel<Integer> nrOfStars = new Model<>(5); /** * The number of votes that have been cast, should result in an Integer object. */ private final IModel<Integer> nrOfVotes; /** * The flag on whether the current user has voted already. */ private final IModel<Boolean> hasVoted; /** * Handle to the rating label to set the visibility. */ private Component ratingLabel; private final boolean addDefaultCssStyle; /** * Constructs a rating component with 5 stars, using a compound property model as its model to * retrieve the rating. * * @param id * the component id. */ public RatingPanel(final String id) { this(id, null, 5, true); } /** * Constructs a rating component with 5 stars, using the rating for retrieving the rating. * * @param id * the component id * @param rating * the model to get the rating */ public RatingPanel(final String id, final IModel<? extends Number> rating) { this(id, rating, new Model<Integer>(5), null, new Model<>(Boolean.FALSE), true); } /** * Constructs a rating component with nrOfStars stars, using a compound property model as its * model to retrieve the rating. * * @param id * the component id * @param nrOfStars * the number of stars to display */ public RatingPanel(final String id, final int nrOfStars) { this(id, null, nrOfStars, true); } /** * Constructs a rating component with nrOfStars stars, using the rating for retrieving the * rating. * * @param id * the component id * @param rating * the model to get the rating * @param nrOfStars * the number of stars to display * @param addDefaultCssStyle * should this component render its own default CSS style? */ public RatingPanel(final String id, final IModel<? extends Number> rating, final int nrOfStars, final boolean addDefaultCssStyle) { this(id, rating, new Model<Integer>(nrOfStars), null, new Model<Boolean>(Boolean.FALSE), addDefaultCssStyle); } /** * Constructs a rating panel with nrOfStars stars, where the rating model is used to retrieve * the rating, the nrOfVotes model to retrieve the number of casted votes. This panel doens't * keep track of whether the user has already voted. * * @param id * the component id * @param rating * the model to get the rating * @param nrOfStars * the number of stars to display * @param nrOfVotes * the number of cast votes * @param addDefaultCssStyle * should this component render its own default CSS style? */ public RatingPanel(final String id, final IModel<? extends Number> rating, final int nrOfStars, final IModel<Integer> nrOfVotes, final boolean addDefaultCssStyle) { this(id, rating, new Model<Integer>(nrOfStars), nrOfVotes, new Model<>(Boolean.FALSE), addDefaultCssStyle); } /** * Constructs a rating panel with nrOfStars stars, where the rating model is used to retrieve * the rating, the nrOfVotes model used to retrieve the number of votes cast and the hasVoted * model to retrieve whether the user already had cast a vote. * * @param id * the component id. * @param rating * the (calculated) rating, i.e. 3.4 * @param nrOfStars * the number of stars to display * @param nrOfVotes * the number of cast votes * @param hasVoted * has the user already voted? * @param addDefaultCssStyle * should this component render its own default CSS style? */ public RatingPanel(final String id, final IModel<? extends Number> rating, final IModel<Integer> nrOfStars, final IModel<Integer> nrOfVotes, final IModel<Boolean> hasVoted, final boolean addDefaultCssStyle) { super(id, rating); this.addDefaultCssStyle = addDefaultCssStyle; this.nrOfStars = wrap(nrOfStars); this.nrOfVotes = wrap(nrOfVotes); this.hasVoted = wrap(hasVoted); WebMarkupContainer rater = new WebMarkupContainer("rater"); rater.add(newRatingStarBar("element", this.nrOfStars)); // add the text label for the message 'Rating 4.5 out of 25 votes' rater.add(ratingLabel = newRatingLabel("rating", wrap(rating), this.nrOfVotes)); // set auto generation of the markup id on, such that ajax calls work. rater.setOutputMarkupId(true); add(rater); // don't render the outer tags in the target document, just the div that // is inside the panel. setRenderBodyOnly(true); } @Override public void renderHead(final IHeaderResponse response) { super.renderHead(response); if (addDefaultCssStyle) { response.render(CssHeaderItem.forReference(new CssResourceReference( RatingPanel.class, "RatingPanel.css"))); } } /** * Creates a new bar filled with stars to click on. * * @param id * the bar id * @param nrOfStars * the number of stars to generate * @return the bar with rating stars */ protected Component newRatingStarBar(final String id, final IModel<Integer> nrOfStars) { return new RatingStarBar(id, nrOfStars); } /** * Creates a new rating label, showing a message like 'Rated 5.4 from 53 votes'. * * @param id * the id of the label * @param rating * the model containing the rating * @param nrOfVotes * the model containing the number of votes (may be null) * @return the label component showing the message. */ protected Component newRatingLabel(final String id, final IModel<? extends Number> rating, final IModel<Integer> nrOfVotes) { IModel<String> model; if (nrOfVotes == null) { Object[] parameters = new Object[] { rating }; model = new StringResourceModel("rating.simple", this).setParameters(parameters); } else { Object[] parameters = new Object[] { rating, nrOfVotes }; model = new StringResourceModel("rating.complete", this).setParameters(parameters); } return new Label(id, model); } /** * Returns the url pointing to the image of active stars, is used to set the URL for the image * of an active star. Override this method to provide your own images. * * @param iteration * the sequence number of the star * @return the url pointing to the image for active stars. */ protected String getActiveStarUrl(final int iteration) { IRequestHandler handler = new ResourceReferenceRequestHandler(STAR1); return getRequestCycle().urlFor(handler).toString(); } /** * Returns the url pointing to the image of inactive stars, is used to set the URL for the image * of an inactive star. Override this method to provide your own images. * * @param iteration * the sequence number of the star * @return the url pointing to the image for inactive stars. */ protected String getInactiveStarUrl(final int iteration) { IRequestHandler handler = new ResourceReferenceRequestHandler(STAR0); return getRequestCycle().urlFor(handler).toString(); } /** * Sets the visibility of the rating label. * * @param visible * true when the label should be visible * @return this for chaining. */ public RatingPanel setRatingLabelVisible(final boolean visible) { ratingLabel.setVisible(visible); return this; } /** * Returns <code>true</code> when the star identified by its sequence number should be shown as * active. * * @param star * the sequence number of the star (ranging from 0 to nrOfStars) * @return <code>true</code> when the star should be rendered as active */ protected abstract boolean onIsStarActive(int star); /** * Notification of a click on a rating star. Add your own components to the request target when * you want to have them updated in the Ajax request. <strong>NB</strong> the target may be null * when the click isn't handled using AJAX, but using a fallback scenario. * @param rating * the number of the star that is clicked, ranging from 1 to nrOfStars * @param target */ protected abstract void onRated(int rating, Optional<AjaxRequestTarget> target); }