Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(167)

Side by Side Diff: ui/login/account_picker/user_pod_row.js

Issue 446743003: Hook up new API for easy unlock to update lock screen (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: v Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * @fileoverview User pod row implementation. 6 * @fileoverview User pod row implementation.
7 */ 7 */
8 8
9 cr.define('login', function() { 9 cr.define('login', function() {
10 /** 10 /**
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
48 * Variables used for pod placement processing. Width and height should be 48 * Variables used for pod placement processing. Width and height should be
49 * synced with computed CSS sizes of pods. 49 * synced with computed CSS sizes of pods.
50 */ 50 */
51 var POD_WIDTH = 180; 51 var POD_WIDTH = 180;
52 var PUBLIC_EXPANDED_BASIC_WIDTH = 500; 52 var PUBLIC_EXPANDED_BASIC_WIDTH = 500;
53 var PUBLIC_EXPANDED_ADVANCED_WIDTH = 610; 53 var PUBLIC_EXPANDED_ADVANCED_WIDTH = 610;
54 var CROS_POD_HEIGHT = 213; 54 var CROS_POD_HEIGHT = 213;
55 var DESKTOP_POD_HEIGHT = 226; 55 var DESKTOP_POD_HEIGHT = 226;
56 var POD_ROW_PADDING = 10; 56 var POD_ROW_PADDING = 10;
57 var DESKTOP_ROW_PADDING = 15; 57 var DESKTOP_ROW_PADDING = 15;
58 var CUSTOM_ICON_CONTAINER_SIZE = 40;
58 59
59 /** 60 /**
60 * Minimal padding between user pod and virtual keyboard. 61 * Minimal padding between user pod and virtual keyboard.
61 * @type {number} 62 * @type {number}
62 * @const 63 * @const
63 */ 64 */
64 var USER_POD_KEYBOARD_MIN_PADDING = 20; 65 var USER_POD_KEYBOARD_MIN_PADDING = 20;
65 66
66 /** 67 /**
67 * Maximum time for which the pod row remains hidden until all user images 68 * Maximum time for which the pod row remains hidden until all user images
(...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after
152 * Stops event propagation from the any user pod child element. 153 * Stops event propagation from the any user pod child element.
153 * @param {Event} e Event to handle. 154 * @param {Event} e Event to handle.
154 */ 155 */
155 function stopEventPropagation(e) { 156 function stopEventPropagation(e) {
156 // Prevent default so that we don't trigger a 'focus' event. 157 // Prevent default so that we don't trigger a 'focus' event.
157 e.preventDefault(); 158 e.preventDefault();
158 e.stopPropagation(); 159 e.stopPropagation();
159 } 160 }
160 161
161 /** 162 /**
163 * Creates an element for custom icon shown in a user pod next to the input
164 * field.
165 * @constructor
166 * @extends {HTMLDivElement}
167 */
168 var UserPodCustomIcon = cr.ui.define(function() {
169 var node = document.createElement('div');
170 node.classList.add('custom-icon-container');
171 node.hidden = true;
172
173 // Create the actual icon element and add it as a child to the container.
174 var iconNode = document.createElement('div');
175 iconNode.classList.add('custom-icon');
176 node.appendChild(iconNode);
177 return node;
178 });
179
180 UserPodCustomIcon.prototype = {
181 __proto__: HTMLDivElement.prototype,
182
183 /**
184 * The icon height.
185 * @type {number}
186 * @private
187 */
188 height_: 0,
189
190 /**
191 * The icon width.
192 * @type {number}
193 * @private
194 */
195 width_: 0,
196
197 /**
198 * Tooltip to be shown when the user hovers over the icon. The icon
199 * properties may be set so the tooltip is shown automatically when the icon
200 * is updated. The tooltip is shown in a bubble attached to the icon
201 * element.
202 * @type {string}
203 * @private
204 */
205 tooltip_: '',
206
207 /**
208 * Whether the tooltip is shown and the user is hovering over the icon.
209 * @type {boolean}
210 * @private
211 */
212 tooltipActive_: false,
213
214 /**
215 * Whether the icon has been shown as a result of |autoshow| parameter begin
216 * set rather than user hovering over the icon.
217 * If this is set, the tooltip will not be removed when the mouse leaves the
218 * icon.
219 * @type {boolean}
220 * @private
221 */
222 tooltipAutoshown_: false,
223
224 /**
225 * A reference to the timeout for showing tooltip after mouse enters the
226 * icon.
227 * @type {?number}
228 * @private
229 */
230 showTooltipTimeout_: null,
231
232 /**
233 * If animation is set, the current horizontal background offset for the
234 * icon resource.
235 * @type {number}
236 * @private
237 */
238 lastAnimationOffset_: 0,
239
240 /**
241 * The reference to interval for progressing the animation.
242 * @type {?number}
243 * @private
244 */
245 animationInterval_: null,
246
247 /**
248 * The width of the resource that contains representations for different
249 * animation stages.
250 * @type {number}
251 * @private
252 */
253 animationResourceSize_: 0,
254
255 /** @override */
256 decorate: function() {
257 this.iconElement.addEventListener('mouseover',
258 this.showTooltipSoon_.bind(this));
259 this.iconElement.addEventListener('mouseout',
260 this.hideTooltip_.bind(this, false));
261 this.iconElement.addEventListener('mousedown',
262 this.hideTooltip_.bind(this, false));
263 },
264
265 /**
266 * Getter for the icon element's div.
267 * @return {HTMLDivElement}
268 */
269 get iconElement() {
270 return this.querySelector('.custom-icon');
271 },
272
273 /**
274 * Set the icon's background image as image set with different
275 * representations for available screen scale factors.
276 * @param {!{scale1x: string, scale2x: string}} icon The icon
277 * representations.
278 */
279 setIconAsImageSet: function(icon) {
280 this.iconElement.style.backgroundImage =
281 '-webkit-image-set(' +
282 'url(' + icon.scale1x + ') 1x,' +
283 'url(' + icon.scale2x + ') 2x)';
284 },
285
286 /**
287 * Sets the icon background image to a chrome://theme URL.
288 * @param {!string} iconUrl The icon's background image URL.
289 */
290 setIconAsResourceUrl: function(iconUrl) {
291 this.iconElement.style.backgroundImage = 'url(' + iconUrl + ')';
292 },
293
294 /**
295 * Shows the icon.
296 */
297 show: function() {
298 this.hidden = false;
299 },
300
301 /**
302 * Hides the icon. Makes sure the tooltip is hidden and animation reset.
303 */
304 hide: function() {
305 this.hideTooltip_(true);
306 this.setAnimation(null);
307 this.hidden = true;
308 },
309
310 /**
311 * Sets the icon size element size.
312 * @param {!{width: number, height: number}} size The icon size.
313 */
314 setSize: function(size) {
315 this.height_ = size.height < CUSTOM_ICON_CONTAINER_SIZE ?
316 size.height : CUSTOM_ICON_COTAINER_SIZE;
317 this.iconElement.style.height = this.height_ + 'px';
318
319 this.width_ = size.width < CUSTOM_ICON_CONTAINER_SIZE ?
320 size.width : CUSTOM_ICON_COTAINER_SIZE;
321 this.iconElement.style.width = this.width_ + 'px';
322 },
323
324 /**
325 * Sets the icon opacity.
326 * @param {number} opacity The icon opacity in [0-100] scale.
327 */
328 setOpacity: function(opacity) {
329 if (opacity > 100) {
330 this.style.opacity = 1;
331 } else if (opacity < 0) {
332 this.style.opacity = 0;
333 } else {
334 this.style.opacity = opacity / 100;
335 }
336 },
337
338 /**
339 * Updates the icon tooltip. If {@code autoshow} parameter is set the
340 * tooltip is immediatelly shown. If tooltip text is not set, the method
341 * ensures the tooltip gets hidden. If tooltip is shown prior to this call,
342 * it remains shown, but the tooltip text is updated.
343 * @param {!{text: string, autoshow: boolean}} tooltip The tooltip
344 * parameters.
345 */
346 setTooltip: function(tooltip) {
347 if (this.tooltip_ == tooltip.text && !tooltip.autoshow)
348 return;
349 this.tooltip_ = tooltip.text;
350
351 // If tooltip is already shown, just update the text.
352 if (tooltip.text && this.tooltipActive_ && !$('bubble').hidden) {
353 // If both previous and the new tooltip are supposed to be shown
354 // automatically, keep the autoshown flag.
355 var markAutoshown = this.tooltipAutoshown_ && tooltip.autoshow;
356 this.hideTooltip_(true);
357 this.showTooltip_();
358 this.tooltipAutoshown_ = markAutoshown;
359 return;
360 }
361
362 // If autoshow flag is set, make sure the tooltip gets shown.
363 if (tooltip.text && tooltip.autoshow) {
364 this.hideTooltip_(true);
365 this.showTooltip_();
366 this.tooltipAutoshown_ = true;
367 // Reset the tooltip active flag, which gets set in |showTooltip_|.
368 this.tooltipActive_ = false;
369 return;
370 }
371
372 this.hideTooltip_(true);
373
374 if (this.tooltip_)
375 this.iconElement.setAttribute('aria-lablel', this.tooltip_);
376 },
377
378 /**
379 * Sets the icon animation parameter and starts the animation.
380 * The animation is set using the resource containing all animation frames
381 * concatenated horizontally. The animator offsets the background image in
382 * regural intervals.
383 * @param {?{resourceWidth: number, frameLengthMs: number}} animation
384 * |resourceWidth|: Total width for the resource containing the
385 * animation frames.
386 * |frameLengthMs|: Time interval for which a single animation frame is
387 * shown.
388 */
389 setAnimation: function(animation) {
390 if (this.animationInterval_)
391 clearInterval(this.animationInterval_);
392 this.iconElement.style.backgroundPosition = 'center';
393 if (!animation)
394 return;
395 this.lastAnimationOffset_ = 0;
396 this.animationResourceSize_ = animation.resourceWidth;
397 this.animationInterval_ = setInterval(this.progressAnimation_.bind(this),
398 animation.frameLengthMs);
399 },
400
401 /**
402 * Called when mouse enters the icon. It sets timeout for showing the
403 * tooltip.
404 * @private
405 */
406 showTooltipSoon_: function() {
407 if (this.showTooltipTimeout_ || this.tooltipActive_)
408 return;
409 this.showTooltipTimeout_ =
410 setTimeout(this.showTooltip_.bind(this), 1000);
411 },
412
413 /**
414 * Shows the current tooltip, if one is set.
415 * @private
416 */
417 showTooltip_: function() {
418 if (this.hidden || !this.tooltip_ || this.tooltipActive_)
419 return;
420
421 // If autoshown bubble got hidden, clear the autoshown flag.
422 if ($('bubble').hidden && this.tooltipAutoshown_)
423 this.tooltipAutoshown_ = false;
424
425 // Show the tooltip bubble.
426 var bubbleContent = document.createElement('div');
427 bubbleContent.textContent = this.tooltip_;
428
429 /** @const */ var BUBBLE_OFFSET = CUSTOM_ICON_CONTAINER_SIZE / 2;
430 /** @const */ var BUBBLE_PADDING = 8;
431 $('bubble').showContentForElement(this,
432 cr.ui.Bubble.Attachment.RIGHT,
433 bubbleContent,
434 BUBBLE_OFFSET,
435 BUBBLE_PADDING);
436 this.ensureTooltipTimeoutCleared_();
437 this.tooltipActive_ = true;
438 },
439
440 /**
441 * Hides the tooltip. If the current tooltip was automatically shown, it
442 * will be hidden only if |force| is set.
443 * @param {boolean} Whether the tooltip should be hidden if it got shown
444 * because autoshow flag was set.
445 * @private
446 */
447 hideTooltip_: function(force) {
448 this.tooltipActive_ = false;
449 this.ensureTooltipTimeoutCleared_();
450 if (!force && this.tooltipAutoshown_)
451 return;
452 $('bubble').hideForElement(this);
453 this.tooltipAutoshown_ = false;
454 this.iconElement.removeAttribute('aria-label');
455 },
456
457 /**
458 * Clears timaout for showing the tooltip if it's set.
459 * @private
460 */
461 ensureTooltipTimeoutCleared_: function() {
462 if (this.showTooltipTimeout_) {
463 clearTimeout(this.showTooltipTimeout_);
464 this.showTooltipTimeout_ = null;
465 }
466 },
467
468 /**
469 * Horizontally offsets the animated icon's background for a single icon
470 * size width.
471 * @private
472 */
473 progressAnimation_: function() {
474 this.lastAnimationOffset_ += this.width_;
475 if (this.lastAnimationOffset_ >= this.animationResourceSize_)
476 this.lastAnimationOffset_ = 0;
477 this.iconElement.style.backgroundPosition =
478 '-' + this.lastAnimationOffset_ + 'px center';
479 }
480 };
481
482 /**
162 * Unique salt added to user image URLs to prevent caching. Dictionary with 483 * Unique salt added to user image URLs to prevent caching. Dictionary with
163 * user names as keys. 484 * user names as keys.
164 * @type {Object} 485 * @type {Object}
165 */ 486 */
166 UserPod.userImageSalt_ = {}; 487 UserPod.userImageSalt_ = {};
167 488
168 UserPod.prototype = { 489 UserPod.prototype = {
169 __proto__: HTMLDivElement.prototype, 490 __proto__: HTMLDivElement.prototype,
170 491
171 /** @override */ 492 /** @override */
(...skipping 19 matching lines...) Expand all
191 this.actionBoxMenuRemoveElement.addEventListener('keydown', 512 this.actionBoxMenuRemoveElement.addEventListener('keydown',
192 this.handleRemoveCommandKeyDown_.bind(this)); 513 this.handleRemoveCommandKeyDown_.bind(this));
193 this.actionBoxMenuRemoveElement.addEventListener('blur', 514 this.actionBoxMenuRemoveElement.addEventListener('blur',
194 this.handleRemoveCommandBlur_.bind(this)); 515 this.handleRemoveCommandBlur_.bind(this));
195 this.actionBoxRemoveUserWarningButtonElement.addEventListener( 516 this.actionBoxRemoveUserWarningButtonElement.addEventListener(
196 'click', 517 'click',
197 this.handleRemoveUserConfirmationClick_.bind(this)); 518 this.handleRemoveUserConfirmationClick_.bind(this));
198 this.actionBoxRemoveUserWarningButtonElement.addEventListener( 519 this.actionBoxRemoveUserWarningButtonElement.addEventListener(
199 'keydown', 520 'keydown',
200 this.handleRemoveUserConfirmationKeyDown_.bind(this)); 521 this.handleRemoveUserConfirmationKeyDown_.bind(this));
522
523 var customIcon = this.customIconElement;
524 customIcon.parentNode.replaceChild(new UserPodCustomIcon(), customIcon);
201 }, 525 },
202 526
203 /** 527 /**
204 * Initializes the pod after its properties set and added to a pod row. 528 * Initializes the pod after its properties set and added to a pod row.
205 */ 529 */
206 initialize: function() { 530 initialize: function() {
207 this.passwordElement.addEventListener('keydown', 531 this.passwordElement.addEventListener('keydown',
208 this.parentNode.handleKeyDown.bind(this.parentNode)); 532 this.parentNode.handleKeyDown.bind(this.parentNode));
209 this.passwordElement.addEventListener('keypress', 533 this.passwordElement.addEventListener('keypress',
210 this.handlePasswordKeyPress_.bind(this)); 534 this.handlePasswordKeyPress_.bind(this));
(...skipping 227 matching lines...) Expand 10 before | Expand all | Expand 10 after
438 get actionBoxRemoveUserWarningButtonElement() { 762 get actionBoxRemoveUserWarningButtonElement() {
439 return this.querySelector('.remove-warning-button'); 763 return this.querySelector('.remove-warning-button');
440 }, 764 },
441 765
442 /** 766 /**
443 * Gets the custom icon. This icon is normally hidden, but can be shown 767 * Gets the custom icon. This icon is normally hidden, but can be shown
444 * using the chrome.screenlockPrivate API. 768 * using the chrome.screenlockPrivate API.
445 * @type {!HTMLDivElement} 769 * @type {!HTMLDivElement}
446 */ 770 */
447 get customIconElement() { 771 get customIconElement() {
448 return this.querySelector('.custom-icon'); 772 return this.querySelector('.custom-icon-container');
449 }, 773 },
450 774
451 /** 775 /**
452 * Updates the user pod element. 776 * Updates the user pod element.
453 */ 777 */
454 update: function() { 778 update: function() {
455 this.imageElement.src = 'chrome://userimage/' + this.user.username + 779 this.imageElement.src = 'chrome://userimage/' + this.user.username +
456 '?id=' + UserPod.userImageSalt_[this.user.username]; 780 '?id=' + UserPod.userImageSalt_[this.user.username];
457 781
458 this.nameElement.textContent = this.user_.displayName; 782 this.nameElement.textContent = this.user_.displayName;
(...skipping 1377 matching lines...) Expand 10 before | Expand all | Expand 10 after
1836 if (this.shouldShowApps_ == shouldShowApps) 2160 if (this.shouldShowApps_ == shouldShowApps)
1837 return; 2161 return;
1838 2162
1839 this.shouldShowApps_ = shouldShowApps; 2163 this.shouldShowApps_ = shouldShowApps;
1840 this.rebuildPods(); 2164 this.rebuildPods();
1841 }, 2165 },
1842 2166
1843 /** 2167 /**
1844 * Shows a custom icon on a user pod besides the input field. 2168 * Shows a custom icon on a user pod besides the input field.
1845 * @param {string} username Username of pod to add button 2169 * @param {string} username Username of pod to add button
1846 * @param {{scale1x: string, scale2x: string}} icon Dictionary of URLs of 2170 * @param {!{resourceUrl: (string | undefined),
1847 * the custom icon's representations for 1x and 2x scale factors. 2171 * data: ({scale1x: string, scale2x: string} | undefined),
2172 * size: ({width: number, height: number} | undefined),
2173 * animation: ({resourceWidth: number, frameLength: number} |
2174 * undefined),
2175 * opacity: (number | undefined),
2176 * tooltip: ({text: string, autoshow: boolean} | undefined)}} icon
2177 * The icon parameters.
1848 */ 2178 */
1849 showUserPodCustomIcon: function(username, icon) { 2179 showUserPodCustomIcon: function(username, icon) {
1850 var pod = this.getPodWithUsername_(username); 2180 var pod = this.getPodWithUsername_(username);
1851 if (pod == null) { 2181 if (pod == null) {
1852 console.error('Unable to show user pod button for ' + username + 2182 console.error('Unable to show user pod button for ' + username +
1853 ': user pod not found.'); 2183 ': user pod not found.');
1854 return; 2184 return;
1855 } 2185 }
1856 2186
1857 pod.customIconElement.hidden = false; 2187 if (icon.resourceUrl) {
1858 pod.customIconElement.style.backgroundImage = 2188 pod.customIconElement.setIconAsResourceUrl(icon.resourceUrl);
1859 '-webkit-image-set(' + 2189 } else if (icon.data) {
1860 'url(' + icon.scale1x + ') 1x,' + 2190 pod.customIconElement.setIconAsImageSet(icon.data);
1861 'url(' + icon.scale2x + ') 2x)'; 2191 } else {
2192 return;
2193 }
2194
2195 pod.customIconElement.setSize(icon.size || {width: 0, height: 0});
2196 pod.customIconElement.setAnimation(icon.animation || null);
2197 pod.customIconElement.setOpacity(icon.opacity || 100);
2198 pod.customIconElement.show();
2199 // This has to be called after |show| in case the tooltip should be shown
2200 // immediatelly.
2201 pod.customIconElement.setTooltip(
2202 icon.tooltip || {text: '', autoshow: false});
1862 }, 2203 },
1863 2204
1864 /** 2205 /**
1865 * Hides the custom icon in the user pod added by showUserPodCustomIcon(). 2206 * Hides the custom icon in the user pod added by showUserPodCustomIcon().
1866 * @param {string} username Username of pod to remove button 2207 * @param {string} username Username of pod to remove button
1867 */ 2208 */
1868 hideUserPodCustomIcon: function(username) { 2209 hideUserPodCustomIcon: function(username) {
1869 var pod = this.getPodWithUsername_(username); 2210 var pod = this.getPodWithUsername_(username);
1870 if (pod == null) { 2211 if (pod == null) {
1871 console.error('Unable to hide user pod button for ' + username + 2212 console.error('Unable to hide user pod button for ' + username +
1872 ': user pod not found.'); 2213 ': user pod not found.');
1873 return; 2214 return;
1874 } 2215 }
1875 2216
1876 pod.customIconElement.hidden = true; 2217 pod.customIconElement.hide();
1877 }, 2218 },
1878 2219
1879 /** 2220 /**
1880 * Sets the authentication type used to authenticate the user. 2221 * Sets the authentication type used to authenticate the user.
1881 * @param {string} username Username of selected user 2222 * @param {string} username Username of selected user
1882 * @param {number} authType Authentication type, must be one of the 2223 * @param {number} authType Authentication type, must be one of the
1883 * values listed in AUTH_TYPE enum. 2224 * values listed in AUTH_TYPE enum.
1884 * @param {string} value The initial value to use for authentication. 2225 * @param {string} value The initial value to use for authentication.
1885 */ 2226 */
1886 setAuthType: function(username, authType, value) { 2227 setAuthType: function(username, authType, value) {
1887 var pod = this.getPodWithUsername_(username); 2228 var pod = this.getPodWithUsername_(username);
1888 if (pod == null) { 2229 if (pod == null) {
1889 console.error('Unable to set auth type for ' + username + 2230 console.error('Unable to set auth type for ' + username +
1890 ': user pod not found.'); 2231 ': user pod not found.');
1891 return; 2232 return;
1892 } 2233 }
1893 pod.setAuthType(authType, value); 2234 pod.setAuthType(authType, value);
1894 }, 2235 },
1895 2236
1896 /** 2237 /**
1897 * Shows a tooltip bubble explaining Easy Unlock for the focused pod.
1898 */
1899 showEasyUnlockBubble: function() {
1900 if (!this.focusedPod_) {
1901 console.error('No focused pod to show Easy Unlock bubble.');
1902 return;
1903 }
1904
1905 var bubbleContent = document.createElement('div');
1906 bubbleContent.classList.add('easy-unlock-button-content');
1907 bubbleContent.textContent = loadTimeData.getString('easyUnlockTooltip');
1908
1909 var attachElement = this.focusedPod_.customIconElement;
1910 /** @const */ var BUBBLE_OFFSET = 20;
1911 /** @const */ var BUBBLE_PADDING = 8;
1912 $('bubble').showContentForElement(attachElement,
1913 cr.ui.Bubble.Attachment.RIGHT,
1914 bubbleContent,
1915 BUBBLE_OFFSET,
1916 BUBBLE_PADDING);
1917 },
1918
1919 /**
1920 * Updates the display name shown on a public session pod. 2238 * Updates the display name shown on a public session pod.
1921 * @param {string} userID The user ID of the public session 2239 * @param {string} userID The user ID of the public session
1922 * @param {string} displayName The new display name 2240 * @param {string} displayName The new display name
1923 */ 2241 */
1924 setPublicSessionDisplayName: function(userID, displayName) { 2242 setPublicSessionDisplayName: function(userID, displayName) {
1925 var pod = this.getPodWithUsername_(userID); 2243 var pod = this.getPodWithUsername_(userID);
1926 if (pod != null) 2244 if (pod != null)
1927 pod.setDisplayName(displayName); 2245 pod.setDisplayName(displayName);
1928 }, 2246 },
1929 2247
(...skipping 604 matching lines...) Expand 10 before | Expand all | Expand 10 after
2534 if (pod && pod.multiProfilesPolicyApplied) { 2852 if (pod && pod.multiProfilesPolicyApplied) {
2535 pod.userTypeBubbleElement.classList.remove('bubble-shown'); 2853 pod.userTypeBubbleElement.classList.remove('bubble-shown');
2536 } 2854 }
2537 } 2855 }
2538 }; 2856 };
2539 2857
2540 return { 2858 return {
2541 PodRow: PodRow 2859 PodRow: PodRow
2542 }; 2860 };
2543 }); 2861 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698