414 lines
15 KiB
TypeScript
Raw Normal View History

/**
* @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 {setRootDomAdapter} from '../dom/dom_adapter';
2016-09-27 17:30:26 -07:00
import {global, isBlank, isPresent, setValueOnPath} from '../facade/lang';
import {GenericBrowserDomAdapter} from './generic_browser_adapter';
2016-09-27 17:30:26 -07:00
const _attrToPropMap = {
'class': 'className',
'innerHtml': 'innerHTML',
'readonly': 'readOnly',
2016-09-27 17:30:26 -07:00
'tabindex': 'tabIndex',
};
const DOM_KEY_LOCATION_NUMPAD = 3;
// Map to convert some key or keyIdentifier values to what will be returned by getEventKey
2016-09-27 17:30:26 -07:00
const _keyMap: {[k: string]: string} = {
// The following values are here for cross-browser compatibility and to match the W3C standard
// cf http://www.w3.org/TR/DOM-Level-3-Events-key/
'\b': 'Backspace',
'\t': 'Tab',
'\x7F': 'Delete',
'\x1B': 'Escape',
'Del': 'Delete',
'Esc': 'Escape',
'Left': 'ArrowLeft',
'Right': 'ArrowRight',
'Up': 'ArrowUp',
'Down': 'ArrowDown',
'Menu': 'ContextMenu',
'Scroll': 'ScrollLock',
'Win': 'OS'
};
// There is a bug in Chrome for numeric keypad keys:
// https://code.google.com/p/chromium/issues/detail?id=155654
// 1, 2, 3 ... are reported as A, B, C ...
2016-09-27 17:30:26 -07:00
const _chromeNumKeyPadMap = {
'A': '1',
'B': '2',
'C': '3',
'D': '4',
'E': '5',
'F': '6',
'G': '7',
'H': '8',
'I': '9',
'J': '*',
'K': '+',
'M': '-',
'N': '.',
'O': '/',
'\x60': '0',
'\x90': 'NumLock'
};
/**
* A `DomAdapter` powered by full browser DOM APIs.
*
* @security Tread carefully! Interacting with the DOM directly is dangerous and
* can introduce XSS risks.
*/
/* tslint:disable:requireParameterType */
export class BrowserDomAdapter extends GenericBrowserDomAdapter {
parse(templateHtml: string) { throw new Error('parse not implemented'); }
static makeCurrent() { setRootDomAdapter(new BrowserDomAdapter()); }
2016-09-27 17:30:26 -07:00
hasProperty(element: Node, name: string): boolean { return name in element; }
setProperty(el: Node, name: string, value: any) { (<any>el)[name] = value; }
getProperty(el: Node, name: string): any { return (<any>el)[name]; }
invoke(el: Node, methodName: string, args: any[]): any { (<any>el)[methodName](...args); }
2015-05-28 14:56:40 -07:00
// TODO(tbosch): move this into a separate environment class once we have it
logError(error: string): void {
if (window.console) {
(window.console.error || window.console.log)(error);
}
}
log(error: string): void {
if (window.console) {
// tslint:disable-next-line:no-console
window.console.log && window.console.log(error);
}
}
logGroup(error: string): void {
if (window.console) {
window.console.group && window.console.group(error);
this.logError(error);
}
}
logGroupEnd(): void {
if (window.console) {
window.console.groupEnd && window.console.groupEnd();
}
}
get attrToPropMap(): any { return _attrToPropMap; }
query(selector: string): any { return document.querySelector(selector); }
2016-09-27 17:30:26 -07:00
querySelector(el: Element, selector: string): HTMLElement {
return el.querySelector(selector) as HTMLElement;
}
2016-09-27 17:30:26 -07:00
querySelectorAll(el: any, selector: string): any[] { return el.querySelectorAll(selector); }
on(el: Node, evt: any, listener: any) { el.addEventListener(evt, listener, false); }
onAndCancel(el: Node, evt: any, listener: any): Function {
el.addEventListener(evt, listener, false);
// Needed to follow Dart's subscription semantic, until fix of
// https://code.google.com/p/dart/issues/detail?id=17406
return () => { el.removeEventListener(evt, listener, false); };
}
2016-09-27 17:30:26 -07:00
dispatchEvent(el: Node, evt: any) { el.dispatchEvent(evt); }
createMouseEvent(eventType: string): MouseEvent {
2016-09-27 17:30:26 -07:00
const evt: MouseEvent = document.createEvent('MouseEvent');
evt.initEvent(eventType, true, true);
return evt;
}
2016-09-27 17:30:26 -07:00
createEvent(eventType: any): Event {
const evt: Event = document.createEvent('Event');
evt.initEvent(eventType, true, true);
return evt;
}
preventDefault(evt: Event) {
evt.preventDefault();
evt.returnValue = false;
}
isPrevented(evt: Event): boolean {
return evt.defaultPrevented || isPresent(evt.returnValue) && !evt.returnValue;
}
2016-09-27 17:30:26 -07:00
getInnerHTML(el: HTMLElement): string { return el.innerHTML; }
getTemplateContent(el: Node): Node {
return 'content' in el && el instanceof HTMLTemplateElement ? el.content : null;
}
2016-09-27 17:30:26 -07:00
getOuterHTML(el: HTMLElement): string { return el.outerHTML; }
nodeName(node: Node): string { return node.nodeName; }
nodeValue(node: Node): string { return node.nodeValue; }
type(node: HTMLInputElement): string { return node.type; }
content(node: Node): Node {
if (this.hasProperty(node, 'content')) {
return (<any>node).content;
} else {
return node;
}
}
2016-09-27 17:30:26 -07:00
firstChild(el: Node): Node { return el.firstChild; }
nextSibling(el: Node): Node { return el.nextSibling; }
parentElement(el: Node): Node { return el.parentNode; }
childNodes(el: any): Node[] { return el.childNodes; }
childNodesAsList(el: Node): any[] {
const childNodes = el.childNodes;
const res = new Array(childNodes.length);
for (let i = 0; i < childNodes.length; i++) {
res[i] = childNodes[i];
}
return res;
}
2016-09-27 17:30:26 -07:00
clearNodes(el: Node) {
feat(compiler): attach components and project light dom during compilation. Closes #2529 BREAKING CHANGES: - shadow dom emulation no longer supports the `<content>` tag. Use the new `<ng-content>` instead (works with all shadow dom strategies). - removed `DomRenderer.setViewRootNodes` and `AppViewManager.getComponentView` -> use `DomRenderer.getNativeElementSync(elementRef)` and change shadow dom directly - the `Renderer` interface has changed: * `createView` now also has to support sub views * the notion of a container has been removed. Instead, the renderer has to implement methods to attach views next to elements or other views. * a RenderView now contains multiple RenderFragments. Fragments are used to move DOM nodes around. Internal changes / design changes: - Introduce notion of view fragments on render side - DomProtoViews and DomViews on render side are merged, AppProtoViews are not merged, AppViews are partially merged (they share arrays with the other merged AppViews but we keep individual AppView instances for now). - DomProtoViews always have a `<template>` element as root * needed for storing subviews * we have less chunks of DOM to clone now - remove fake ElementBinder / Bound element for root text bindings and model them explicitly. This removes a lot of special cases we had! - AppView shares data with nested component views - some methods in AppViewManager (create, hydrate, dehydrate) are iterative now * now possible as we have all child AppViews / ElementRefs already in an array!
2015-06-24 13:46:39 -07:00
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
2016-09-27 17:30:26 -07:00
appendChild(el: Node, node: Node) { el.appendChild(node); }
removeChild(el: Node, node: Node) { el.removeChild(node); }
replaceChild(el: Node, newChild: Node, oldChild: Node) { el.replaceChild(newChild, oldChild); }
remove(node: Node): Node {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
feat(compiler): attach components and project light dom during compilation. Closes #2529 BREAKING CHANGES: - shadow dom emulation no longer supports the `<content>` tag. Use the new `<ng-content>` instead (works with all shadow dom strategies). - removed `DomRenderer.setViewRootNodes` and `AppViewManager.getComponentView` -> use `DomRenderer.getNativeElementSync(elementRef)` and change shadow dom directly - the `Renderer` interface has changed: * `createView` now also has to support sub views * the notion of a container has been removed. Instead, the renderer has to implement methods to attach views next to elements or other views. * a RenderView now contains multiple RenderFragments. Fragments are used to move DOM nodes around. Internal changes / design changes: - Introduce notion of view fragments on render side - DomProtoViews and DomViews on render side are merged, AppProtoViews are not merged, AppViews are partially merged (they share arrays with the other merged AppViews but we keep individual AppView instances for now). - DomProtoViews always have a `<template>` element as root * needed for storing subviews * we have less chunks of DOM to clone now - remove fake ElementBinder / Bound element for root text bindings and model them explicitly. This removes a lot of special cases we had! - AppView shares data with nested component views - some methods in AppViewManager (create, hydrate, dehydrate) are iterative now * now possible as we have all child AppViews / ElementRefs already in an array!
2015-06-24 13:46:39 -07:00
return node;
}
2016-09-27 17:30:26 -07:00
insertBefore(el: Node, node: Node) { el.parentNode.insertBefore(node, el); }
insertAllBefore(el: Node, nodes: Node[]) {
nodes.forEach((n: any) => el.parentNode.insertBefore(n, el));
}
insertAfter(el: Node, node: any) { el.parentNode.insertBefore(node, el.nextSibling); }
setInnerHTML(el: Element, value: string) { el.innerHTML = value; }
getText(el: Node): string { return el.textContent; }
setText(el: Node, value: string) { el.textContent = value; }
getValue(el: any): string { return el.value; }
setValue(el: any, value: string) { el.value = value; }
getChecked(el: any): boolean { return el.checked; }
setChecked(el: any, value: boolean) { el.checked = value; }
createComment(text: string): Comment { return document.createComment(text); }
2016-09-27 17:30:26 -07:00
createTemplate(html: any): HTMLElement {
const t = document.createElement('template');
t.innerHTML = html;
return t;
}
2016-09-27 17:30:26 -07:00
createElement(tagName: string, doc = document): HTMLElement { return doc.createElement(tagName); }
createElementNS(ns: string, tagName: string, doc = document): Element {
return doc.createElementNS(ns, tagName);
}
createTextNode(text: string, doc = document): Text { return doc.createTextNode(text); }
createScriptTag(attrName: string, attrValue: string, doc = document): HTMLScriptElement {
2016-09-27 17:30:26 -07:00
const el = <HTMLScriptElement>doc.createElement('SCRIPT');
el.setAttribute(attrName, attrValue);
return el;
}
createStyleElement(css: string, doc = document): HTMLStyleElement {
2016-09-27 17:30:26 -07:00
const style = <HTMLStyleElement>doc.createElement('style');
this.appendChild(style, this.createTextNode(css));
return style;
}
createShadowRoot(el: HTMLElement): DocumentFragment { return (<any>el).createShadowRoot(); }
getShadowRoot(el: HTMLElement): DocumentFragment { return (<any>el).shadowRoot; }
getHost(el: HTMLElement): HTMLElement { return (<any>el).host; }
clone(node: Node): Node { return node.cloneNode(true); }
2016-09-27 17:30:26 -07:00
getElementsByClassName(element: any, name: string): HTMLElement[] {
return element.getElementsByClassName(name);
}
2016-09-27 17:30:26 -07:00
getElementsByTagName(element: any, name: string): HTMLElement[] {
return element.getElementsByTagName(name);
}
2016-09-27 17:30:26 -07:00
classList(element: any): any[] { return Array.prototype.slice.call(element.classList, 0); }
addClass(element: any, className: string) { element.classList.add(className); }
removeClass(element: any, className: string) { element.classList.remove(className); }
hasClass(element: any, className: string): boolean {
return element.classList.contains(className);
}
2016-09-27 17:30:26 -07:00
setStyle(element: any, styleName: string, styleValue: string) {
element.style[styleName] = styleValue;
}
removeStyle(element: any, stylename: string) {
// IE requires '' instead of null
// see https://github.com/angular/angular/issues/7916
element.style[stylename] = '';
}
2016-09-27 17:30:26 -07:00
getStyle(element: any, stylename: string): string { return element.style[stylename]; }
hasStyle(element: any, styleName: string, styleValue: string = null): boolean {
const value = this.getStyle(element, styleName) || '';
return styleValue ? value == styleValue : value.length > 0;
}
2016-09-27 17:30:26 -07:00
tagName(element: any): string { return element.tagName; }
attributeMap(element: any): Map<string, string> {
const res = new Map<string, string>();
const elAttrs = element.attributes;
for (let i = 0; i < elAttrs.length; i++) {
const attrib = elAttrs[i];
res.set(attrib.name, attrib.value);
}
return res;
}
2016-09-27 17:30:26 -07:00
hasAttribute(element: Element, attribute: string): boolean {
return element.hasAttribute(attribute);
}
2016-09-27 17:30:26 -07:00
hasAttributeNS(element: Element, ns: string, attribute: string): boolean {
return element.hasAttributeNS(ns, attribute);
}
2016-09-27 17:30:26 -07:00
getAttribute(element: Element, attribute: string): string {
return element.getAttribute(attribute);
}
2016-09-27 17:30:26 -07:00
getAttributeNS(element: Element, ns: string, name: string): string {
return element.getAttributeNS(ns, name);
}
2016-09-27 17:30:26 -07:00
setAttribute(element: Element, name: string, value: string) { element.setAttribute(name, value); }
setAttributeNS(element: Element, ns: string, name: string, value: string) {
element.setAttributeNS(ns, name, value);
}
2016-09-27 17:30:26 -07:00
removeAttribute(element: Element, attribute: string) { element.removeAttribute(attribute); }
removeAttributeNS(element: Element, ns: string, name: string) {
element.removeAttributeNS(ns, name);
}
2016-09-27 17:30:26 -07:00
templateAwareRoot(el: Node): any { return this.isTemplateElement(el) ? this.content(el) : el; }
createHtmlDocument(): HTMLDocument {
return document.implementation.createHTMLDocument('fakeTitle');
}
defaultDoc(): HTMLDocument { return document; }
2016-09-27 17:30:26 -07:00
getBoundingClientRect(el: Element): any {
try {
return el.getBoundingClientRect();
} catch (e) {
return {top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0};
}
}
getTitle(): string { return document.title; }
setTitle(newTitle: string) { document.title = newTitle || ''; }
2016-09-27 17:30:26 -07:00
elementMatches(n: any, selector: string): boolean {
if (n instanceof HTMLElement) {
2016-09-27 17:30:26 -07:00
return n.matches && n.matches(selector) ||
n.msMatchesSelector && n.msMatchesSelector(selector) ||
n.webkitMatchesSelector && n.webkitMatchesSelector(selector);
}
2016-09-27 17:30:26 -07:00
return false;
}
2016-09-27 17:30:26 -07:00
isTemplateElement(el: Node): boolean {
return el instanceof HTMLElement && el.nodeName == 'TEMPLATE';
}
isTextNode(node: Node): boolean { return node.nodeType === Node.TEXT_NODE; }
isCommentNode(node: Node): boolean { return node.nodeType === Node.COMMENT_NODE; }
isElementNode(node: Node): boolean { return node.nodeType === Node.ELEMENT_NODE; }
2016-09-27 17:30:26 -07:00
hasShadowRoot(node: any): boolean {
return isPresent(node.shadowRoot) && node instanceof HTMLElement;
}
2016-09-27 17:30:26 -07:00
isShadowRoot(node: any): boolean { return node instanceof DocumentFragment; }
importIntoDoc(node: Node): any { return document.importNode(this.templateAwareRoot(node), true); }
adoptNode(node: Node): any { return document.adoptNode(node); }
getHref(el: Element): string { return (<any>el).href; }
2016-09-27 17:30:26 -07:00
getEventKey(event: any): string {
let key = event.key;
if (isBlank(key)) {
key = event.keyIdentifier;
// keyIdentifier is defined in the old draft of DOM Level 3 Events implemented by Chrome and
2016-09-27 17:30:26 -07:00
// Safari cf
// http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/events.html#Events-KeyboardEvents-Interfaces
if (isBlank(key)) {
return 'Unidentified';
}
if (key.startsWith('U+')) {
key = String.fromCharCode(parseInt(key.substring(2), 16));
if (event.location === DOM_KEY_LOCATION_NUMPAD && _chromeNumKeyPadMap.hasOwnProperty(key)) {
// There is a bug in Chrome for numeric keypad keys:
// https://code.google.com/p/chromium/issues/detail?id=155654
// 1, 2, 3 ... are reported as A, B, C ...
2016-09-27 17:30:26 -07:00
key = (_chromeNumKeyPadMap as any)[key];
}
}
}
2016-09-27 17:30:26 -07:00
return _keyMap[key] || key;
}
getGlobalEventTarget(target: string): EventTarget {
2016-09-27 17:30:26 -07:00
if (target === 'window') {
return window;
2016-09-27 17:30:26 -07:00
}
if (target === 'document') {
return document;
2016-09-27 17:30:26 -07:00
}
if (target === 'body') {
return document.body;
}
}
getHistory(): History { return window.history; }
getLocation(): Location { return window.location; }
getBaseHref(): string {
2016-09-27 17:30:26 -07:00
const href = getBaseElementHref();
return isBlank(href) ? null : relativePath(href);
}
resetBaseElement(): void { baseElement = null; }
getUserAgent(): string { return window.navigator.userAgent; }
2016-09-27 17:30:26 -07:00
setData(element: Element, name: string, value: string) {
2015-08-20 10:12:46 +02:00
this.setAttribute(element, 'data-' + name, value);
}
2016-09-27 17:30:26 -07:00
getData(element: Element, name: string): string {
return this.getAttribute(element, 'data-' + name);
}
2016-09-27 17:30:26 -07:00
getComputedStyle(element: any): any { return getComputedStyle(element); }
2015-05-28 14:56:40 -07:00
// TODO(tbosch): move this into a separate environment class once we have it
setGlobalVar(path: string, value: any) { setValueOnPath(global, path, value); }
2016-09-27 17:30:26 -07:00
supportsWebAnimation(): boolean {
return typeof(<any>Element).prototype['animate'] === 'function';
}
performanceNow(): number {
// performance.now() is not available in all browsers, see
// http://caniuse.com/#search=performance.now
2016-09-27 17:30:26 -07:00
return window.performance && window.performance.now ? window.performance.now() :
new Date().getTime();
}
supportsCookies(): boolean { return true; }
getCookie(name: string): string { return parseCookieValue(document.cookie, name); }
setCookie(name: string, value: string) {
// document.cookie is magical, assigning into it assigns/overrides one cookie value, but does
// not clear other cookies.
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
}
}
2016-09-27 17:30:26 -07:00
let baseElement: HTMLElement = null;
function getBaseElementHref(): string {
2016-09-27 17:30:26 -07:00
if (!baseElement) {
baseElement = document.querySelector('base');
2016-09-27 17:30:26 -07:00
if (!baseElement) {
return null;
}
}
return baseElement.getAttribute('href');
}
// based on urlUtils.js in AngularJS 1
2016-09-27 17:30:26 -07:00
let urlParsingNode: any;
function relativePath(url: any): string {
if (!urlParsingNode) {
urlParsingNode = document.createElement('a');
}
urlParsingNode.setAttribute('href', url);
return (urlParsingNode.pathname.charAt(0) === '/') ? urlParsingNode.pathname :
'/' + urlParsingNode.pathname;
}
export function parseCookieValue(cookieStr: string, name: string): string {
name = encodeURIComponent(name);
for (const cookie of cookieStr.split(';')) {
const eqIndex = cookie.indexOf('=');
const [cookieName, cookieValue]: string[] =
eqIndex == -1 ? [cookie, ''] : [cookie.slice(0, eqIndex), cookie.slice(eqIndex + 1)];
if (cookieName.trim() === name) {
return decodeURIComponent(cookieValue);
}
}
return null;
}