UX: Improve second factor UI (#9526)

This will make a few minor improvements to the second factor user interface. Highlights include:

- Using the site's title to prefix the backup code filename. If non-ascii characters are detected, then prefix "discourse" instead.
- Add icons and change the text on some of the buttons for better clarity and consistency
- Add an education link to the security key modal
This commit is contained in:
tshenry 2020-05-04 18:05:25 -07:00 committed by GitHub
parent d59d170452
commit b8b1cbbfb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 239 additions and 109 deletions

View File

@ -1,5 +1,6 @@
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component"; import Component from "@ember/component";
import { toAsciiPrintable, slugify } from "discourse/lib/utilities";
// https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
function b64EncodeUnicode(str) { function b64EncodeUnicode(str) {
@ -42,6 +43,14 @@ export default Component.extend({
return backupCodes.join("\n").trim(); return backupCodes.join("\n").trim();
}, },
@discourseComputed()
siteTitleSlug() {
const title = this.siteSettings.title;
const convertedTitle = toAsciiPrintable(title, "discourse");
return slugify(convertedTitle);
},
actions: { actions: {
copyToClipboard() { copyToClipboard() {
this._selectAllBackupCodes(); this._selectAllBackupCodes();

View File

@ -1,7 +1,6 @@
import { alias } from "@ember/object/computed"; import { alias } from "@ember/object/computed";
import { later } from "@ember/runloop"; import { later } from "@ember/runloop";
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import { SECOND_FACTOR_METHODS } from "discourse/models/user"; import { SECOND_FACTOR_METHODS } from "discourse/models/user";
import ModalFunctionality from "discourse/mixins/modal-functionality"; import ModalFunctionality from "discourse/mixins/modal-functionality";
@ -14,13 +13,6 @@ export default Controller.extend(ModalFunctionality, {
backupCodes: null, backupCodes: null,
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP, secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
@discourseComputed("backupEnabled")
generateBackupCodeBtnLabel(backupEnabled) {
return backupEnabled
? "user.second_factor_backup.regenerate"
: "user.second_factor_backup.enable";
},
onShow() { onShow() {
this.setProperties({ this.setProperties({
loading: false, loading: false,

View File

@ -328,6 +328,28 @@ export function clipboardData(e, canUpload) {
return { clipboard, types, canUpload, canPasteHtml }; return { clipboard, types, canUpload, canPasteHtml };
} }
// Replace any accented characters with their ASCII equivalent
// Return the string if it only contains ASCII printable characters,
// otherwise use the fallback
export function toAsciiPrintable(string, fallback) {
if (typeof string.normalize === "function") {
string = string.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
return /^[/\040-\176/]*$/.test(string) ? string : fallback;
}
export function slugify(string) {
return string
.trim()
.toLowerCase()
.replace(/\s|_+/g, "-") // Replace spaces and underscores with dashes
.replace(/[^\w\-]+/g, "") // Remove non-word characters except for dashes
.replace(/\-\-+/g, "-") // Replace multiple dashes with a single dash
.replace(/^-+/, "") // Remove leading dashes
.replace(/-+$/, ""); // Remove trailing dashes
}
export function toNumber(input) { export function toNumber(input) {
return typeof input === "number" ? input : parseFloat(input); return typeof input === "number" ? input : parseFloat(input);
} }

View File

@ -4,10 +4,14 @@
{{d-button {{d-button
action=(action "copyToClipboard") action=(action "copyToClipboard")
class="backup-codes-copy-btn" class="backup-codes-copy-btn"
icon="copy"}} icon="copy"
aria-label="user.second_factor_backup.copy_to_clipboard"
title="user.second_factor_backup.copy_to_clipboard"}}
<a download="backup_codes.txt" <a download="{{siteTitleSlug}}-backup-codes.txt"
class="btn no-text btn-icon backup-codes-download-btn" class="btn no-text btn-icon backup-codes-download-btn"
aria-label={{i18n "user.second_factor_backup.download_backup_codes"}}
title={{i18n "user.second_factor_backup.download_backup_codes"}}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
href="data:application/octet-stream;charset=utf-8;base64,{{base64BackupCode}}"> href="data:application/octet-stream;charset=utf-8;base64,{{base64BackupCode}}">

View File

@ -18,18 +18,27 @@
{{/if}} {{/if}}
<div class="actions"> <div class="actions">
{{#if backupEnabled}}
{{d-button
class="btn-primary"
icon="redo"
action=(action "generateSecondFactorCodes")
type="submit"
isLoading=loading
label="user.second_factor_backup.regenerate"}}
{{d-button
class="btn-danger"
icon="ban"
action=(action "disableSecondFactorBackup")
disabled=loading
label="user.second_factor_backup.disable"}}
{{else}}
{{d-button {{d-button
class="btn-primary" class="btn-primary"
action=(action "generateSecondFactorCodes") action=(action "generateSecondFactorCodes")
type="submit" type="submit"
disabled=loading disabled=loading
label=generateBackupCodeBtnLabel}} label="user.second_factor_backup.enable"}}
{{#if backupEnabled}}
{{d-button
class="btn-danger"
action=(action "disableSecondFactorBackup")
disabled=loading
label="user.second_factor_backup.disable"}}
{{/if}} {{/if}}
</div> </div>

View File

@ -8,11 +8,13 @@
{{d-button {{d-button
action=(action "editSecurityKey") action=(action "editSecurityKey")
class="btn-primary" class="btn-primary"
label="user.second_factor.security_key.edit" label="user.second_factor.security_key.save"
}} }}
{{d-button {{d-button
action=(action "disableSecurityKey") action=(action "disableSecurityKey")
class="btn-danger" class="btn-danger"
label="user.second_factor.security_key.delete" icon="trash-alt"
aria-label="user.second_factor.disable"
title="user.second_factor.disable"
}} }}
{{/d-modal-body}} {{/d-modal-body}}

View File

@ -8,11 +8,13 @@
{{d-button {{d-button
action=(action "editSecondFactor") action=(action "editSecondFactor")
class="btn-primary" class="btn-primary"
label="user.second_factor.edit" label="user.second_factor.save"
}} }}
{{d-button {{d-button
action=(action "disableSecondFactor") action=(action "disableSecondFactor")
class="btn-danger" class="btn-danger"
label="user.second_factor.disable" icon="trash-alt"
aria-label="user.second_factor.disable"
title="user.second_factor.disable"
}} }}
{{/d-modal-body}} {{/d-modal-body}}

View File

@ -31,6 +31,7 @@
<h2>{{i18n "user.second_factor.totp.title"}}</h2> <h2>{{i18n "user.second_factor.totp.title"}}</h2>
{{d-button action=(action "createTotp") {{d-button action=(action "createTotp")
class="btn-primary new-totp" class="btn-primary new-totp"
icon="plus"
disabled=loading disabled=loading
label="user.second_factor.totp.add"}} label="user.second_factor.totp.add"}}
{{#each totps as |totp|}} {{#each totps as |totp|}}
@ -46,6 +47,8 @@
class="btn-default btn-small btn-icon pad-left no-text edit" class="btn-default btn-small btn-icon pad-left no-text edit"
disabled=loading disabled=loading
icon="pencil-alt" icon="pencil-alt"
aria-label="user.second_factor.edit"
title="user.second_factor.edit"
}} }}
{{/if}} {{/if}}
</div> </div>
@ -58,6 +61,7 @@
<h2>{{i18n "user.second_factor.security_key.title"}}</h2> <h2>{{i18n "user.second_factor.security_key.title"}}</h2>
{{d-button action=(action "createSecurityKey") {{d-button action=(action "createSecurityKey")
class="btn-primary new-security-key" class="btn-primary new-security-key"
icon="plus"
disabled=loading disabled=loading
label="user.second_factor.security_key.add"}} label="user.second_factor.security_key.add"}}
{{#each security_keys as |security_key|}} {{#each security_keys as |security_key|}}
@ -73,6 +77,8 @@
class="btn-default btn-small btn-icon pad-left no-text edit" class="btn-default btn-small btn-icon pad-left no-text edit"
disabled=loading disabled=loading
icon="pencil-alt" icon="pencil-alt"
aria-label="user.second_factor.edit"
title="user.second_factor.edit"
}} }}
{{/if}} {{/if}}
</div> </div>
@ -95,6 +101,8 @@
class="btn-default btn-small btn-icon pad-left no-text edit edit-2fa-backup" class="btn-default btn-small btn-icon pad-left no-text edit edit-2fa-backup"
disabled=loading disabled=loading
icon="pencil-alt" icon="pencil-alt"
aria-label="user.second_factor.edit"
title="user.second_factor.edit"
}} }}
{{/if}} {{/if}}
{{else}} {{else}}
@ -107,9 +115,9 @@
{{#unless showEnforcedNotice}} {{#unless showEnforcedNotice}}
<div class="control-group"> <div class="control-group">
<div class="controls"> <div class="controls">
<h2>{{i18n "user.second_factor.disable_title"}}</h2>
{{d-button {{d-button
class="btn-danger" class="btn-danger"
icon="ban"
action=(action "disableAllSecondFactors") action=(action "disableAllSecondFactors")
disabled=loading disabled=loading
label="disable"}} label="disable"}}

View File

@ -590,9 +590,14 @@
margin-right: 10px; margin-right: 10px;
} }
.form-horizontal .instructions { .form-horizontal {
.instructions {
margin-left: 0; margin-left: 0;
} }
.actions {
margin-top: 5px;
}
}
.backup-codes { .backup-codes {
margin: 2em 0; margin: 2em 0;

View File

@ -1010,8 +1010,10 @@ en:
enable: "Enable" enable: "Enable"
enable_long: "Enable backup codes" enable_long: "Enable backup codes"
manage: "Manage backup codes. You have <strong>{{count}}</strong> backup codes remaining." manage: "Manage backup codes. You have <strong>{{count}}</strong> backup codes remaining."
copied_to_clipboard: "Copied to Clipboard" copy_to_clipboard: "Copy to Clipboard"
copy_to_clipboard_error: "Error copying data to Clipboard" copy_to_clipboard_error: "Error copying data to Clipboard"
copied_to_clipboard: "Copied to Clipboard"
download_backup_codes: "Download backup codes"
remaining_codes: "You have <strong>{{count}}</strong> backup codes remaining." remaining_codes: "You have <strong>{{count}}</strong> backup codes remaining."
use: "Use a backup code" use: "Use a backup code"
enable_prerequisites: "You must enable a primary second factor before generating backup codes." enable_prerequisites: "You must enable a primary second factor before generating backup codes."
@ -1038,28 +1040,29 @@ en:
oauth_enabled_warning: "Please note that social logins will be disabled once two factor authentication has been enabled on your account." oauth_enabled_warning: "Please note that social logins will be disabled once two factor authentication has been enabled on your account."
use: "Use Authenticator app" use: "Use Authenticator app"
enforced_notice: "You are required to enable two factor authentication before accessing this site." enforced_notice: "You are required to enable two factor authentication before accessing this site."
disable: "disable" disable: "Disable"
disable_title: "Disable Second Factor"
disable_confirm: "Are you sure you want to disable all second factors?" disable_confirm: "Are you sure you want to disable all second factors?"
save: "Save"
edit: "Edit" edit: "Edit"
edit_title: "Edit Second Factor" edit_title: "Edit Second Factor"
edit_description: "Second Factor Name" edit_description: "Second Factor Name"
enable_security_key_description: "When you have your physical security key prepared press the Register button below." enable_security_key_description: |
When you have your <a href="https://www.google.com/search?q=hardware+security+key" target="_blank">hardware security key</a> prepared, press the Register button below.
totp: totp:
title: "Token-Based Authenticators" title: "Token-Based Authenticators"
add: "New Authenticator" add: "Add Authenticator"
default_name: "My Authenticator" default_name: "My Authenticator"
name_and_code_required_error: "You must provide a name and the code from your authenticator app." name_and_code_required_error: "You must provide a name and the code from your authenticator app."
security_key: security_key:
register: "Register" register: "Register"
title: "Security Keys" title: "Security Keys"
add: "Register Security Key" add: "Add Security Key"
default_name: "Main Security Key" default_name: "Main Security Key"
not_allowed_error: "The security key registration process either timed out or was cancelled." not_allowed_error: "The security key registration process either timed out or was cancelled."
already_added_error: "You have already registered this security key. You dont have to register it again." already_added_error: "You have already registered this security key. You dont have to register it again."
edit: "Edit Security Key" edit: "Edit Security Key"
save: "Save"
edit_description: "Security Key Name" edit_description: "Security Key Name"
delete: "Delete"
name_required_error: "You must provide a name for your security key." name_required_error: "You must provide a name for your security key."
change_about: change_about:

View File

@ -8,12 +8,14 @@
<span class="presence-text"> <span class="presence-text">
<span class="description"> <span class="description">
{{#if isReply ~}} {{#if isReply ~}}
{{i18n 'presence.replying'}} {{i18n "presence.replying"}}
{{~else~}} {{~else~}}
{{i18n 'presence.editing'}} {{i18n "presence.editing"}}
{{~/if}}</span>{{!-- (using comment to stop whitespace) {{~/if}}
--}}</span>{{!-- </span>{{!-- (using comment to stop whitespace)
--}}<span class="wave"><span class="dot">.</span><span class="dot">.</span><span class="dot">.</span> --}}</span>{{!--
--}}<span class="wave">
<span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>
</span> </span>
</div> </div>
{{/if}} {{/if}}

View File

@ -1,40 +1,40 @@
/*! /*!
* lazyYT (lazy load YouTube videos) * lazyYT (lazy load YouTube videos)
* v1.0.1 - 2014-12-30 * v1.0.1 - 2014-12-30
* (CC) This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. * (CC) This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
* http://creativecommons.org/licenses/by-sa/4.0/ * http://creativecommons.org/licenses/by-sa/4.0/
* Contributors: https://github.com/tylerpearson/lazyYT/graphs/contributors || https://github.com/daugilas/lazyYT/graphs/contributors * Contributors: https://github.com/tylerpearson/lazyYT/graphs/contributors || https://github.com/daugilas/lazyYT/graphs/contributors
* *
* Usage: <div class="lazyYT" data-youtube-id="laknj093n" data-parameters="rel=0">loading...</div> * Usage: <div class="lazyYT" data-youtube-id="laknj093n" data-parameters="rel=0">loading...</div>
* *
* Note: Discourse has forked this from the original, beware when updating the file. * Note: Discourse has forked this from the original, beware when updating the file.
* *
*/ */
(function ($) { (function($) {
'use strict'; "use strict";
function setUp($el, settings) { function setUp($el, settings) {
var width = $el.data('width'), var width = $el.data("width"),
height = $el.data('height'), height = $el.data("height"),
ratio = ($el.data('ratio')) ? $el.data('ratio') : settings.default_ratio, ratio = $el.data("ratio") ? $el.data("ratio") : settings.default_ratio,
id = $el.data('youtube-id'), id = $el.data("youtube-id"),
title = $el.data('youtube-title'), title = $el.data("youtube-title"),
padding_bottom, padding_bottom,
innerHtml = [], innerHtml = [],
$thumb, $thumb,
thumb_img, thumb_img,
youtube_parameters = $el.data('parameters') || ''; youtube_parameters = $el.data("parameters") || "";
ratio = ratio.split(":"); ratio = ratio.split(":");
// width and height might override default_ratio value // width and height might override default_ratio value
if (typeof width === 'number' && typeof height === 'number') { if (typeof width === "number" && typeof height === "number") {
$el.width(width); $el.width(width);
padding_bottom = height + 'px'; padding_bottom = height + "px";
} else if (typeof width === 'number') { } else if (typeof width === "number") {
$el.width(width); $el.width(width);
padding_bottom = (width * ratio[1] / ratio[0]) + 'px'; padding_bottom = (width * ratio[1]) / ratio[0] + "px";
} else { } else {
width = $el.width(); width = $el.width();
@ -43,7 +43,7 @@
width = $el.parent().width(); width = $el.parent().width();
} }
padding_bottom = (ratio[1] / ratio[0] * 100) + '%'; padding_bottom = (ratio[1] / ratio[0]) * 100 + "%";
} }
// //
@ -53,81 +53,108 @@
// Play button from YouTube (exactly as it is in YouTube) // Play button from YouTube (exactly as it is in YouTube)
innerHtml.push('<div class="ytp-large-play-button"'); innerHtml.push('<div class="ytp-large-play-button"');
if (width <= 640) innerHtml.push(' style="transform: scale(0.563888888888889);"'); if (width <= 640)
innerHtml.push('>'); innerHtml.push(' style="transform: scale(0.563888888888889);"');
innerHtml.push('<svg>'); innerHtml.push(">");
innerHtml.push('<path fill-rule="evenodd" clip-rule="evenodd" fill="#1F1F1F" class="ytp-large-play-button-svg" d="M84.15,26.4v6.35c0,2.833-0.15,5.967-0.45,9.4c-0.133,1.7-0.267,3.117-0.4,4.25l-0.15,0.95c-0.167,0.767-0.367,1.517-0.6,2.25c-0.667,2.367-1.533,4.083-2.6,5.15c-1.367,1.4-2.967,2.383-4.8,2.95c-0.633,0.2-1.316,0.333-2.05,0.4c-0.767,0.1-1.3,0.167-1.6,0.2c-4.9,0.367-11.283,0.617-19.15,0.75c-2.434,0.034-4.883,0.067-7.35,0.1h-2.95C38.417,59.117,34.5,59.067,30.3,59c-8.433-0.167-14.05-0.383-16.85-0.65c-0.067-0.033-0.667-0.117-1.8-0.25c-0.9-0.133-1.683-0.283-2.35-0.45c-2.066-0.533-3.783-1.5-5.15-2.9c-1.033-1.067-1.9-2.783-2.6-5.15C1.317,48.867,1.133,48.117,1,47.35L0.8,46.4c-0.133-1.133-0.267-2.55-0.4-4.25C0.133,38.717,0,35.583,0,32.75V26.4c0-2.833,0.133-5.95,0.4-9.35l0.4-4.25c0.167-0.966,0.417-2.05,0.75-3.25c0.7-2.333,1.567-4.033,2.6-5.1c1.367-1.434,2.967-2.434,4.8-3c0.633-0.167,1.333-0.3,2.1-0.4c0.4-0.066,0.917-0.133,1.55-0.2c4.9-0.333,11.283-0.567,19.15-0.7C35.65,0.05,39.083,0,42.05,0L45,0.05c2.467,0,4.933,0.034,7.4,0.1c7.833,0.133,14.2,0.367,19.1,0.7c0.3,0.033,0.833,0.1,1.6,0.2c0.733,0.1,1.417,0.233,2.05,0.4c1.833,0.566,3.434,1.566,4.8,3c1.066,1.066,1.933,2.767,2.6,5.1c0.367,1.2,0.617,2.284,0.75,3.25l0.4,4.25C84,20.45,84.15,23.567,84.15,26.4z M33.3,41.4L56,29.6L33.3,17.75V41.4z"></path>'); innerHtml.push("<svg>");
innerHtml.push('<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="33.3,41.4 33.3,17.75 56,29.6"></polygon>'); innerHtml.push(
innerHtml.push('</svg>'); '<path fill-rule="evenodd" clip-rule="evenodd" fill="#1F1F1F" class="ytp-large-play-button-svg" d="M84.15,26.4v6.35c0,2.833-0.15,5.967-0.45,9.4c-0.133,1.7-0.267,3.117-0.4,4.25l-0.15,0.95c-0.167,0.767-0.367,1.517-0.6,2.25c-0.667,2.367-1.533,4.083-2.6,5.15c-1.367,1.4-2.967,2.383-4.8,2.95c-0.633,0.2-1.316,0.333-2.05,0.4c-0.767,0.1-1.3,0.167-1.6,0.2c-4.9,0.367-11.283,0.617-19.15,0.75c-2.434,0.034-4.883,0.067-7.35,0.1h-2.95C38.417,59.117,34.5,59.067,30.3,59c-8.433-0.167-14.05-0.383-16.85-0.65c-0.067-0.033-0.667-0.117-1.8-0.25c-0.9-0.133-1.683-0.283-2.35-0.45c-2.066-0.533-3.783-1.5-5.15-2.9c-1.033-1.067-1.9-2.783-2.6-5.15C1.317,48.867,1.133,48.117,1,47.35L0.8,46.4c-0.133-1.133-0.267-2.55-0.4-4.25C0.133,38.717,0,35.583,0,32.75V26.4c0-2.833,0.133-5.95,0.4-9.35l0.4-4.25c0.167-0.966,0.417-2.05,0.75-3.25c0.7-2.333,1.567-4.033,2.6-5.1c1.367-1.434,2.967-2.434,4.8-3c0.633-0.167,1.333-0.3,2.1-0.4c0.4-0.066,0.917-0.133,1.55-0.2c4.9-0.333,11.283-0.567,19.15-0.7C35.65,0.05,39.083,0,42.05,0L45,0.05c2.467,0,4.933,0.034,7.4,0.1c7.833,0.133,14.2,0.367,19.1,0.7c0.3,0.033,0.833,0.1,1.6,0.2c0.733,0.1,1.417,0.233,2.05,0.4c1.833,0.566,3.434,1.566,4.8,3c1.066,1.066,1.933,2.767,2.6,5.1c0.367,1.2,0.617,2.284,0.75,3.25l0.4,4.25C84,20.45,84.15,23.567,84.15,26.4z M33.3,41.4L56,29.6L33.3,17.75V41.4z"></path>'
innerHtml.push('</div>'); // end of .ytp-large-play-button );
innerHtml.push(
'<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="33.3,41.4 33.3,17.75 56,29.6"></polygon>'
);
innerHtml.push("</svg>");
innerHtml.push("</div>"); // end of .ytp-large-play-button
innerHtml.push('</div>'); // end of .ytp-thumbnail innerHtml.push("</div>"); // end of .ytp-thumbnail
// Video title (info bar) // Video title (info bar)
innerHtml.push('<div class="html5-info-bar">'); innerHtml.push('<div class="html5-info-bar">');
innerHtml.push('<div class="html5-title">'); innerHtml.push('<div class="html5-title">');
innerHtml.push('<div class="html5-title-text-wrapper">'); innerHtml.push('<div class="html5-title-text-wrapper">');
innerHtml.push('<a class="html5-title-text" target="_blank" tabindex="3100" href="https://www.youtube.com/watch?v=', id, '">'); innerHtml.push(
if (title === undefined || title === null || title === '') { '<a class="html5-title-text" target="_blank" tabindex="3100" href="https://www.youtube.com/watch?v=',
innerHtml.push('youtube.com/watch?v=' + id); id,
'">'
);
if (title === undefined || title === null || title === "") {
innerHtml.push("youtube.com/watch?v=" + id);
} else { } else {
innerHtml.push(title); innerHtml.push(title);
} }
innerHtml.push('</a>'); innerHtml.push("</a>");
innerHtml.push('</div>'); // .html5-title innerHtml.push("</div>"); // .html5-title
innerHtml.push('</div>'); // .html5-title-text-wrapper innerHtml.push("</div>"); // .html5-title-text-wrapper
innerHtml.push('</div>'); // end of Video title .html5-info-bar innerHtml.push("</div>"); // end of Video title .html5-info-bar
$el.css({ $el
'padding-bottom': padding_bottom .css({
"padding-bottom": padding_bottom
}) })
.html(innerHtml.join('')); .html(innerHtml.join(""));
if (width > 640) { if (width > 640) {
thumb_img = 'maxresdefault.jpg'; thumb_img = "maxresdefault.jpg";
} else if (width > 480) { } else if (width > 480) {
thumb_img = 'sddefault.jpg'; thumb_img = "sddefault.jpg";
} else if (width > 320) { } else if (width > 320) {
thumb_img = 'hqdefault.jpg'; thumb_img = "hqdefault.jpg";
} else if (width > 120) { } else if (width > 120) {
thumb_img = 'mqdefault.jpg'; thumb_img = "mqdefault.jpg";
} else if (width === 0) { // sometimes it fails on fluid layout } else if (width === 0) {
thumb_img = 'hqdefault.jpg'; // sometimes it fails on fluid layout
thumb_img = "hqdefault.jpg";
} else { } else {
thumb_img = 'default.jpg'; thumb_img = "default.jpg";
} }
$thumb = $el.find('.ytp-thumbnail').css({ $thumb = $el
'background-image': ['url(https://img.youtube.com/vi/', id, '/', thumb_img, ')'].join('') .find(".ytp-thumbnail")
.css({
"background-image": [
"url(https://img.youtube.com/vi/",
id,
"/",
thumb_img,
")"
].join("")
}) })
.addClass('lazyYT-image-loaded') .addClass("lazyYT-image-loaded")
.on('click', function (e) { .on("click", function(e) {
e.preventDefault(); e.preventDefault();
if (!$el.hasClass('lazyYT-video-loaded') && $thumb.hasClass('lazyYT-image-loaded')) { if (
$el.html('<iframe src="//www.youtube.com/embed/' + id + '?autoplay=1&' + youtube_parameters + '" frameborder="0" allowfullscreen></iframe>') !$el.hasClass("lazyYT-video-loaded") &&
.addClass('lazyYT-video-loaded'); $thumb.hasClass("lazyYT-image-loaded")
) {
$el
.html(
'<iframe src="//www.youtube.com/embed/' +
id +
"?autoplay=1&" +
youtube_parameters +
'" frameborder="0" allowfullscreen></iframe>'
)
.addClass("lazyYT-video-loaded");
} }
if (settings.onPlay) { if (settings.onPlay) {
settings.onPlay(e, $el); settings.onPlay(e, $el);
} }
}); });
} }
$.fn.lazyYT = function (newSettings) { $.fn.lazyYT = function(newSettings) {
var defaultSettings = { var defaultSettings = {
default_ratio: '16:9', default_ratio: "16:9",
callback: null, // ToDO execute callback if given callback: null, // ToDO execute callback if given
container_class: 'lazyYT-container' container_class: "lazyYT-container"
}; };
var settings = $.extend(defaultSettings, newSettings); var settings = $.extend(defaultSettings, newSettings);
return this.each(function () { return this.each(function() {
var $el = $(this).addClass(settings.container_class); var $el = $(this).addClass(settings.container_class);
setUp($el, settings); setUp($el, settings);
}); });
}; };
})(jQuery); })(jQuery);

View File

@ -9,6 +9,8 @@ import {
setDefaultHomepage, setDefaultHomepage,
caretRowCol, caretRowCol,
setCaretPosition, setCaretPosition,
toAsciiPrintable,
slugify,
fillMissingDates, fillMissingDates,
inCodeBlock inCodeBlock
} from "discourse/lib/utilities"; } from "discourse/lib/utilities";
@ -175,6 +177,49 @@ QUnit.test("caretRowCol", assert => {
document.body.removeChild(textarea); document.body.removeChild(textarea);
}); });
QUnit.test("toAsciiPrintable", assert => {
const accentedString = "Créme_Brûlée!";
const unicodeString = "談話";
assert.equal(
toAsciiPrintable(accentedString, "discourse"),
"Creme_Brulee!",
"it replaces accented characters with the appropriate ASCII equivalent"
);
assert.equal(
toAsciiPrintable(unicodeString, "discourse"),
"discourse",
"it uses the fallback string when unable to convert"
);
assert.strictEqual(
typeof toAsciiPrintable(unicodeString),
"undefined",
"it returns undefined when unable to convert and no fallback is provided"
);
});
QUnit.test("slugify", assert => {
const asciiString = "--- 0__( Some-cool Discourse Site! )__0 --- ";
const accentedString = "Créme_Brûlée!";
const unicodeString = "談話";
assert.equal(
slugify(asciiString),
"0-some-cool-discourse-site-0",
"it properly slugifies an ASCII string"
);
assert.equal(
slugify(accentedString),
"crme-brle",
"it removes accented characters"
);
assert.equal(slugify(unicodeString), "", "it removes unicode characters");
});
QUnit.test("fillMissingDates", assert => { QUnit.test("fillMissingDates", assert => {
const startDate = "2017-11-12"; // YYYY-MM-DD const startDate = "2017-11-12"; // YYYY-MM-DD
const endDate = "2017-12-12"; // YYYY-MM-DD const endDate = "2017-12-12"; // YYYY-MM-DD