blob: fe509be18d2a40eeae6f60c38317de5c9fd47e8a [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {LitElement, html, css} from 'lit-element';
6
7/**
8 * `<mr-star>`
9 *
10 * A button for starring a resource. Does not directly integrate with app
11 * state. Subclasses by <mr-issue-star> and <mr-project-star>, which add
12 * resource-specific logic for state management.
13 *
14 */
15export class MrStar extends LitElement {
16 /** @override */
17 static get styles() {
18 return css`
19 :host {
20 display: block;
21 --mr-star-size: var(--chops-icon-font-size);
22 }
23 button {
24 background: none;
25 border: none;
26 cursor: pointer;
27 padding: 0;
28 margin: 0;
29 display: flex;
30 align-items: center;
31 }
32 /* TODO(crbug.com/monorail/8008): Add nicer looking loading style. */
33 button.loading {
34 opacity: 0.5;
35 cursor: default;
36 }
37 i.material-icons {
38 font-size: var(--mr-star-size);
39 color: var(--chops-primary-icon-color);
40 }
41 i.material-icons.starred {
42 color: var(--chops-primary-accent-color);
43 }
44 `;
45 }
46
47 /** @override */
48 render() {
49 const {isStarred} = this;
50 return html`
51 <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
52 <button class="star-button"
53 @click=${this._loginOrStar}
54 title=${this._starToolTip}
55 role="checkbox"
56 aria-checked=${isStarred ? 'true' : 'false'}
57 class=${this.requesting ? 'loading' : ''}
58 >
59 ${isStarred ? html`
60 <i class="material-icons starred" role="presentation">
61 star
62 </i>
63 `: html`
64 <i class="material-icons" role="presentation">
65 star_border
66 </i>
67 `}
68 </button>
69 `;
70 }
71
72 /** @override */
73 static get properties() {
74 return {
75 /**
76 * Note: In order for re-renders to happen based on the getters defined
77 * in this class, those getters must have values based on properties.
78 * Subclasses of <mr-star> are not expected to inherit <mr-star>'s
79 * properties, but they should make sure their getter implementations
80 * are also backed by properties.
81 */
82 _isStarred: {type: Boolean},
83 _isLoggedIn: {type: Boolean},
84 _canStar: {type: Boolean},
85 _requesting: {type: Boolean},
86 };
87 }
88
89 /** @override */
90 constructor() {
91 super();
92 /**
93 * @type {boolean} Whether the user has starred the resource or not.
94 */
95 this._isStarred = false;
96
97 /**
98 * @type {boolean} If the user is logged in.
99 */
100 this._isLoggedIn = false;
101
102 /**
103 * @return {boolean} Whether the user has permission to star the star.
104 */
105 this._canStar = true;
106
107 /**
108 * @return {boolean} Whether there's an in-flight request to star
109 * the resource.
110 */
111 this._requesting = false;
112 }
113
114 /** @override */
115 connectedCallback() {
116 super.connectedCallback();
117
118 // Prevent clicks on this element from causing navigation if the element
119 // is embedded inside a link.
120 this.addEventListener('click', (e) => e.preventDefault());
121 }
122
123 /**
124 * @return {boolean} If the user is logged in.
125 */
126 get isLoggedIn() {
127 return this._isLoggedIn;
128 }
129
130 /**
131 * @return {boolean} If there's an in-flight request that might affect the
132 * star's data.
133 */
134 get requesting() {
135 return this._requesting;
136 }
137
138 /**
139 * @return {boolean} Whether the resource is starred or not.
140 */
141 get isStarred() {
142 return this._isStarred;
143 }
144
145 /**
146 * @return {boolean} If the user has permission to star.
147 */
148 get canStar() {
149 return this._canStar;
150 }
151
152 /**
153 * @return {boolean}
154 */
155 get _starringEnabled() {
156 return this.isLoggedIn && this.canStar && !this.requesting;
157 }
158
159 /**
160 * @return {string} The name of the resource kind being starred.
161 * ie: issue, project, etc.
162 */
163 get type() {
164 return 'resource';
165 }
166
167 /**
168 * @return {string} the title to display on the star button.
169 */
170 get _starToolTip() {
171 if (!this.isLoggedIn) {
172 return `Login to star this ${this.type}.`;
173 }
174 if (!this.canStar) {
175 return `You don't have permission to star this ${this.type}.`;
176 }
177 if (this.requesting) {
178 return `Loading star state for this ${this.type}.`;
179 }
180 return `${this.isStarred ? 'Unstar' : 'Star'} this ${this.type}.`;
181 }
182
183 /**
184 * Logins the user if they're not logged in. Otherwise, stars or
185 * unstars the resource based on star state.
186 */
187 _loginOrStar() {
188 if (!this.isLoggedIn) {
189 this.login();
190 } else {
191 this.toggleStar();
192 }
193 }
194
195 /**
196 * Logs in the user.
197 */
198 login() {
199 // TODO(crbug.com/monorail/6073): Replace this logic with a function call
200 // when moving authentication to frontend.
201 // HACK: In our current login implementation, login URLs can only be
202 // generated by the backend which makes piping a login URL into a component
203 // a <mr-star> complex. To get around this, we're using the
204 // legacy window.CS_env infrastructure.
205 window.location.href = window.CS_env.login_url;
206 }
207
208 /**
209 * Stars or unstars the resource based on the user's interaction.
210 */
211 toggleStar() {
212 if (!this._starringEnabled) return;
213 if (this.isStarred) {
214 this.unstar();
215 } else {
216 this.star();
217 }
218 }
219
220 /**
221 * Stars the given resource. To be implemented by a subclass.
222 */
223 star() {
224 throw new Error('Method not implemented.');
225 }
226
227 /**
228 * Unstars the given resource. To be implemented by a subclass.
229 */
230 unstar() {
231 throw new Error('Method not implemented.');
232 }
233}
234
235customElements.define('mr-star', MrStar);