From d65e00a9fa333e6faca39f564401f544e7873b69 Mon Sep 17 00:00:00 2001 From: Oussama Ben Brahim Date: Sun, 6 Aug 2017 14:24:17 +0200 Subject: [PATCH] feat(srcset): add srcset directive to inject elements to support responsive images Inject a element for every srcset. in the HTML markup of an element contained in a element. Closes #81. --- .../app/docs-layout-responsive/_module.ts | 5 +- .../responsivePicture.demo.ts | 49 ++++ src/lib/flexbox/api/base.ts | 14 +- src/lib/flexbox/api/flex.ts | 4 - src/lib/flexbox/api/img-srcset.spec.ts | 232 ++++++++++++++++++ src/lib/flexbox/api/img-srcset.ts | 119 +++++++++ src/lib/flexbox/api/layout-gap.ts | 2 +- src/lib/flexbox/api/layout-wrap.ts | 2 +- .../responsive/responsive-activation.ts | 14 +- src/lib/module.ts | 4 +- src/lib/utils/testing/custom-matchers.ts | 28 +++ src/lib/utils/testing/dom-tools.ts | 24 +- 12 files changed, 484 insertions(+), 13 deletions(-) create mode 100644 src/demo-app/app/docs-layout-responsive/responsivePicture.demo.ts create mode 100644 src/lib/flexbox/api/img-srcset.spec.ts create mode 100644 src/lib/flexbox/api/img-srcset.ts diff --git a/src/demo-app/app/docs-layout-responsive/_module.ts b/src/demo-app/app/docs-layout-responsive/_module.ts index 57c382041..9e125bb17 100644 --- a/src/demo-app/app/docs-layout-responsive/_module.ts +++ b/src/demo-app/app/docs-layout-responsive/_module.ts @@ -9,6 +9,7 @@ import { Component } from '@angular/core'; + ` }) export class DemosResponsiveLayout { } @@ -23,6 +24,7 @@ import {DemoResponsiveShowHide} from './responsiveShowHide.demo'; import {DemoResponsiveFlexDirectives} from './responsiveFlexDirective.demo'; import {DemoResponsiveFlexOrder} from './responsiveFlexOrder.demo'; import {DemoResponsiveStyle} from './responsiveStyle.demo'; +import {DemoResponsivePicture} from './responsivePicture.demo'; @NgModule({ declarations : [ @@ -33,7 +35,8 @@ import {DemoResponsiveStyle} from './responsiveStyle.demo'; DemoResponsiveFlexDirectives, DemoResponsiveFlexOrder, DemoResponsiveShowHide, - DemoResponsiveStyle + DemoResponsiveStyle, + DemoResponsivePicture ], imports : [ SharedModule, diff --git a/src/demo-app/app/docs-layout-responsive/responsivePicture.demo.ts b/src/demo-app/app/docs-layout-responsive/responsivePicture.demo.ts new file mode 100644 index 000000000..0e94f98ae --- /dev/null +++ b/src/demo-app/app/docs-layout-responsive/responsivePicture.demo.ts @@ -0,0 +1,49 @@ +import {Component} from '@angular/core'; + +@Component({ + moduleId: module.id, + selector: 'demo-responsive-picture', + template: ` + + + Responsive Picture + + Use the srcset API on an <img> to inject <source> elements within a + <picture> container. + + + +
+
+ + + +
+
+
+ +
+            <picture>
+                <img    style="width:auto;"
+                        src="https://dummyimage.com/400x200/c7c224/000.png&text=default"
+                        srcset.md="https://dummyimage.com/500x200/76c720/fff.png&text=md"
+                        srcset.sm="https://dummyimage.com/400x200/b925c7/fff.png&text=sm"
+                        srcset.lt-sm="https://dummyimage.com/300x200/c7751e/fff.png&text=lt-sm"
+                        srcset.gt-md="https://dummyimage.com/700x200/258cc7/fff.png&text=gt-md" >
+            </picture>
+        
+
+ + + + +
+ ` +}) +export class DemoResponsivePicture { +} diff --git a/src/lib/flexbox/api/base.ts b/src/lib/flexbox/api/base.ts index efc1abd14..918b7d2f6 100644 --- a/src/lib/flexbox/api/base.ts +++ b/src/lib/flexbox/api/base.ts @@ -117,7 +117,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { * and optional restore it when the mediaQueries deactivate */ protected _getDisplayStyle(source?: HTMLElement): string { - let element: HTMLElement = source || this._elementRef.nativeElement; + let element: HTMLElement = source || this.nativeElement; let value = this._lookupStyle(element, 'display'); return value ? value.trim() : ((element.nodeType === 1) ? 'block' : 'inline-block'); @@ -174,7 +174,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { value?: string | number, nativeElement?: any) { let styles = {}; - let element = nativeElement || this._elementRef.nativeElement; + let element = nativeElement || this.nativeElement; if (typeof style === 'string') { styles[style] = value; @@ -234,7 +234,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { * Special accessor to query for all child 'element' nodes regardless of type, class, etc. */ protected get childrenNodes() { - const obj = this._elementRef.nativeElement.children; + const obj = this.nativeElement.children; const buffer = []; // iterate backwards ensuring that length is an UInt32 @@ -244,6 +244,14 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { return buffer; } + protected get nativeElement(): any { + return this._elementRef.nativeElement; + } + + protected get parentElement(): any { + return this.nativeElement.parentNode; + } + /** * Fast validator for presence of attribute on the host element */ diff --git a/src/lib/flexbox/api/flex.ts b/src/lib/flexbox/api/flex.ts index 721a92ac2..08ae557c9 100644 --- a/src/lib/flexbox/api/flex.ts +++ b/src/lib/flexbox/api/flex.ts @@ -253,8 +253,4 @@ export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges, return extendObject(css, {'box-sizing': 'border-box'}); } - - protected get parentElement(): any { - return this._elementRef.nativeElement.parentNode; - } } diff --git a/src/lib/flexbox/api/img-srcset.spec.ts b/src/lib/flexbox/api/img-srcset.spec.ts new file mode 100644 index 000000000..498dadcb9 --- /dev/null +++ b/src/lib/flexbox/api/img-srcset.spec.ts @@ -0,0 +1,232 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; + +import {DEFAULT_BREAKPOINTS_PROVIDER} from '../../media-query/breakpoints/break-points-provider'; +import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-registry'; +import {MockMatchMedia} from '../../media-query/mock/mock-match-media'; +import {MatchMedia} from '../../media-query/match-media'; +import {FlexLayoutModule} from '../../module'; + +import {customMatchers} from '../../utils/testing/custom-matchers'; +import {makeCreateTestComponent, queryFor} from '../../utils/testing/helpers'; +import {expect} from '../../utils/testing/custom-matchers'; +import {_dom as _} from '../../utils/testing/dom-tools'; + +const SRCSET_URLS_MAP = { + 'xs': [ + 'https://dummyimage.com/300x200/c7751e/fff.png', + 'https://dummyimage.com/300x200/c7751e/000.png' + ], + 'gt-xs': [ + 'https://dummyimage.com/400x250/c7c224/fff.png', + 'https://dummyimage.com/400x250/c7c224/000.png' + ], + 'md': [ + 'https://dummyimage.com/500x300/76c720/fff.png', + 'https://dummyimage.com/500x300/76c720/000.png' + ], + 'lt-lg': [ + 'https://dummyimage.com/600x350/25c794/fff.png', + 'https://dummyimage.com/600x350/25c794/000.png' + ], + 'lg': [ + 'https://dummyimage.com/700x400/258cc7/fff.png', + 'https://dummyimage.com/700x400/258cc7/000.png' + ], + 'lt-xl': [ + 'https://dummyimage.com/800x500/b925c7/ffffff.png', + 'https://dummyimage.com/800x500/b925c7/000.png' + ] +}; +const DEFAULT_SRC = 'https://dummyimage.com/300x300/c72538/ffffff.png'; + +describe('srcset directive', () => { + let fixture: ComponentFixture; + let matchMedia: MockMatchMedia; + let breakpoints: BreakPointRegistry; + + let componentWithTemplate = (template: string) => { + fixture = makeCreateTestComponent(() => TestSrcsetComponent)(template); + + inject([MatchMedia, BreakPointRegistry], + (_matchMedia: MockMatchMedia, _breakpoints: BreakPointRegistry) => { + matchMedia = _matchMedia; + breakpoints = _breakpoints; + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, FlexLayoutModule], + declarations: [TestSrcsetComponent], + providers: [ + BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER, + {provide: MatchMedia, useClass: MockMatchMedia} + ] + }); + }); + + it('should work when no srcset flex-layout directive is used', () => { + const template = ` + + + + `; + componentWithTemplate(template); + fixture.detectChanges(); + + const nodes = queryFor(fixture, 'source'); + const pictureElt = queryFor(fixture, 'picture')[0].nativeElement; + + expect(nodes.length).toBe(0); + expect(pictureElt.children.length).toEqual(1); + expect(_.tagName(_.lastElementChild(pictureElt))).toEqual('IMG'); + }); + + it('should keep img as the last child tag of after source tags injection', () => { + const template = ` +
+ + + +
+ `; + componentWithTemplate(template); + fixture.detectChanges(); + + const pictureElt = queryFor(fixture, 'picture')[0].nativeElement; + + expect(_.tagName(_.lastElementChild(pictureElt))).toEqual('IMG'); + }); + + it('should inject source elements from largest to smallest corresponding media queries', () => { + const template = ` + + + + `; + componentWithTemplate(template); + fixture.detectChanges(); + + const nodes = queryFor(fixture, 'source'); + + expect(nodes.length).toBe(3); + expect(nodes[0].nativeElement).toHaveAttributes({ + srcset: `${SRCSET_URLS_MAP['lg'][0]}`, + media: breakpoints.findByAlias('lg').mediaQuery + }); + expect(nodes[1].nativeElement).toHaveAttributes({ + srcset: `${SRCSET_URLS_MAP['md'][0]}`, + media: breakpoints.findByAlias('md').mediaQuery + }); + expect(nodes[2].nativeElement).toHaveAttributes({ + srcset: `${SRCSET_URLS_MAP['xs'][0]}`, + media: breakpoints.findByAlias('xs').mediaQuery + }); + }); + + it('should update source elements srcset values when srcset input properties change', () => { + const template = ` + + + + `; + componentWithTemplate(template); + fixture.detectChanges(); + + fixture.componentInstance.xsSrcSet = SRCSET_URLS_MAP['xs'][1]; + fixture.componentInstance.mdSrcSet = SRCSET_URLS_MAP['md'][1]; + fixture.componentInstance.lgSrcSet = SRCSET_URLS_MAP['lg'][1]; + fixture.detectChanges(); + + let nodes = queryFor(fixture, 'source'); + + expect(nodes.length).toBe(3); + expect(nodes[0].nativeElement).toHaveAttributes({ + srcset: `${SRCSET_URLS_MAP['lg'][1]}`, + media: breakpoints.findByAlias('lg').mediaQuery + }); + expect(nodes[1].nativeElement).toHaveAttributes({ + srcset: `${SRCSET_URLS_MAP['md'][1]}`, + media: breakpoints.findByAlias('md').mediaQuery + }); + expect(nodes[2].nativeElement).toHaveAttributes({ + srcset: `${SRCSET_URLS_MAP['xs'][1]}`, + media: breakpoints.findByAlias('xs').mediaQuery + }); + }); + + it('should work with overlapping breakpoints', () => { + const template = ` + + + + `; + componentWithTemplate(template); + fixture.detectChanges(); + + let nodes = queryFor(fixture, 'source'); + expect(nodes[0].nativeElement).toHaveAttributes({ + srcset: `${SRCSET_URLS_MAP['lt-xl'][0]}`, + media: breakpoints.findByAlias('lt-xl').mediaQuery + }); + expect(nodes[1].nativeElement).toHaveAttributes({ + srcset: `${SRCSET_URLS_MAP['lt-lg'][0]}`, + media: breakpoints.findByAlias('lt-lg').mediaQuery + }); + expect(nodes[2].nativeElement).toHaveAttributes({ + srcset: `${SRCSET_URLS_MAP['xs'][0]}`, + media: breakpoints.findByAlias('xs').mediaQuery + }); + }); +}); + +// ***************************************************************** +// Template Component +// ***************************************************************** + +@Component({ + selector: 'test-srcset-api', + template: '' +}) +export class TestSrcsetComponent implements OnInit { + xsSrcSet: string; + mdSrcSet: string; + lgSrcSet: string; + constructor() { + this.xsSrcSet = SRCSET_URLS_MAP['xs'][0]; + this.mdSrcSet = SRCSET_URLS_MAP['md'][0]; + this.lgSrcSet = SRCSET_URLS_MAP['lg'][0]; + } + + ngOnInit() { + } +} + + diff --git a/src/lib/flexbox/api/img-srcset.ts b/src/lib/flexbox/api/img-srcset.ts new file mode 100644 index 000000000..dd4d41bb5 --- /dev/null +++ b/src/lib/flexbox/api/img-srcset.ts @@ -0,0 +1,119 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + Input, + OnInit, + OnChanges, + ElementRef, + Renderer, + SimpleChanges +} from '@angular/core'; +import {BaseFxDirective} from './base'; +import {MediaMonitor} from '../../media-query/media-monitor'; +import {ɵgetDOM as getDom} from '@angular/platform-browser'; + +/** + * Directive that injects -in a container element- elements with media and + * srcset attributes. + * elemets are sorted according to the related media query : from largest to smallest + * + * For browsers not supporting the element, the Picturefill polyfill is still needed. + * + * @see https://html.spec.whatwg.org/multipage/embedded-content.html#the-picture-element + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture + * @see https://www.html5rocks.com/en/tutorials/responsive/picture-element/ + * @see https://caniuse.com/#search=picture + * @see http://scottjehl.github.io/picturefill/ + */ +@Directive({ + selector: ` + [srcset.xs], [srcset.sm], [srcset.md], [srcset.lg], [srcset.xl], + [srcset.lt-sm], [srcset.lt-md], [srcset.lt-lg], [srcset.lt-xl], + [srcset.gt-xs], [srcset.gt-sm], [srcset.gt-md], [srcset.gt-lg] +` +}) +export class ImgSrcsetDirective extends BaseFxDirective implements OnInit, OnChanges { + + /** Reference to injected source elements to be used when there is a need to update their + * attributes. */ + private _inputSourceEltMap: {[input: string]: any} = {}; + + /* tslint:disable */ + @Input('srcset.xs') set srcsetXs(val) {this._cacheInput('srcsetXs', val);} + @Input('srcset.sm') set srcsetSm(val) {this._cacheInput('srcsetSm', val);}; + @Input('srcset.md') set srcsetMd(val) {this._cacheInput('srcsetMd', val);}; + @Input('srcset.lg') set srcsetLg(val) {this._cacheInput('srcsetLg', val);}; + @Input('srcset.xl') set srcsetXl(val) {this._cacheInput('srcsetXl', val);}; + + @Input('srcset.lt-sm') set srcsetLtSm(val) {this._cacheInput('srcsetLtSm', val);}; + @Input('srcset.lt-md') set srcsetLtMd(val) {this._cacheInput('srcsetLtMd', val);}; + @Input('srcset.lt-lg') set srcsetLtLg(val) {this._cacheInput('srcsetLtLg', val);}; + @Input('srcset.lt-xl') set srcsetLtXl(val) {this._cacheInput('srcsetLtXl', val);}; + + @Input('srcset.gt-xs') set srcsetGtXs(val) {this._cacheInput('srcsetGtXs', val);}; + @Input('srcset.gt-sm') set srcsetGtSm(val) {this._cacheInput('srcsetGtSm', val);}; + @Input('srcset.gt-md') set srcsetGtMd(val) {this._cacheInput('srcsetGtMd', val);}; + @Input('srcset.gt-lg') set srcsetGtLg(val) {this._cacheInput('srcsetGtLg', val);}; + + /* tslint:enable */ + constructor(elRef: ElementRef, renderer: Renderer, monitor: MediaMonitor) { + super(monitor, elRef, renderer); + } + + /** + * Inject elements once based on the used input properties + */ + ngOnInit() { + // build ResponsiveActivation proxy. There is no need to subsribe to mediaQuery changes as it is + // up to the browser to use the relevant injected element + this._listenForMediaQueryChanges('srcset', '', () => {}); + this._injectSourceElements(); + } + + /** + * Update the srcset of the relevant injected elements with the new data-bound input + * properties. elements are injected once through ngOnInit + */ + ngOnChanges(changes: SimpleChanges) { + Object.keys(changes).forEach(key => { + if (!changes[key].firstChange && this._inputSourceEltMap[key]) { + this._renderer.setElementAttribute( + this._inputSourceEltMap[key], 'srcset', this._queryInput(key)); + } + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + // remove reference to dom elements to avoid memory leaks + this._inputSourceEltMap = null; + } + + /** + * Inject source elements based on their related media queries from largest to smallest. + * Keep the element as the last child of the element: this necessary as the + * browser process the children of and uses the first one with the acceptable media + * query. is defaulted to when no element matches (and providing in the same time) + * backward compatibility. + */ + protected _injectSourceElements() { + if (!this._mqActivation) { + return; + } + this._mqActivation.registryFromLargest.forEach(bpX => { + const sourceElt = this._renderer.createElement(this.parentElement, 'source'); + this._inputSourceEltMap[bpX.key] = sourceElt; + + getDom().insertBefore(this.parentElement, this.nativeElement, sourceElt); + this._renderer.setElementAttribute(sourceElt, 'media', bpX.mediaQuery); + this._renderer.setElementAttribute(sourceElt, 'srcset', this._queryInput(bpX.key)); + }); + } + +} diff --git a/src/lib/flexbox/api/layout-gap.ts b/src/lib/flexbox/api/layout-gap.ts index f437c2abd..ed1639676 100644 --- a/src/lib/flexbox/api/layout-gap.ts +++ b/src/lib/flexbox/api/layout-gap.ts @@ -128,7 +128,7 @@ export class LayoutGapDirective extends BaseFxDirective implements AfterContentI if (typeof MutationObserver !== 'undefined') { this._observer = new MutationObserver(onMutationCallback); - this._observer.observe(this._elementRef.nativeElement, {childList: true}); + this._observer.observe(this.nativeElement, {childList: true}); } } diff --git a/src/lib/flexbox/api/layout-wrap.ts b/src/lib/flexbox/api/layout-wrap.ts index 6f860e65d..3b631e7aa 100644 --- a/src/lib/flexbox/api/layout-wrap.ts +++ b/src/lib/flexbox/api/layout-wrap.ts @@ -140,7 +140,7 @@ export class LayoutWrapDirective extends BaseFxDirective implements OnInit, OnCh } protected get flowDirection(): string { - let computeFlowDirection = () => this._getFlowDirection(this._elementRef.nativeElement); + let computeFlowDirection = () => this._getFlowDirection(this.nativeElement); return this._layoutWatcher ? this._layout : computeFlowDirection(); } diff --git a/src/lib/flexbox/responsive/responsive-activation.ts b/src/lib/flexbox/responsive/responsive-activation.ts index ffcf791be..b65828aeb 100644 --- a/src/lib/flexbox/responsive/responsive-activation.ts +++ b/src/lib/flexbox/responsive/responsive-activation.ts @@ -43,6 +43,7 @@ export class KeyOptions { export class ResponsiveActivation { private _subscribers: SubscriptionList = []; private _activatedInputKey: string; + private _registryMap: BreakPointX[]; /** * Constructor @@ -50,9 +51,20 @@ export class ResponsiveActivation { constructor(private _options: KeyOptions, private _mediaMonitor: MediaMonitor, private _onMediaChanges: MediaQuerySubscriber) { + this._registryMap = this._buildRegistryMap(); this._subscribers = this._configureChangeObservers(); } + /** + * Get a readonly sorted list of the breakpoints corresponding to the directive properties + * defined in the HTML markup: the sorting is done from largest to smallest. The order is + * important when several media queries are 'registered' and from which, the browser uses the + * first matching media query. + */ + get registryFromLargest() { + return [...this._registryMap].reverse(); + } + /** * Accessor to the DI'ed directive property * Each directive instance has a reference to the MediaMonitor which is @@ -107,7 +119,7 @@ export class ResponsiveActivation { private _configureChangeObservers(): SubscriptionList { let subscriptions = []; - this._buildRegistryMap().forEach((bp: BreakPointX) => { + this._registryMap.forEach((bp: BreakPointX) => { if (this._keyInUse(bp.key)) { // Inject directive default property key name: to let onMediaChange() calls // know which property is being triggered... diff --git a/src/lib/module.ts b/src/lib/module.ts index 90333afea..26d7b10f0 100644 --- a/src/lib/module.ts +++ b/src/lib/module.ts @@ -30,6 +30,7 @@ import {LayoutWrapDirective} from './flexbox/api/layout-wrap'; import {LayoutGapDirective} from './flexbox/api/layout-gap'; import {ClassDirective} from './flexbox/api/class'; import {StyleDirective} from './flexbox/api/style'; +import {ImgSrcsetDirective} from './flexbox/api/img-srcset'; /** * Since the equivalent results are easily achieved with a css class attached to each @@ -51,7 +52,8 @@ const ALL_DIRECTIVES = [ FlexAlignDirective, ShowHideDirective, ClassDirective, - StyleDirective + StyleDirective, + ImgSrcsetDirective ]; /** diff --git a/src/lib/utils/testing/custom-matchers.ts b/src/lib/utils/testing/custom-matchers.ts index 37d7685f2..91237522e 100644 --- a/src/lib/utils/testing/custom-matchers.ts +++ b/src/lib/utils/testing/custom-matchers.ts @@ -41,6 +41,11 @@ export interface NgMatchers extends jasmine.Matchers { */ toHaveCssClass(expected: string): boolean; + /** + * Expect the element to have the given pairs of attribute name and attribute value + */ + toHaveAttributes(expected: {[k: string]: string}): boolean; + /** * Expect the element to have the given CSS styles. * @@ -138,6 +143,29 @@ export const customMatchers: jasmine.CustomMatcherFactories = { }; }, + toHaveAttributes: function() { + return { + compare: function (actual: any, map: {[k: string]: string}) { + let allPassed: boolean; + let attributeNames = Object.keys(map); + allPassed = attributeNames.length !== 0; + attributeNames.forEach(name => { + allPassed = allPassed && _.hasAttribute(actual, name) + && _.getAttribute(actual, name) === map[name]; + }); + return { + pass: allPassed, + get message() { + return ` + Expected ${actual.outerHTML} ${allPassed ? 'not ' : ''} attributes to contain + '${JSON.stringify(map)}' + `; + } + }; + } + }; + }, + toHaveCssStyle: function () { return { compare: function (actual: any, styles: {[k: string]: string}|string) { diff --git a/src/lib/utils/testing/dom-tools.ts b/src/lib/utils/testing/dom-tools.ts index 28268fc7e..01773090e 100644 --- a/src/lib/utils/testing/dom-tools.ts +++ b/src/lib/utils/testing/dom-tools.ts @@ -18,12 +18,15 @@ export const _dom = { childNodes, childNodesAsList, hasClass, + hasAttribute, + getAttribute, hasShadowRoot, isCommentNode, isElementNode, isPresent, isShadowRoot, - tagName + tagName, + lastElementChild }; // ****************************************************************************************** @@ -66,6 +69,14 @@ function hasClass(element: any, className: string): boolean { return element.classList.contains(className); } +function hasAttribute(element: any, attributeName: string): boolean { + return element.hasAttribute(attributeName); +} + +function getAttribute(element: any, attributeName: string): string { + return element.getAttribute(attributeName); +} + function childNodes(el: any): Node[] { return el.childNodes; } @@ -89,6 +100,17 @@ function isShadowRoot(node: any): boolean { function isPresent(obj: any): boolean { return obj != null; } + function tagName(element: any): string { return element.tagName; } + +// ****************************************************************************************** +// These functions are part of the DOM API +// and are to be used ONLY internally in custom-matchers.ts and Unit Tests +// ****************************************************************************************** + +function lastElementChild(element: any): Node|null { + return element.lastElementChild; +} +