From 9a5084412dd65aabdd753fb8146d9f7d19c692a8 Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Wed, 12 Apr 2017 16:08:02 -0700 Subject: [PATCH] feat(aio): force plunker to embedded-style on narrow (mobile) screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regular plunker is unusable on narrow screen Refactors LiveExampleComponent and adds tests. Refactor width detection to `DeviceService` because need to know width change in 2 places. Keep “disable” option add in earlier spikes because simple and potentially useful in future. --- aio/src/app/app.component.spec.ts | 1 - .../live-example/live-example.component.html | 2 +- .../live-example.component.spec.ts | 50 ++++++--- .../live-example/live-example.component.ts | 102 +++++++++++------- aio/src/app/shared/device.service.ts | 13 --- 5 files changed, 100 insertions(+), 68 deletions(-) diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index e6e3055f85..526ecd2e72 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -338,7 +338,6 @@ class TestDeviceService { readonly sideBySideWidth = 1032; // Default to "wide", desktop browser. displayWidth = new BehaviorSubject(this.sideBySideWidth + 1); - isMobile = false; } class TestGaService { diff --git a/aio/src/app/embedded/live-example/live-example.component.html b/aio/src/app/embedded/live-example/live-example.component.html index dafc538393..7ab9d25a8d 100644 --- a/aio/src/app/embedded/live-example/live-example.component.html +++ b/aio/src/app/embedded/live-example/live-example.component.html @@ -1,5 +1,5 @@ - {{title}} (not available on mobile devices) + {{title}} (not available on this device)
diff --git a/aio/src/app/embedded/live-example/live-example.component.spec.ts b/aio/src/app/embedded/live-example/live-example.component.spec.ts index e07fac9889..3896386dc2 100644 --- a/aio/src/app/embedded/live-example/live-example.component.spec.ts +++ b/aio/src/app/embedded/live-example/live-example.component.spec.ts @@ -3,6 +3,8 @@ import { By } from '@angular/platform-browser'; import { Component, DebugElement, ElementRef } from '@angular/core'; import { Location } from '@angular/common'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + import { DeviceService } from 'app/shared/device.service'; import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example.component'; @@ -18,11 +20,12 @@ describe('LiveExampleComponent', () => { //////// test helpers //////// class TestDeviceService { - isMobile = false; + displayWidth = new BehaviorSubject(1001); } class TestMobileDeviceService { - isMobile = true; + // 1000 is the trigger width for "narrow mobile device"" + displayWidth = new BehaviorSubject(999); } @Component({ @@ -35,6 +38,12 @@ describe('LiveExampleComponent', () => { path() { return testPath; } } + function getAnchors() { + return liveExampleDe.queryAll(By.css('a')).map(de => de.nativeElement as HTMLAnchorElement); + } + + function getHrefs() { return getAnchors().map(a => a.href); } + function setHostTemplate(template: string) { TestBed.overrideComponent(HostComponent, {set: {template}}); } @@ -74,11 +83,6 @@ describe('LiveExampleComponent', () => { }); describe('when not embedded', () => { - - function getAnchors() { - return liveExampleDe.queryAll(By.css('a')).map(de => de.nativeElement as HTMLAnchorElement); - } - function getHrefs() { return getAnchors().map(a => a.href); } function getLiveExampleAnchor() { return getAnchors()[0]; } function getDownloadAnchor() { return getAnchors()[1]; } @@ -154,6 +158,22 @@ describe('LiveExampleComponent', () => { }); })); + it('should be embedded style by default', async(() => { + setHostTemplate(''); + testComponent(() => { + const hrefs = getHrefs(); + expect(hrefs[0]).toContain(defaultTestPath + '/eplnkr.html'); + }); + })); + + it('should be flat style when flat-style requested', async(() => { + setHostTemplate(''); + testComponent(() => { + const hrefs = getHrefs(); + expect(hrefs[0]).toContain(defaultTestPath + '/plnkr.html'); + }); + })); + it('should not have a download link when `noDownload` atty present', async(() => { setHostTemplate(''); testComponent(() => { @@ -273,7 +293,7 @@ describe('LiveExampleComponent', () => { }); }); - describe('when mobile', () => { + describe('when narrow display (mobile)', () => { beforeEach(() => { TestBed.configureTestingModule({ @@ -281,17 +301,19 @@ describe('LiveExampleComponent', () => { }); }); - it('should say that live example is not available on mobile', async(() => { - testPath = '/tutorial/toh-pt1'; + it('should be embedded style when no style defined', async(() => { + setHostTemplate(''); testComponent(() => { - expect(liveExampleDe.nativeElement.innerText).toContain('not available on mobile'); + const hrefs = getHrefs(); + expect(hrefs[0]).toContain(defaultTestPath + '/eplnkr.html'); }); })); - it('should say that the embedded live example is not available on mobile', async(() => { - setHostTemplate(''); + it('should be embedded style even when flat-style requested', async(() => { + setHostTemplate(''); testComponent(() => { - expect(liveExampleDe.nativeElement.innerText).toContain('not available on mobile'); + const hrefs = getHrefs(); + expect(hrefs[0]).toContain(defaultTestPath + '/eplnkr.html'); }); })); }); diff --git a/aio/src/app/embedded/live-example/live-example.component.ts b/aio/src/app/embedded/live-example/live-example.component.ts index 3dfb5c700e..f4b1f80c59 100644 --- a/aio/src/app/embedded/live-example/live-example.component.ts +++ b/aio/src/app/embedded/live-example/live-example.component.ts @@ -1,7 +1,10 @@ /* tslint:disable component-selector */ -import { Component, ElementRef, Input, OnInit, AfterViewInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, Input, OnInit, OnDestroy, AfterViewInit, ViewChild } from '@angular/core'; import { Location } from '@angular/common'; +import { Subject } from 'rxjs/Subject'; +import 'rxjs/add/operator/takeUntil'; + import { DeviceService } from 'app/shared/device.service'; const defaultPlnkrImg = 'plunker/placeholder.png'; @@ -12,7 +15,7 @@ const zipBase = 'content/zips/'; /** * Angular.io Live Example Embedded Component * -* Renders a link to a live/host example of the doc page (except on mobile). +* Renders a link to a live/host example of the doc page. * * All attributes and the text content are optional * @@ -20,10 +23,10 @@ const zipBase = 'content/zips/'; * // text for live example link and tooltip * text // higher precedence way to specify text for live example link and tooltip @@ -65,62 +68,73 @@ const zipBase = 'content/zips/'; selector: 'live-example', templateUrl: 'live-example.component.html' }) -export class LiveExampleComponent implements OnInit { +export class LiveExampleComponent implements OnInit, OnDestroy { + // Will force to embedded-style when viewport width is narrow + // "narrow" value was picked based on phone dimensions from http://screensiz.es/phone + readonly narrowWidth = 1000; + + attrs: any; enableDownload = true; - mode: string; + exampleDir: string; + isEmbedded = false; + mode = 'disabled'; + onDestroy = new Subject(); plnkr: string; + plnkrName: string; plnkrImg: string; showEmbedded = false; title: string; zip: string; constructor( - deviceService: DeviceService, + private deviceService: DeviceService, private elementRef: ElementRef, location: Location ) { - const attrs = this.getAttrs(); - + const attrs = this.attrs = this.getAttrs(); let exampleDir = attrs.name; if (!exampleDir) { // take last segment, excluding hash fragment and query params exampleDir = location.path(false).match(/[^\/?\#]+(?=\/?(?:$|\#|\?))/)[0]; } - exampleDir = exampleDir.trim(); + this.exampleDir = exampleDir.trim(); + this.plnkrName = attrs.plnkr ? attrs.plnkr.trim() + '.' : ''; + this.zip = `${zipBase}${exampleDir}/${this.plnkrName}${exampleDir}.zip`; - let plnkrStyle = 'eplnkr'; - - const isEmbedded = boolFromAtty(attrs.embedded); - if (!isEmbedded) { - // Not embedded in doc page; determine if is embedded- or flat-style in another browser tab. - // External plunker is embedded style by default. - // Make flat style with `flat-style` or `embedded-style="false` - // Must support aliases - const flatStyle = getAttrValue(['flat-style', 'flatstyle', 'flatStyle']); - const isFlatStyle = boolFromAtty(flatStyle); - const embeddedStyle = getAttrValue(['embedded-style', 'embeddedstyle', 'embeddedStyle']); - const isEmbeddedStyle = boolFromAtty(embeddedStyle, !isFlatStyle); - - plnkrStyle = isEmbeddedStyle ? 'eplnkr' : 'plnkr'; - } - - this.mode = deviceService.isMobile ? 'mobile' : isEmbedded ? 'embedded' : 'default'; - - const plnkrName = attrs.plnkr ? attrs.plnkr.trim() + '.' : ''; - - this.plnkr = `${liveExampleBase}${exampleDir}/${plnkrName}${plnkrStyle}.html`; - this.zip = `${zipBase}${exampleDir}/${plnkrName}${exampleDir}.zip`; - - const noDownload = getAttrValue(['noDownload', 'nodownload']); // noDownload aliases + const noDownload = this.getAttrValue(['noDownload', 'nodownload']); // noDownload aliases this.enableDownload = !boolFromAtty(noDownload); this.plnkrImg = imageBase + (attrs.img || defaultPlnkrImg); + } - this.title = attrs.title || ''; + calcPlnkrLink(width: number) { - function getAttrValue(atty: string | string[]) { - return attrs[typeof atty === 'string' ? atty : atty.find(a => attrs[a] !== undefined)]; + const attrs = this.attrs; + const exampleDir = this.exampleDir; + + let plnkrStyle = 'eplnkr'; // embedded style by default + this.mode = 'default'; // display in another browser tab by default + + this.isEmbedded = boolFromAtty(attrs.embedded); + + if (this.isEmbedded) { + this.mode = 'embedded'; // display embedded in the doc + } else { + // Not embedded in doc page; determine if is embedded- or flat-style in another browser tab. + // Embedded style if on tiny screen (reg. plunker no good on narrow screen) + // If wide enough, choose style based on style attributes + if (width > this.narrowWidth) { + // Make flat style with `flat-style` or `embedded-style="false`; support atty aliases + const flatStyle = this.getAttrValue(['flat-style', 'flatstyle', 'flatStyle']); + const isFlatStyle = boolFromAtty(flatStyle); + + const embeddedStyle = this.getAttrValue(['embedded-style', 'embeddedstyle', 'embeddedStyle']); + const isEmbeddedStyle = boolFromAtty(embeddedStyle, !isFlatStyle); + plnkrStyle = isEmbeddedStyle ? 'eplnkr' : 'plnkr'; + } } + + this.plnkr = `${liveExampleBase}${exampleDir}/${this.plnkrName}${plnkrStyle}.html`; } getAttrs(): any { @@ -130,12 +144,22 @@ export class LiveExampleComponent implements OnInit { return attrMap; } + getAttrValue(atty: string | string[]) { + return this.attrs[typeof atty === 'string' ? atty : atty.find(a => this.attrs[a] !== undefined)]; + } + ngOnInit() { // The `liveExampleContent` property is set by the DocViewer when it builds this component. // It is the original innerHTML of the host element. // Angular will sanitize this title when displayed so should be plain text. const title = this.elementRef.nativeElement.liveExampleContent; - this.title = (title || this.title || 'live example').trim(); + this.title = (title || this.attrs.title || 'live example').trim(); + + this.deviceService.displayWidth.takeUntil(this.onDestroy).subscribe(width => this.calcPlnkrLink(width)); + } + + ngOnDestroy() { + this.onDestroy.next(); } toggleEmbedded () { this.showEmbedded = !this.showEmbedded; } diff --git a/aio/src/app/shared/device.service.ts b/aio/src/app/shared/device.service.ts index 99c860f7e3..cc3d9f64ea 100644 --- a/aio/src/app/shared/device.service.ts +++ b/aio/src/app/shared/device.service.ts @@ -8,11 +8,9 @@ export class DeviceService { // Show sidenav next to the main doc when display width on current device is greater than this. readonly sideBySideWidth = 1032; - isMobile = false; displayWidth = new ReplaySubject(1); constructor() { - this.isMobile = this.isMobileCheck(); if (window) { window.onresize = () => this.onResize(); @@ -23,17 +21,6 @@ export class DeviceService { } } - isMobileCheck() { - if (!window) { return false; } - let check = false; - - // http://stackoverflow.com/questions/11381673/detecting-a-mobile-browser - // tslint:disable-next-line:curly max-line-length whitespace - (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window['opera']); - - return check; - } - onResize(width?: number) { this.displayWidth.next(width == null ? window.innerWidth : width); }