Skip to content

Commit

Permalink
Merge branch 'master' into feat/screen-orientation
Browse files Browse the repository at this point in the history
  • Loading branch information
jfmcquade authored Oct 31, 2024
2 parents 10e15a2 + 6761ce1 commit 0b958d3
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 4 deletions.
1 change: 1 addition & 0 deletions cspell.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"tmpl",
"venv",
"viewbox",
"youtube",
]
# flagWords - list of words to be always considered incorrect
# This is useful for offensive words and common spelling errors.
Expand Down
3 changes: 2 additions & 1 deletion packages/data-models/flowTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ export namespace FlowTypes {
| "toggle_bar"
| "update_action_list"
| "video"
| "workshops_accordion";
| "workshops_accordion"
| "youtube";

export interface TemplateRow extends Row_with_translations {
type: TemplateRowType;
Expand Down
7 changes: 4 additions & 3 deletions src/app/shared/components/template/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { TemplateBaseComponent } from "./base";
import { TemplateDebuggerComponent } from "./debugger";
import { TemplateHTMLComponent } from "./html/html.component";
import { TemplatePopupComponent } from "./layout/popup/popup.component";

import { TmplAccordionComponent } from "./accordion/accordion.component";
import { TmplAdvancedDashedBoxComponent } from "./layout/advanced-dashed-box/advanced-dashed-box.component";
import { TmplAnimatedSlidesComponent } from "./animated-slides/animated-slides.component";
Expand Down Expand Up @@ -54,14 +53,15 @@ import { TmplTaskProgressBarComponent } from "./task-progress-bar/task-progress-
import { TmplTextAreaComponent } from "./text-area/text-area.component";
import { TmplTextBoxComponent } from "./text-box/text-box.component";
import { TmplTextComponent } from "./text/text.component";
import { TmplTextBubbleComponent } from "./text-bubble/text-bubble.component";
import { TmplTileComponent } from "./tile-component/tile-component.component";
import { TmplTitleComponent } from "./title";
import { TmplTimerComponent } from "./timer/timer.component";
import { TmplToggleBarComponent } from "./toggle-bar/toggle-bar";
import { TmplVideoComponent } from "./video";

import { WorkshopsComponent } from "./layout/workshops_accordion";
import { TmplTextBubbleComponent } from "./text-bubble/text-bubble.component";
import { YoutubeComponent } from "./youtube/youtube.component";

import { DEMO_COMPONENT_MAPPING } from "packages/components/demo";
import { PLH_COMPONENT_MAPPING } from "packages/components/plh";

Expand Down Expand Up @@ -189,6 +189,7 @@ const CORE_COMPONENT_MAPPING: Record<FlowTypes.TemplateRowType, Type<ITemplateRo
update_action_list: null as any,
video: TmplVideoComponent,
workshops_accordion: WorkshopsComponent,
youtube: YoutubeComponent,
};

export const TEMPLATE_COMPONENT_MAPPING = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@if (src()) {
<div class="youtube-container">
<!-- The `iframe` property `allowFullscreen` must be set as a static attribute,
see https://angular.dev/errors/NG0910 -->
@if (params().allowFullScreen) {
<iframe [src]="src()" frameborder="0" allowfullscreen></iframe>
} @else {
<iframe [src]="src()" frameborder="0"></iframe>
}
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.youtube-container {
width: 100%;
aspect-ratio: 16 / 9; // see https://caniuse.com/mdn-css_properties_aspect-ratio
iframe {
width: 100%;
height: 100%;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { IonicModule } from "@ionic/angular";

import { YoutubeComponent } from "./youtube.component";

describe("YoutubeComponent", () => {
let component: YoutubeComponent;
let fixture: ComponentFixture<YoutubeComponent>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [YoutubeComponent],
imports: [IonicModule.forRoot()],
}).compileComponents();

fixture = TestBed.createComponent(YoutubeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));

it("should create", () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Component, computed } from "@angular/core";
import { TemplateBaseComponent } from "../base";
import { DomSanitizer } from "@angular/platform-browser";
import { getBooleanParamFromTemplateRow } from "src/app/shared/utils";
import { TemplateTranslateService } from "../../services/template-translate.service";

interface ITemplateParams {
allow_fullscreen?: string;
}

interface IYoutubeParams {
/** TEMPLATE PARAMETER: allow_fullscreen. Default true */
allowFullScreen?: boolean;
}

/**
* The names of the Youtube-specific query params that will be added to the url
* For a full list and explanation, see https://developers.google.com/youtube/player_parameters
* */
const YOUTUBE_URL_QUERY_PARAMS: { [K in keyof YouTubeUrlQueryParamValues]: string } = {
videoId: "v",
color: "color",
showFullscreenButton: "fs",
interfaceLanguage: "hl",
showRelatedVideos: "rel",
};

/** Possible values of the supported query params */
interface YouTubeUrlQueryParamValues {
videoId: string;
color: "red" | "white";
showFullscreenButton: "0" | "1";
interfaceLanguage: string; // 2-letter ISO 639-1 code
showRelatedVideos: "0" | "1";
}

@Component({
selector: "youtube",
templateUrl: "./youtube.component.html",
styleUrls: ["./youtube.component.scss"],
})
export class YoutubeComponent extends TemplateBaseComponent {
constructor(
private domSanitizer: DomSanitizer,
private templateTranslateService: TemplateTranslateService
) {
super();
}

public params = computed(() => this.parseParams(this.parameterList()));

public src = computed(() => {
const url = this.parseValue(this.value());
if (url) {
const urlWithParams = this.setUrlParams(url, this.params());
return this.domSanitizer.bypassSecurityTrustResourceUrl(urlWithParams.toString());
}
return undefined;
});

private parseParams(parameterList: ITemplateParams): IYoutubeParams {
// NOTE - param parsing takes full row not just parameterList
// Still included as function arg to prompt re-evaluate if parameters change
return { allowFullScreen: getBooleanParamFromTemplateRow(this._row, "allow_fullscreen", true) };
}

/** Validate template value field and convert to URL object **/
private parseValue(value: any) {
// Expect valid url. Don't specify domain as could start youtube.com, youtu.be, m.youtube.com, etc.
// https://stackoverflow.com/a/70512384/5693245
if (value && typeof value === "string" && value.startsWith("https://")) {
const url = new URL(value);
// only support urls that include video id through parameter (e.g. not youtu.be/12345678901)
const videoId = url.searchParams.get(YOUTUBE_URL_QUERY_PARAMS.videoId);
if (videoId) {
url.searchParams.delete(YOUTUBE_URL_QUERY_PARAMS.videoId);
// rewrite host and pathname to use youtube embed version
url.host = "youtube.com";
url.pathname = `/embed/${videoId}`;
return url;
}
}
console.error("[Youtube] Invalid value:", value);
}

/**
* Update player parameters from authored
* See https://developers.google.com/youtube/player_parameters
* NOTE - these will be merged with any params passed with the url itself
*/
private setUrlParams(url: URL, params: IYoutubeParams) {
// Favour white over red for more theme compatibility
this.setYouTubeParam(url, "color", "white");
// hide the fullscreen button if allow_fullscreen is false
this.setYouTubeParam(url, "showFullscreenButton", params.allowFullScreen ? "1" : "0");
// Attempt to set the player's interface language to match the app language
const languageCode = this.templateTranslateService.app_language_code;
this.setYouTubeParam(url, "interfaceLanguage", languageCode);
// Disable related videos (at least those from external channels)
this.setYouTubeParam(url, "showRelatedVideos", "0");
return url;
}

private setYouTubeParam = <K extends keyof YouTubeUrlQueryParamValues>(
url: URL,
key: K,
value: YouTubeUrlQueryParamValues[K]
) => {
const paramName = YOUTUBE_URL_QUERY_PARAMS[key];
url.searchParams.set(paramName, value);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ export class TemplateTranslateService extends AsyncServiceBase {
return this.app_language$.value;
}

/**
* Extracts the two-letter language code from a given country language string.
* Two-letter ISO 639-1 Codes: https://www.loc.gov/standards/iso639-2/php/code_list.php
* @param {string} languageCode Country language code from in `xx_yy` format, where `yy` is the two-letter language code
* @returns {string} The extracted two-letter language code, e.g. `yy`, or the original country language code if the format is invalid
*/
get app_language_code() {
const parts = this.app_language.split("_");
if (parts.length === 2 && parts[1].length === 2) {
return parts[1];
}
// Return original language code if the format is invalid
return this.app_language;
}

/** Set the local storage variable that tracks the app language */
async setLanguage(code: string, updateDB = true) {
if (code) {
Expand Down

0 comments on commit 0b958d3

Please sign in to comment.