fix(platform): better detect platforms + css API

fixes #15165
fixes #15116
This commit is contained in:
Manu Mtz.-Almeida
2018-08-25 16:25:31 +02:00
parent 70a38ac594
commit 3ffa3cd7db
18 changed files with 167 additions and 300 deletions

View File

@@ -1,12 +1,11 @@
import { EventEmitter, Injectable } from '@angular/core';
import { PlatformConfig, detectPlatforms } from '@ionic/core';
import { Platforms, getPlatforms, isPlatform } from '@ionic/core';
import { proxyEvent } from '../util/util';
@Injectable()
export class Platform {
private _platforms = detectPlatforms(window);
private _readyPromise: Promise<string>;
/**
@@ -94,20 +93,8 @@ export class Platform {
* | electron | in Electron on a desktop device. |
*
*/
is(platformName: string): boolean {
return this._platforms.some(p => p.name === platformName);
}
/**
* @param win the window object
* @param platforms an array of platforms (platform configs)
* to get the appropriate platforms according to the configs provided.
* @description
* Detects the platforms using window and the platforms config provided.
* Populates the platforms array so they can be used later on for platform detection.
*/
detectPlatforms(platforms: PlatformConfig[]) {
return detectPlatforms(window, platforms);
is(platformName: Platforms): boolean {
return isPlatform(window, platformName);
}
/**
@@ -130,32 +117,9 @@ export class Platform {
* ```
*/
platforms(): string[] {
return this._platforms.map(platform => platform.name);
return getPlatforms(window);
}
/**
* Returns an object containing version information about all of the platforms.
*
* ```
* import { Platform } from 'ionic-angular';
*
* @Component({...})
* export MyPage {
* constructor(public platform: Platform) {
* // This will print an object containing
* // all of the platforms and their versions
* console.log(platform.versions());
* }
* }
* ```
*
* @returns An object containing all of the platforms and their versions.
*/
versions(): PlatformConfig[] {
return this._platforms.slice();
}
ready(): Promise<string> {
return this._readyPromise;
}
@@ -164,7 +128,6 @@ export class Platform {
return document.dir === 'rtl';
}
/**
* Get the query string parameter
*/

View File

@@ -9,7 +9,7 @@
}
.action-sheet-wrapper {
@include margin(var(--ion-safe-area-top), auto, var(--ion-safe-area-bottom), auto);
@include margin(var(--ion-safe-area-top, 0), auto, var(--ion-safe-area-bottom, 0), auto);
}

View File

@@ -1,27 +1,4 @@
ion-app.is-device {
html.plt-mobile ion-app {
user-select: none;
}
ion-app.statusbar-padding {
--ion-safe-area-top: 20px;
}
// TODO: remove once Safari 11.2 dies
@supports (padding-top: constant(safe-area-inset-top)) {
ion-app.statusbar-padding {
--ion-safe-area-top: constant(safe-area-inset-top);
--ion-safe-area-bottom: constant(safe-area-inset-bottom);
--ion-safe-area-left: constant(safe-area-inset-left);
--ion-safe-area-right: constant(safe-area-inset-right);
}
}
@supports (padding-top: env(safe-area-inset-top)) {
ion-app.statusbar-padding {
--ion-safe-area-top: env(safe-area-inset-top);
--ion-safe-area-bottom: env(safe-area-inset-bottom);
--ion-safe-area-left: env(safe-area-inset-left);
--ion-safe-area-right: env(safe-area-inset-right);
}
}

View File

@@ -1,7 +1,7 @@
import { Component, Element, Prop, QueueApi } from '@stencil/core';
import { Config } from '../../interface';
import { isDevice, isHybrid, isStandaloneMode, needInputShims } from '../../utils/platform';
import { isPlatform } from '../../utils/platform';
@Component({
tag: 'ion-app',
@@ -9,45 +9,31 @@ import { isDevice, isHybrid, isStandaloneMode, needInputShims } from '../../util
})
export class App {
private isDevice = false;
@Element() el!: HTMLElement;
@Prop({ context: 'window' }) win!: Window;
@Prop({ context: 'config' }) config!: Config;
@Prop({ context: 'queue' }) queue!: QueueApi;
componentWillLoad() {
this.isDevice = this.config.getBoolean('isDevice', isDevice(this.win));
}
componentDidLoad() {
setTimeout(() => {
importTapClick(this.win);
importInputShims(this.win, this.config);
importStatusTap(this.win, this.isDevice, this.queue);
importStatusTap(this.win, this.queue);
}, 32);
}
hostData() {
const hybrid = isHybrid(this.win);
const isStandalone = isStandaloneMode(this.win);
const statusbarPadding = this.config.getBoolean('statusbarPadding', hybrid || isStandalone);
return {
class: {
'ion-page': true,
'is-device': this.isDevice,
'is-hydrid': hybrid,
'is-standalone': isStandalone,
'statusbar-padding': statusbarPadding
}
};
}
}
async function importStatusTap(win: Window, device: boolean, queue: QueueApi) {
if (device) {
async function importStatusTap(win: Window, queue: QueueApi) {
if (isPlatform(win, 'hybrid')) {
(await import('../../utils/status-tap')).startStatusTap(win, queue);
}
}
@@ -62,3 +48,7 @@ async function importInputShims(win: Window, config: Config) {
(await import('../../utils/input-shims/input-shims')).startInputShims(win.document, config);
}
}
function needInputShims(win: Window) {
return isPlatform(win, 'ios') && isPlatform(win, 'mobile');
}

View File

@@ -1,6 +1,6 @@
import { Component, Element, Listen, Prop } from '@stencil/core';
import { isMatch } from '../../utils/media';
import { matchBreakpoint } from '../../utils/media';
const SUPPORTS_VARS = !!(CSS && CSS.supports && CSS.supports('--a', '0'));
const BREAKPOINTS = ['', 'xs', 'sm', 'md', 'lg', 'xl'];
@@ -11,7 +11,7 @@ const BREAKPOINTS = ['', 'xs', 'sm', 'md', 'lg', 'xl'];
shadow: true
})
export class Col {
[key: string]: any;
@Prop({ context: 'window' }) win!: Window;
@Element() el!: HTMLStencilElement;
@@ -166,11 +166,11 @@ export class Col {
let matched;
for (const breakpoint of BREAKPOINTS) {
const matches = isMatch(breakpoint);
const matches = matchBreakpoint(this.win, breakpoint);
// Grab the value of the property, if it exists and our
// media query matches we return the value
const columns = this[property + breakpoint.charAt(0).toUpperCase() + breakpoint.slice(1)];
const columns = (this as any)[property + breakpoint.charAt(0).toUpperCase() + breakpoint.slice(1)];
if (matches && columns !== undefined) {
matched = columns;

View File

@@ -15,5 +15,5 @@ ion-footer {
}
ion-footer ion-toolbar:last-child {
padding-bottom: var(--ion-safe-area-bottom);
padding-bottom: var(--ion-safe-area-bottom, 0);
}

View File

@@ -1,7 +1,7 @@
import { Component, Element, Listen, Prop, State } from '@stencil/core';
import { Config, Mode } from '../../interface';
import { DisplayWhen, PLATFORM_CONFIGS, PlatformConfig, detectPlatforms, updateTestResults } from '../../utils/show-hide-when-utils';
import { DisplayWhen, updateTestResults } from '../../utils/show-hide-when-utils';
@Component({
tag: 'ion-hide-when',
@@ -9,8 +9,6 @@ import { DisplayWhen, PLATFORM_CONFIGS, PlatformConfig, detectPlatforms, updateT
})
export class HideWhen implements DisplayWhen {
calculatedPlatforms!: PlatformConfig[];
@Element() element!: HTMLElement;
@Prop({ context: 'config' }) config!: Config;
@Prop({ context: 'window' }) win!: Window;
@@ -52,13 +50,12 @@ export class HideWhen implements DisplayWhen {
@State() passesTest = false;
componentWillLoad() {
this.calculatedPlatforms = detectPlatforms(this.win, PLATFORM_CONFIGS);
this.onResize();
}
@Listen('window:resize')
onResize() {
updateTestResults(this);
this.passesTest = updateTestResults(this);
}
hostData() {

View File

@@ -797,29 +797,21 @@ export class Nav implements NavOutlet {
...opts
};
const trns = await transition(animationOpts);
return this.transitionFinish(trns, enteringView, leavingView, opts);
const { hasCompleted } = await transition(animationOpts);
return this.transitionFinish(hasCompleted, enteringView, leavingView, opts);
}
private transitionFinish(
trans: Animation | null,
hasCompleted: boolean,
enteringView: ViewController,
leavingView: ViewController | undefined,
opts: NavOptions
): NavResult {
const hasCompleted = trans ? trans.hasCompleted : true;
const cleanupView = hasCompleted ? enteringView : leavingView;
if (cleanupView) {
this.cleanup(cleanupView);
}
// this is the root transition
// it's safe to destroy this transition
if (trans) {
trans.destroy();
}
return {
hasCompleted,
requiresTransition: true,

View File

@@ -56,7 +56,7 @@ ion-picker {
justify-content: center;
margin-bottom: var(--ion-safe-area-bottom);
margin-bottom: var(--ion-safe-area-bottom, 0);
contain: strict;
overflow: hidden;

View File

@@ -1,7 +1,7 @@
import { Component, Element, Listen, Prop, State } from '@stencil/core';
import { Config, Mode } from '../../interface';
import { DisplayWhen, PLATFORM_CONFIGS, PlatformConfig, detectPlatforms, updateTestResults } from '../../utils/show-hide-when-utils';
import { DisplayWhen, updateTestResults } from '../../utils/show-hide-when-utils';
@Component({
tag: 'ion-show-when',
@@ -12,7 +12,6 @@ export class ShowWhen implements DisplayWhen {
@Element() element?: HTMLElement;
@Prop({ context: 'config' }) config!: Config;
@Prop({ context: 'platforms' }) calculatedPlatforms!: PlatformConfig[];
@Prop({ context: 'window' }) win!: Window;
/**
@@ -52,13 +51,12 @@ export class ShowWhen implements DisplayWhen {
@State() passesTest = false;
componentWillLoad() {
this.calculatedPlatforms = detectPlatforms(this.win, PLATFORM_CONFIGS);
this.onResize();
}
@Listen('window:resize')
onResize() {
updateTestResults(this);
this.passesTest = updateTestResults(this);
}
hostData() {

View File

@@ -34,7 +34,7 @@
}
:host(.placement-bottom) {
padding-bottom: var(--ion-safe-area-bottom);
padding-bottom: var(--ion-safe-area-bottom, 0);
}

View File

@@ -25,11 +25,11 @@
}
.toast-md .toast-wrapper.toast-top {
padding-top: var(--ion-safe-area-top);
padding-top: var(--ion-safe-area-top, 0);
}
.toast-md .toast-wrapper.toast-bottom {
padding-bottom: var(--ion-safe-area-bottom);
padding-bottom: var(--ion-safe-area-bottom, 0);
}
.toast-md .toast-wrapper.toast-middle {

View File

@@ -90,3 +90,29 @@ ion-toast-controller,
.ion-page-invisible {
opacity: 0;
}
html.plt-ios.plt-hybrid, html.plt-ios.plt-pwa {
--ion-statusbar-padding: 20px;
}
html {
--ion-safe-area-top: var(--ion-statusbar-padding);
}
// TODO: remove once Safari 11.2 dies
@supports (padding-top: constant(safe-area-inset-top)) {
html {
--ion-safe-area-top: constant(safe-area-inset-top);
--ion-safe-area-bottom: constant(safe-area-inset-bottom);
--ion-safe-area-left: constant(safe-area-inset-left);
--ion-safe-area-right: constant(safe-area-inset-right);
}
}
@supports (padding-top: env(safe-area-inset-top)) {
html {
--ion-safe-area-top: env(safe-area-inset-top);
--ion-safe-area-bottom: env(safe-area-inset-bottom);
--ion-safe-area-left: env(safe-area-inset-left);
--ion-safe-area-right: env(safe-area-inset-right);
}
}

View File

@@ -8,8 +8,6 @@ export interface IonicConfig {
mode?: Mode;
persistConfig?: boolean;
isDevice?: boolean;
statusbarPadding?: boolean;
inputShims?: boolean;
backButtonIcon?: string;
backButtonText?: string;

View File

@@ -1,11 +1,12 @@
import 'ionicons';
import { configFromSession, configFromURL, saveConfig } from '../utils/config';
import { isIOS } from '../utils/platform';
import { isPlatform, setupPlatforms } from '../utils/platform';
import { Config } from './config';
const Ionic = (window as any)['Ionic'] = (window as any)['Ionic'] || {};
const win = window;
const Ionic = (win as any)['Ionic'] = (win as any)['Ionic'] || {};
declare const Context: any;
// queue used to coordinate DOM reads and
@@ -14,6 +15,10 @@ Object.defineProperty(Ionic, 'queue', {
get: () => Context['queue']
});
// Setup platforms
setupPlatforms(win);
Context.isPlatform = isPlatform;
// create the Ionic.config from raw config object (if it exists)
// and convert Ionic.config into a ConfigApi that has a get() fn
const configObj = {
@@ -31,7 +36,7 @@ if (config.getBoolean('persistConfig')) {
// which could have been set by the user, or by prerendering
// otherwise get the mode via config settings, and fallback to md
const documentElement = document.documentElement;
const mode = config.get('mode', documentElement.getAttribute('mode') || (isIOS(window) ? 'ios' : 'md'));
const mode = config.get('mode', documentElement.getAttribute('mode') || (isPlatform(win, 'ios') ? 'ios' : 'md'));
Ionic.mode = Context.mode = mode;
config.set('mode', mode);
documentElement.setAttribute('mode', mode);

View File

@@ -1,5 +1,3 @@
// Media Query Functions
// -----------------------------------------------------
export const SIZE_TO_MEDIA: any = {
'xs': '(min-width: 0px)',
@@ -11,15 +9,11 @@ export const SIZE_TO_MEDIA: any = {
// Check if the window matches the media query
// at the breakpoint passed
// e.g. isMatch('sm') => true if screen width exceeds 576px
export function isMatch(breakpoint: string | undefined) {
// e.g. matchBreakpoint('sm') => true if screen width exceeds 576px
export function matchBreakpoint(win: Window, breakpoint: string | undefined) {
if (!breakpoint) {
return true;
}
const mediaQuery = SIZE_TO_MEDIA[breakpoint];
if (mediaQuery && matchMedia(mediaQuery)) {
const media = matchMedia(mediaQuery);
return media.matches;
}
return false;
return win.matchMedia(mediaQuery).matches;
}

View File

@@ -1,21 +1,59 @@
export function isIpad(win: Window) {
export const PLATFORMS_MAP = {
'ipad': isIpad,
'iphone': isIphone,
'ios': isIOS,
'android': isAndroid,
'phablet': isPhablet,
'tablet': isTablet,
'cordova': isCordova,
'electron': isElectron,
'pwa': isPWA,
'mobile': isMobile,
'desktop': isDesktop,
'hybrid': isHybrid
};
export type Platforms = keyof typeof PLATFORMS_MAP;
export function getPlatforms(win: any) {
return win.Ionic.platforms;
}
export function isPlatform(win: Window, platform: Platforms) {
return getPlatforms(win).includes(platform);
}
export function setupPlatforms(win: any) {
let platforms: string[] = win.Ionic.platforms;
if (platforms == null) {
platforms = win.Ionic.platforms = detectPlatforms(win);
const classList = win.document.documentElement.classList;
platforms.forEach(p => classList.add(`plt-${p}`));
}
}
function detectPlatforms(win: Window): string[] {
return Object.keys(PLATFORMS_MAP).filter(p => (PLATFORMS_MAP as any)[p](win));
}
function isIpad(win: Window) {
return testUserAgent(win, /iPad/i);
}
export function isIphone(win: Window) {
function isIphone(win: Window) {
return testUserAgent(win, /iPhone/i);
}
export function isIOS(win: Window) {
function isIOS(win: Window) {
return testUserAgent(win, /iPad|iPhone|iPod/i);
}
export function isAndroid(win: Window) {
return !isIOS(win);
function isAndroid(win: Window) {
return testUserAgent(win, /android|sink/i);
}
export function isPhablet(win: Window) {
function isPhablet(win: Window) {
const width = win.innerWidth;
const height = win.innerHeight;
const smallest = Math.min(width, height);
@@ -25,7 +63,7 @@ export function isPhablet(win: Window) {
(largest > 620 && largest < 800);
}
export function isTablet(win: Window) {
function isTablet(win: Window) {
const width = win.innerWidth;
const height = win.innerHeight;
const smallest = Math.min(width, height);
@@ -34,88 +72,41 @@ export function isTablet(win: Window) {
(largest > 780 && largest < 1400);
}
export function isDevice(win: Window) {
function isMobile(win: Window) {
return matchMedia(win, '(any-pointer:coarse)');
}
export function isHybrid(win: Window) {
function isDesktop(win: Window) {
return !isMobile(win);
}
function isHybrid(win: Window) {
return isCordova(win) || isCapacitorNative(win);
}
export function isCordova(window: Window): boolean {
function isCordova(window: Window): boolean {
const win = window as any;
return !!(win['cordova'] || win['phonegap'] || win['PhoneGap']);
}
export function isCapacitorNative(window: Window): boolean {
function isCapacitorNative(window: Window): boolean {
const win = window as any;
const capacitor = win['Capacitor'];
return !!(capacitor && capacitor.isNative);
}
export function isElectron(win: Window): boolean {
function isElectron(win: Window): boolean {
return testUserAgent(win, /electron/);
}
export function isStandaloneMode(win: Window): boolean {
function isPWA(win: Window): boolean {
return win.matchMedia('(display-mode: standalone)').matches;
}
export function needInputShims(win: Window) {
return isIOS(win) && isDevice(win);
}
export function testUserAgent(win: Window, expr: RegExp) {
function testUserAgent(win: Window, expr: RegExp) {
return expr.test(win.navigator.userAgent);
}
export function matchMedia(win: Window, query: string, fallback = false): boolean {
return win.matchMedia
? win.matchMedia(query).matches
: fallback;
}
export interface PlatformConfig {
name: string;
isMatch: (win: Window) => boolean;
}
export const PLATFORM_CONFIGS: PlatformConfig[] = [
{
name: 'ipad',
isMatch: isIpad
},
{
name: 'iphone',
isMatch: isIphone
},
{
name: 'ios',
isMatch: isIOS
},
{
name: 'android',
isMatch: isAndroid
},
{
name: 'phablet',
isMatch: isPhablet
},
{
name: 'tablet',
isMatch: isTablet
},
{
name: 'cordova',
isMatch: isCordova
},
{
name: 'electron',
isMatch: isElectron
}
];
export function detectPlatforms(win: Window, platforms: PlatformConfig[] = PLATFORM_CONFIGS) {
// bracket notation to ensure they're not property renamed
return platforms.filter(p => p.isMatch(win));
function matchMedia(win: Window, query: string): boolean {
return win.matchMedia(query).matches;
}

View File

@@ -1,76 +1,69 @@
import { Config, Mode } from '../interface';
import { SIZE_TO_MEDIA } from './media';
import { isAndroid, isCordova, isElectron, isIOS, isIpad, isIphone, isPhablet, isTablet, matchMedia } from './platform';
import { matchBreakpoint } from './media';
import { isPlatform } from './platform';
export interface DisplayWhen {
config: Config;
win: Window;
mediaQuery?: string;
mode: Mode;
or: boolean;
orientation?: string;
platform?: string;
size?: string;
}
export function updateTestResults(displayWhen: DisplayWhen) {
displayWhen.passesTest = getTestResult(displayWhen);
return getTestResult(displayWhen);
}
export function isPlatformMatch(platforms: string[], multiPlatformString: string) {
const userProvidedPlatforms = multiPlatformString.replace(/\s/g, '').split(',');
for (const userProvidedPlatform of userProvidedPlatforms) {
for (const platform of platforms) {
if (userProvidedPlatform === platform) {
return true;
}
}
}
return false;
function isPlatformMatch(win: Window, multiPlatformString: string) {
const platforms = split(multiPlatformString);
return platforms.some(p => isPlatform(win, p as any));
}
export function isModeMatch(config: Config, multiModeString: string) {
// you can only ever be in one mode, so if an entry from the list matches, return true
const modes = multiModeString.replace(/\s/g, '').split(',');
function isModeMatch(config: Config, multiModeString: string) {
const modes = split(multiModeString);
const currentMode = config.get('mode');
return modes.indexOf(currentMode) >= 0;
return modes.includes(currentMode);
}
export function isSizeMatch(win: Window, multiSizeString: string) {
const sizes = multiSizeString.replace(/\s/g, '').split(',');
for (const size of sizes) {
const mediaQuery = SIZE_TO_MEDIA[size];
if (mediaQuery && matchMedia(win, mediaQuery)) {
return true;
}
}
return false;
function isSizeMatch(win: Window, multiSizeString: string) {
const sizes = split(multiSizeString);
return sizes.some(s => matchBreakpoint(win, s));
}
export function getTestResult(displayWhen: DisplayWhen) {
const resultsToConsider: boolean[] = [];
function split(multiOptions: string): string[] {
return multiOptions.replace(/\s/g, '').split(',');
}
function getTestResult(displayWhen: DisplayWhen) {
const results: boolean[] = [];
if (displayWhen.mediaQuery) {
resultsToConsider.push(matchMedia(displayWhen.win, displayWhen.mediaQuery));
results.push(matchMedia(displayWhen.win, displayWhen.mediaQuery));
}
if (displayWhen.size) {
resultsToConsider.push(isSizeMatch(displayWhen.win, displayWhen.size));
results.push(isSizeMatch(displayWhen.win, displayWhen.size));
}
if (displayWhen.mode) {
resultsToConsider.push(isModeMatch(displayWhen.config, displayWhen.mode));
results.push(isModeMatch(displayWhen.config, displayWhen.mode));
}
if (displayWhen.platform) {
const platformNames = displayWhen.calculatedPlatforms.map(platformConfig => platformConfig.name);
resultsToConsider.push(isPlatformMatch(platformNames, displayWhen.platform));
results.push(isPlatformMatch(displayWhen.win, displayWhen.platform));
}
if (displayWhen.orientation) {
resultsToConsider.push(isOrientationMatch(displayWhen.win, displayWhen.orientation));
results.push(isOrientationMatch(displayWhen.win, displayWhen.orientation));
}
if (!resultsToConsider.length) {
return true;
if (displayWhen.or) {
return results.some(r => r);
} else {
return results.every(r => r);
}
if (resultsToConsider.length === 1) {
return resultsToConsider[0];
}
return resultsToConsider.reduce((prev: boolean, current: boolean) => {
if (displayWhen.or) {
return prev || current;
}
return prev && current;
});
}
export function isOrientationMatch(win: Window, orientation: string) {
function isOrientationMatch(win: Window, orientation: string) {
if (orientation === 'portrait') {
return isPortrait(win);
} else if (orientation === 'landscape') {
@@ -80,67 +73,10 @@ export function isOrientationMatch(win: Window, orientation: string) {
return false;
}
export function isPortrait(win: Window): boolean {
function isPortrait(win: Window): boolean {
return matchMedia(win, '(orientation: portrait)');
}
// order from most specifc to least specific
export const PLATFORM_CONFIGS: PlatformConfig[] = [
{
name: 'ipad',
isMatch: isIpad
},
{
name: 'iphone',
isMatch: isIphone
},
{
name: 'ios',
isMatch: isIOS
},
{
name: 'android',
isMatch: isAndroid
},
{
name: 'phablet',
isMatch: isPhablet
},
{
name: 'tablet',
isMatch: isTablet
},
{
name: 'cordova',
isMatch: isCordova
},
{
name: 'electron',
isMatch: isElectron
}
];
export interface PlatformConfig {
name: string;
isMatch: (win: Window) => boolean;
}
export function detectPlatforms(win: Window, platforms: PlatformConfig[]) {
// bracket notation to ensure they're not property renamed
return platforms.filter(p => p.isMatch(win));
}
export interface DisplayWhen {
calculatedPlatforms: PlatformConfig[];
config: Config;
win: Window;
mediaQuery?: string;
mode: Mode;
or: boolean;
orientation?: string;
passesTest: boolean;
platform?: string;
size?: string;
function matchMedia(win: Window, query: string): boolean {
return win.matchMedia(query).matches;
}