refactor(render): delete copies files so we add them via moves
This commit is contained in:
parent
09067ebdc5
commit
be5ccf6957
|
@ -1,138 +0,0 @@
|
|||
import {StringWrapper, RegExpWrapper, BaseException, isPresent, isBlank, isString, stringify} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {reflector} from 'angular2/src/reflection/reflection';
|
||||
|
||||
var DASH_CASE_REGEXP = RegExpWrapper.create('-([a-z])');
|
||||
var CAMEL_CASE_REGEXP = RegExpWrapper.create('([A-Z])');
|
||||
|
||||
export function dashCaseToCamelCase(input:string): string {
|
||||
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP, (m) => {
|
||||
return m[1].toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
export function camelCaseToDashCase(input:string): string {
|
||||
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP, (m) => {
|
||||
return '-' + m[1].toLowerCase();
|
||||
});
|
||||
}
|
||||
|
||||
const STYLE_SEPARATOR = '.';
|
||||
var propertySettersCache = StringMapWrapper.create();
|
||||
var innerHTMLSetterCache;
|
||||
|
||||
export function setterFactory(property: string): Function {
|
||||
var setterFn, styleParts, styleSuffix;
|
||||
if (StringWrapper.startsWith(property, ATTRIBUTE_PREFIX)) {
|
||||
setterFn = attributeSetterFactory(StringWrapper.substring(property, ATTRIBUTE_PREFIX.length));
|
||||
} else if (StringWrapper.startsWith(property, CLASS_PREFIX)) {
|
||||
setterFn = classSetterFactory(StringWrapper.substring(property, CLASS_PREFIX.length));
|
||||
} else if (StringWrapper.startsWith(property, STYLE_PREFIX)) {
|
||||
styleParts = property.split(STYLE_SEPARATOR);
|
||||
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
|
||||
setterFn = styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
|
||||
} else if (StringWrapper.equals(property, 'innerHtml')) {
|
||||
if (isBlank(innerHTMLSetterCache)) {
|
||||
innerHTMLSetterCache = (el, value) => DOM.setInnerHTML(el, value);
|
||||
}
|
||||
setterFn = innerHTMLSetterCache;
|
||||
} else {
|
||||
property = resolvePropertyName(property);
|
||||
setterFn = StringMapWrapper.get(propertySettersCache, property);
|
||||
if (isBlank(setterFn)) {
|
||||
var propertySetterFn = reflector.setter(property);
|
||||
setterFn = function(receiver, value) {
|
||||
if (DOM.hasProperty(receiver, property)) {
|
||||
return propertySetterFn(receiver, value);
|
||||
}
|
||||
}
|
||||
StringMapWrapper.set(propertySettersCache, property, setterFn);
|
||||
}
|
||||
}
|
||||
return setterFn;
|
||||
}
|
||||
|
||||
const ATTRIBUTE_PREFIX = 'attr.';
|
||||
var attributeSettersCache = StringMapWrapper.create();
|
||||
|
||||
function _isValidAttributeValue(attrName:string, value: any): boolean {
|
||||
if (attrName == "role") {
|
||||
return isString(value);
|
||||
} else {
|
||||
return isPresent(value);
|
||||
}
|
||||
}
|
||||
|
||||
function attributeSetterFactory(attrName:string): Function {
|
||||
var setterFn = StringMapWrapper.get(attributeSettersCache, attrName);
|
||||
var dashCasedAttributeName;
|
||||
|
||||
if (isBlank(setterFn)) {
|
||||
dashCasedAttributeName = camelCaseToDashCase(attrName);
|
||||
setterFn = function(element, value) {
|
||||
if (_isValidAttributeValue(dashCasedAttributeName, value)) {
|
||||
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
|
||||
} else {
|
||||
if (isPresent(value)) {
|
||||
throw new BaseException("Invalid " + dashCasedAttributeName +
|
||||
" attribute, only string values are allowed, got '" + stringify(value) + "'");
|
||||
}
|
||||
DOM.removeAttribute(element, dashCasedAttributeName);
|
||||
}
|
||||
};
|
||||
StringMapWrapper.set(attributeSettersCache, attrName, setterFn);
|
||||
}
|
||||
|
||||
return setterFn;
|
||||
}
|
||||
|
||||
const CLASS_PREFIX = 'class.';
|
||||
var classSettersCache = StringMapWrapper.create();
|
||||
|
||||
function classSetterFactory(className:string): Function {
|
||||
var setterFn = StringMapWrapper.get(classSettersCache, className);
|
||||
|
||||
if (isBlank(setterFn)) {
|
||||
setterFn = function(element, value) {
|
||||
if (value) {
|
||||
DOM.addClass(element, className);
|
||||
} else {
|
||||
DOM.removeClass(element, className);
|
||||
}
|
||||
};
|
||||
StringMapWrapper.set(classSettersCache, className, setterFn);
|
||||
}
|
||||
|
||||
return setterFn;
|
||||
}
|
||||
|
||||
const STYLE_PREFIX = 'style.';
|
||||
var styleSettersCache = StringMapWrapper.create();
|
||||
|
||||
function styleSetterFactory(styleName:string, styleSuffix:string): Function {
|
||||
var cacheKey = styleName + styleSuffix;
|
||||
var setterFn = StringMapWrapper.get(styleSettersCache, cacheKey);
|
||||
var dashCasedStyleName;
|
||||
|
||||
if (isBlank(setterFn)) {
|
||||
dashCasedStyleName = camelCaseToDashCase(styleName);
|
||||
setterFn = function(element, value) {
|
||||
var valAsStr;
|
||||
if (isPresent(value)) {
|
||||
valAsStr = stringify(value);
|
||||
DOM.setStyle(element, dashCasedStyleName, valAsStr + styleSuffix);
|
||||
} else {
|
||||
DOM.removeStyle(element, dashCasedStyleName);
|
||||
}
|
||||
};
|
||||
StringMapWrapper.set(styleSettersCache, cacheKey, setterFn);
|
||||
}
|
||||
|
||||
return setterFn;
|
||||
}
|
||||
|
||||
function resolvePropertyName(attrName:string): string {
|
||||
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, attrName);
|
||||
return isPresent(mappedPropName) ? mappedPropName : attrName;
|
||||
}
|
|
@ -1,352 +0,0 @@
|
|||
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||
import {isPresent, isBlank, RegExpWrapper, RegExpMatcherWrapper, StringWrapper, BaseException} from 'angular2/src/facade/lang';
|
||||
|
||||
const _EMPTY_ATTR_VALUE = '';
|
||||
|
||||
// TODO: Can't use `const` here as
|
||||
// in Dart this is not transpiled into `final` yet...
|
||||
var _SELECTOR_REGEXP =
|
||||
RegExpWrapper.create('(\\:not\\()|' + //":not("
|
||||
'([-\\w]+)|' + // "tag"
|
||||
'(?:\\.([-\\w]+))|' + // ".class"
|
||||
'(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])|' + // "[name]", "[name=value]" or "[name*=value]"
|
||||
'(?:\\))|' + // ")"
|
||||
'(\\s*,\\s*)'); // ","
|
||||
|
||||
/**
|
||||
* A css selector contains an element name,
|
||||
* css classes and attribute/value pairs with the purpose
|
||||
* of selecting subsets out of them.
|
||||
*/
|
||||
export class CssSelector {
|
||||
element:string;
|
||||
classNames:List;
|
||||
attrs:List;
|
||||
notSelector: CssSelector;
|
||||
static parse(selector:string): List<CssSelector> {
|
||||
var results = ListWrapper.create();
|
||||
var _addResult = (res, cssSel) => {
|
||||
if (isPresent(cssSel.notSelector) && isBlank(cssSel.element)
|
||||
&& ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) {
|
||||
cssSel.element = "*";
|
||||
}
|
||||
ListWrapper.push(res, cssSel);
|
||||
}
|
||||
var cssSelector = new CssSelector();
|
||||
var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector);
|
||||
var match;
|
||||
var current = cssSelector;
|
||||
while (isPresent(match = RegExpMatcherWrapper.next(matcher))) {
|
||||
if (isPresent(match[1])) {
|
||||
if (isPresent(cssSelector.notSelector)) {
|
||||
throw new BaseException('Nesting :not is not allowed in a selector');
|
||||
}
|
||||
current.notSelector = new CssSelector();
|
||||
current = current.notSelector;
|
||||
}
|
||||
if (isPresent(match[2])) {
|
||||
current.setElement(match[2]);
|
||||
}
|
||||
if (isPresent(match[3])) {
|
||||
current.addClassName(match[3]);
|
||||
}
|
||||
if (isPresent(match[4])) {
|
||||
current.addAttribute(match[4], match[5]);
|
||||
}
|
||||
if (isPresent(match[6])) {
|
||||
_addResult(results, cssSelector);
|
||||
cssSelector = current = new CssSelector();
|
||||
}
|
||||
}
|
||||
_addResult(results, cssSelector);
|
||||
return results;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.element = null;
|
||||
this.classNames = ListWrapper.create();
|
||||
this.attrs = ListWrapper.create();
|
||||
this.notSelector = null;
|
||||
}
|
||||
|
||||
setElement(element:string = null) {
|
||||
if (isPresent(element)) {
|
||||
element = element.toLowerCase();
|
||||
}
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
addAttribute(name:string, value:string = _EMPTY_ATTR_VALUE) {
|
||||
ListWrapper.push(this.attrs, name.toLowerCase());
|
||||
if (isPresent(value)) {
|
||||
value = value.toLowerCase();
|
||||
} else {
|
||||
value = _EMPTY_ATTR_VALUE;
|
||||
}
|
||||
ListWrapper.push(this.attrs, value);
|
||||
}
|
||||
|
||||
addClassName(name:string) {
|
||||
ListWrapper.push(this.classNames, name.toLowerCase());
|
||||
}
|
||||
|
||||
toString():string {
|
||||
var res = '';
|
||||
if (isPresent(this.element)) {
|
||||
res += this.element;
|
||||
}
|
||||
if (isPresent(this.classNames)) {
|
||||
for (var i=0; i<this.classNames.length; i++) {
|
||||
res += '.' + this.classNames[i];
|
||||
}
|
||||
}
|
||||
if (isPresent(this.attrs)) {
|
||||
for (var i=0; i<this.attrs.length;) {
|
||||
var attrName = this.attrs[i++];
|
||||
var attrValue = this.attrs[i++]
|
||||
res += '[' + attrName;
|
||||
if (attrValue.length > 0) {
|
||||
res += '=' + attrValue;
|
||||
}
|
||||
res += ']';
|
||||
}
|
||||
}
|
||||
if (isPresent(this.notSelector)) {
|
||||
res += ":not(" + this.notSelector.toString() + ")";
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a list of CssSelectors and allows to calculate which ones
|
||||
* are contained in a given CssSelector.
|
||||
*/
|
||||
export class SelectorMatcher {
|
||||
_elementMap:Map;
|
||||
_elementPartialMap:Map;
|
||||
_classMap:Map;
|
||||
_classPartialMap:Map;
|
||||
_attrValueMap:Map;
|
||||
_attrValuePartialMap:Map;
|
||||
_listContexts:List;
|
||||
constructor() {
|
||||
this._elementMap = MapWrapper.create();
|
||||
this._elementPartialMap = MapWrapper.create();
|
||||
|
||||
this._classMap = MapWrapper.create();
|
||||
this._classPartialMap = MapWrapper.create();
|
||||
|
||||
this._attrValueMap = MapWrapper.create();
|
||||
this._attrValuePartialMap = MapWrapper.create();
|
||||
|
||||
this._listContexts = ListWrapper.create();
|
||||
}
|
||||
|
||||
addSelectables(cssSelectors:List<CssSelector>, callbackCtxt) {
|
||||
var listContext = null;
|
||||
if (cssSelectors.length > 1) {
|
||||
listContext= new SelectorListContext(cssSelectors);
|
||||
ListWrapper.push(this._listContexts, listContext);
|
||||
}
|
||||
for (var i = 0; i < cssSelectors.length; i++) {
|
||||
this.addSelectable(cssSelectors[i], callbackCtxt, listContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an object that can be found later on by calling `match`.
|
||||
* @param cssSelector A css selector
|
||||
* @param callbackCtxt An opaque object that will be given to the callback of the `match` function
|
||||
*/
|
||||
addSelectable(cssSelector, callbackCtxt, listContext: SelectorListContext) {
|
||||
var matcher = this;
|
||||
var element = cssSelector.element;
|
||||
var classNames = cssSelector.classNames;
|
||||
var attrs = cssSelector.attrs;
|
||||
var selectable = new SelectorContext(cssSelector, callbackCtxt, listContext);
|
||||
|
||||
|
||||
if (isPresent(element)) {
|
||||
var isTerminal = attrs.length === 0 && classNames.length === 0;
|
||||
if (isTerminal) {
|
||||
this._addTerminal(matcher._elementMap, element, selectable);
|
||||
} else {
|
||||
matcher = this._addPartial(matcher._elementPartialMap, element);
|
||||
}
|
||||
}
|
||||
|
||||
if (isPresent(classNames)) {
|
||||
for (var index = 0; index<classNames.length; index++) {
|
||||
var isTerminal = attrs.length === 0 && index === classNames.length - 1;
|
||||
var className = classNames[index];
|
||||
if (isTerminal) {
|
||||
this._addTerminal(matcher._classMap, className, selectable);
|
||||
} else {
|
||||
matcher = this._addPartial(matcher._classPartialMap, className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isPresent(attrs)) {
|
||||
for (var index = 0; index<attrs.length; ) {
|
||||
var isTerminal = index === attrs.length - 2;
|
||||
var attrName = attrs[index++];
|
||||
var attrValue = attrs[index++];
|
||||
var map = isTerminal ? matcher._attrValueMap : matcher._attrValuePartialMap;
|
||||
var valuesMap = MapWrapper.get(map, attrName)
|
||||
if (isBlank(valuesMap)) {
|
||||
valuesMap = MapWrapper.create();
|
||||
MapWrapper.set(map, attrName, valuesMap);
|
||||
}
|
||||
if (isTerminal) {
|
||||
this._addTerminal(valuesMap, attrValue, selectable);
|
||||
} else {
|
||||
matcher = this._addPartial(valuesMap, attrValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_addTerminal(map:Map<string,string>, name:string, selectable) {
|
||||
var terminalList = MapWrapper.get(map, name)
|
||||
if (isBlank(terminalList)) {
|
||||
terminalList = ListWrapper.create();
|
||||
MapWrapper.set(map, name, terminalList);
|
||||
}
|
||||
ListWrapper.push(terminalList, selectable);
|
||||
}
|
||||
|
||||
_addPartial(map:Map<string,string>, name:string) {
|
||||
var matcher = MapWrapper.get(map, name)
|
||||
if (isBlank(matcher)) {
|
||||
matcher = new SelectorMatcher();
|
||||
MapWrapper.set(map, name, matcher);
|
||||
}
|
||||
return matcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the objects that have been added via `addSelectable`
|
||||
* whose css selector is contained in the given css selector.
|
||||
* @param cssSelector A css selector
|
||||
* @param matchedCallback This callback will be called with the object handed into `addSelectable`
|
||||
* @return boolean true if a match was found
|
||||
*/
|
||||
match(cssSelector:CssSelector, matchedCallback:Function):boolean {
|
||||
var result = false;
|
||||
var element = cssSelector.element;
|
||||
var classNames = cssSelector.classNames;
|
||||
var attrs = cssSelector.attrs;
|
||||
|
||||
for (var i = 0; i < this._listContexts.length; i++) {
|
||||
this._listContexts[i].alreadyMatched = false;
|
||||
}
|
||||
|
||||
result = this._matchTerminal(this._elementMap, element, cssSelector, matchedCallback) || result;
|
||||
result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) || result;
|
||||
|
||||
if (isPresent(classNames)) {
|
||||
for (var index = 0; index<classNames.length; index++) {
|
||||
var className = classNames[index];
|
||||
result = this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
|
||||
result = this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback) || result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPresent(attrs)) {
|
||||
for (var index = 0; index<attrs.length;) {
|
||||
var attrName = attrs[index++];
|
||||
var attrValue = attrs[index++];
|
||||
|
||||
var valuesMap = MapWrapper.get(this._attrValueMap, attrName);
|
||||
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
|
||||
result = this._matchTerminal(valuesMap, _EMPTY_ATTR_VALUE, cssSelector, matchedCallback) || result;
|
||||
}
|
||||
result = this._matchTerminal(valuesMap, attrValue, cssSelector, matchedCallback) || result;
|
||||
|
||||
valuesMap = MapWrapper.get(this._attrValuePartialMap, attrName)
|
||||
result = this._matchPartial(valuesMap, attrValue, cssSelector, matchedCallback) || result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_matchTerminal(map:Map<string,string> = null, name, cssSelector, matchedCallback):boolean {
|
||||
if (isBlank(map) || isBlank(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var selectables = MapWrapper.get(map, name);
|
||||
var starSelectables = MapWrapper.get(map, "*");
|
||||
if (isPresent(starSelectables)) {
|
||||
selectables = ListWrapper.concat(selectables, starSelectables);
|
||||
}
|
||||
if (isBlank(selectables)) {
|
||||
return false;
|
||||
}
|
||||
var selectable;
|
||||
var result = false;
|
||||
for (var index=0; index<selectables.length; index++) {
|
||||
selectable = selectables[index];
|
||||
result = selectable.finalize(cssSelector, matchedCallback) || result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_matchPartial(map:Map<string,string> = null, name, cssSelector, matchedCallback):boolean {
|
||||
if (isBlank(map) || isBlank(name)) {
|
||||
return false;
|
||||
}
|
||||
var nestedSelector = MapWrapper.get(map, name)
|
||||
if (isBlank(nestedSelector)) {
|
||||
return false;
|
||||
}
|
||||
// TODO(perf): get rid of recursion and measure again
|
||||
// TODO(perf): don't pass the whole selector into the recursion,
|
||||
// but only the not processed parts
|
||||
return nestedSelector.match(cssSelector, matchedCallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SelectorListContext {
|
||||
selectors: List<CssSelector>;
|
||||
alreadyMatched: boolean;
|
||||
|
||||
constructor(selectors:List<CssSelector>) {
|
||||
this.selectors = selectors;
|
||||
this.alreadyMatched = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Store context to pass back selector and context when a selector is matched
|
||||
class SelectorContext {
|
||||
selector:CssSelector;
|
||||
notSelector:CssSelector;
|
||||
cbContext; // callback context
|
||||
listContext: SelectorListContext;
|
||||
|
||||
constructor(selector:CssSelector, cbContext, listContext: SelectorListContext) {
|
||||
this.selector = selector;
|
||||
this.notSelector = selector.notSelector;
|
||||
this.cbContext = cbContext;
|
||||
this.listContext = listContext;
|
||||
}
|
||||
|
||||
finalize(cssSelector: CssSelector, callback) {
|
||||
var result = true;
|
||||
if (isPresent(this.notSelector) && (isBlank(this.listContext) || !this.listContext.alreadyMatched)) {
|
||||
var notMatcher = new SelectorMatcher();
|
||||
notMatcher.addSelectable(this.notSelector, null, null);
|
||||
result = !notMatcher.match(cssSelector, null);
|
||||
}
|
||||
if (result && isPresent(callback) && (isBlank(this.listContext) || !this.listContext.alreadyMatched)) {
|
||||
if (isPresent(this.listContext)) {
|
||||
this.listContext.alreadyMatched = true;
|
||||
}
|
||||
callback(this.selector, this.cbContext);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -1,537 +0,0 @@
|
|||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {List, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {
|
||||
StringWrapper,
|
||||
RegExp,
|
||||
RegExpWrapper,
|
||||
RegExpMatcherWrapper,
|
||||
isPresent,
|
||||
isBlank,
|
||||
BaseException,
|
||||
int
|
||||
} from 'angular2/src/facade/lang';
|
||||
|
||||
/**
|
||||
* This file is a port of shadowCSS from webcomponents.js to AtScript.
|
||||
*
|
||||
* Please make sure to keep to edits in sync with the source file.
|
||||
*
|
||||
* Source: https://github.com/webcomponents/webcomponentsjs/blob/4efecd7e0e/src/ShadowCSS/ShadowCSS.js
|
||||
*
|
||||
* The original file level comment is reproduced below
|
||||
*/
|
||||
|
||||
/*
|
||||
This is a limited shim for ShadowDOM css styling.
|
||||
https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#styles
|
||||
|
||||
The intention here is to support only the styling features which can be
|
||||
relatively simply implemented. The goal is to allow users to avoid the
|
||||
most obvious pitfalls and do so without compromising performance significantly.
|
||||
For ShadowDOM styling that's not covered here, a set of best practices
|
||||
can be provided that should allow users to accomplish more complex styling.
|
||||
|
||||
The following is a list of specific ShadowDOM styling features and a brief
|
||||
discussion of the approach used to shim.
|
||||
|
||||
Shimmed features:
|
||||
|
||||
* :host, :host-context: ShadowDOM allows styling of the shadowRoot's host
|
||||
element using the :host rule. To shim this feature, the :host styles are
|
||||
reformatted and prefixed with a given scope name and promoted to a
|
||||
document level stylesheet.
|
||||
For example, given a scope name of .foo, a rule like this:
|
||||
|
||||
:host {
|
||||
background: red;
|
||||
}
|
||||
}
|
||||
|
||||
becomes:
|
||||
|
||||
.foo {
|
||||
background: red;
|
||||
}
|
||||
|
||||
* encapsultion: Styles defined within ShadowDOM, apply only to
|
||||
dom inside the ShadowDOM. Polymer uses one of two techniques to imlement
|
||||
this feature.
|
||||
|
||||
By default, rules are prefixed with the host element tag name
|
||||
as a descendant selector. This ensures styling does not leak out of the 'top'
|
||||
of the element's ShadowDOM. For example,
|
||||
|
||||
div {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
becomes:
|
||||
|
||||
x-foo div {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
becomes:
|
||||
|
||||
|
||||
Alternatively, if WebComponents.ShadowCSS.strictStyling is set to true then
|
||||
selectors are scoped by adding an attribute selector suffix to each
|
||||
simple selector that contains the host element tag name. Each element
|
||||
in the element's ShadowDOM template is also given the scope attribute.
|
||||
Thus, these rules match only elements that have the scope attribute.
|
||||
For example, given a scope name of x-foo, a rule like this:
|
||||
|
||||
div {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
becomes:
|
||||
|
||||
div[x-foo] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
Note that elements that are dynamically added to a scope must have the scope
|
||||
selector added to them manually.
|
||||
|
||||
* upper/lower bound encapsulation: Styles which are defined outside a
|
||||
shadowRoot should not cross the ShadowDOM boundary and should not apply
|
||||
inside a shadowRoot.
|
||||
|
||||
This styling behavior is not emulated. Some possible ways to do this that
|
||||
were rejected due to complexity and/or performance concerns include: (1) reset
|
||||
every possible property for every possible selector for a given scope name;
|
||||
(2) re-implement css in javascript.
|
||||
|
||||
As an alternative, users should make sure to use selectors
|
||||
specific to the scope in which they are working.
|
||||
|
||||
* ::distributed: This behavior is not emulated. It's often not necessary
|
||||
to style the contents of a specific insertion point and instead, descendants
|
||||
of the host element can be styled selectively. Users can also create an
|
||||
extra node around an insertion point and style that node's contents
|
||||
via descendent selectors. For example, with a shadowRoot like this:
|
||||
|
||||
<style>
|
||||
::content(div) {
|
||||
background: red;
|
||||
}
|
||||
</style>
|
||||
<content></content>
|
||||
|
||||
could become:
|
||||
|
||||
<style>
|
||||
/ *@polyfill .content-container div * /
|
||||
::content(div) {
|
||||
background: red;
|
||||
}
|
||||
</style>
|
||||
<div class="content-container">
|
||||
<content></content>
|
||||
</div>
|
||||
|
||||
Note the use of @polyfill in the comment above a ShadowDOM specific style
|
||||
declaration. This is a directive to the styling shim to use the selector
|
||||
in comments in lieu of the next selector when running under polyfill.
|
||||
*/
|
||||
|
||||
export class ShadowCss {
|
||||
strictStyling: boolean;
|
||||
|
||||
constructor() {
|
||||
this.strictStyling = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Shim a style element with the given selector. Returns cssText that can
|
||||
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
|
||||
*/
|
||||
shimStyle(style, selector: string, hostSelector: string = ''): string {
|
||||
var cssText = DOM.getText(style);
|
||||
return this.shimCssText(cssText, selector, hostSelector);
|
||||
}
|
||||
|
||||
/*
|
||||
* Shim some cssText with the given selector. Returns cssText that can
|
||||
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
|
||||
*
|
||||
* When strictStyling is true:
|
||||
* - selector is the attribute added to all elements inside the host,
|
||||
* - hostSelector is the attribute added to the host itself.
|
||||
*/
|
||||
shimCssText(cssText: string, selector: string, hostSelector: string = ''): string {
|
||||
cssText = this._insertDirectives(cssText);
|
||||
return this._scopeCssText(cssText, selector, hostSelector);
|
||||
}
|
||||
|
||||
_insertDirectives(cssText: string): string {
|
||||
cssText = this._insertPolyfillDirectivesInCssText(cssText);
|
||||
return this._insertPolyfillRulesInCssText(cssText);
|
||||
}
|
||||
|
||||
/*
|
||||
* Process styles to convert native ShadowDOM rules that will trip
|
||||
* up the css parser; we rely on decorating the stylesheet with inert rules.
|
||||
*
|
||||
* For example, we convert this rule:
|
||||
*
|
||||
* polyfill-next-selector { content: ':host menu-item'; }
|
||||
* ::content menu-item {
|
||||
*
|
||||
* to this:
|
||||
*
|
||||
* scopeName menu-item {
|
||||
*
|
||||
**/
|
||||
_insertPolyfillDirectivesInCssText(cssText: string): string {
|
||||
// Difference with webcomponents.js: does not handle comments
|
||||
return StringWrapper.replaceAllMapped(cssText, _cssContentNextSelectorRe, function(m) {
|
||||
return m[1] + '{';
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Process styles to add rules which will only apply under the polyfill
|
||||
*
|
||||
* For example, we convert this rule:
|
||||
*
|
||||
* polyfill-rule {
|
||||
* content: ':host menu-item';
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* to this:
|
||||
*
|
||||
* scopeName menu-item {...}
|
||||
*
|
||||
**/
|
||||
_insertPolyfillRulesInCssText(cssText: string): string {
|
||||
// Difference with webcomponents.js: does not handle comments
|
||||
return StringWrapper.replaceAllMapped(cssText, _cssContentRuleRe, function(m) {
|
||||
var rule = m[0];
|
||||
rule = StringWrapper.replace(rule, m[1], '');
|
||||
rule = StringWrapper.replace(rule, m[2], '');
|
||||
return m[3] + rule;
|
||||
});
|
||||
}
|
||||
|
||||
/* Ensure styles are scoped. Pseudo-scoping takes a rule like:
|
||||
*
|
||||
* .foo {... }
|
||||
*
|
||||
* and converts this to
|
||||
*
|
||||
* scopeName .foo { ... }
|
||||
*/
|
||||
_scopeCssText(cssText: string, scopeSelector: string, hostSelector: string): string {
|
||||
|
||||
var unscoped = this._extractUnscopedRulesFromCssText(cssText);
|
||||
cssText = this._insertPolyfillHostInCssText(cssText);
|
||||
cssText = this._convertColonHost(cssText);
|
||||
cssText = this._convertColonHostContext(cssText);
|
||||
cssText = this._convertShadowDOMSelectors(cssText);
|
||||
if (isPresent(scopeSelector)) {
|
||||
_withCssRules(cssText, (rules) => {
|
||||
cssText = this._scopeRules(rules, scopeSelector, hostSelector);
|
||||
});
|
||||
}
|
||||
cssText = cssText + '\n' + unscoped;
|
||||
return cssText.trim();
|
||||
}
|
||||
|
||||
/*
|
||||
* Process styles to add rules which will only apply under the polyfill
|
||||
* and do not process via CSSOM. (CSSOM is destructive to rules on rare
|
||||
* occasions, e.g. -webkit-calc on Safari.)
|
||||
* For example, we convert this rule:
|
||||
*
|
||||
* @polyfill-unscoped-rule {
|
||||
* content: 'menu-item';
|
||||
* ... }
|
||||
*
|
||||
* to this:
|
||||
*
|
||||
* menu-item {...}
|
||||
*
|
||||
**/
|
||||
_extractUnscopedRulesFromCssText(cssText: string): string {
|
||||
// Difference with webcomponents.js: does not handle comments
|
||||
var r = '', m;
|
||||
var matcher = RegExpWrapper.matcher(_cssContentUnscopedRuleRe, cssText);
|
||||
while (isPresent(m = RegExpMatcherWrapper.next(matcher))) {
|
||||
var rule = m[0];
|
||||
rule = StringWrapper.replace(rule, m[2], '');
|
||||
rule = StringWrapper.replace(rule, m[1], m[3]);
|
||||
r = rule + '\n\n';
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/*
|
||||
* convert a rule like :host(.foo) > .bar { }
|
||||
*
|
||||
* to
|
||||
*
|
||||
* scopeName.foo > .bar
|
||||
*/
|
||||
_convertColonHost(cssText: string): string {
|
||||
return this._convertColonRule(cssText, _cssColonHostRe,
|
||||
this._colonHostPartReplacer);
|
||||
}
|
||||
|
||||
/*
|
||||
* convert a rule like :host-context(.foo) > .bar { }
|
||||
*
|
||||
* to
|
||||
*
|
||||
* scopeName.foo > .bar, .foo scopeName > .bar { }
|
||||
*
|
||||
* and
|
||||
*
|
||||
* :host-context(.foo:host) .bar { ... }
|
||||
*
|
||||
* to
|
||||
*
|
||||
* scopeName.foo .bar { ... }
|
||||
*/
|
||||
_convertColonHostContext(cssText: string): string {
|
||||
return this._convertColonRule(cssText, _cssColonHostContextRe,
|
||||
this._colonHostContextPartReplacer);
|
||||
}
|
||||
|
||||
_convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string {
|
||||
// p1 = :host, p2 = contents of (), p3 rest of rule
|
||||
return StringWrapper.replaceAllMapped(cssText, regExp, function(m) {
|
||||
if (isPresent(m[2])) {
|
||||
var parts = m[2].split(','), r = [];
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i];
|
||||
if (isBlank(p)) break;
|
||||
p = p.trim();
|
||||
ListWrapper.push(r, partReplacer(_polyfillHostNoCombinator, p, m[3]));
|
||||
}
|
||||
return r.join(',');
|
||||
} else {
|
||||
return _polyfillHostNoCombinator + m[3];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_colonHostContextPartReplacer(host: string, part: string, suffix: string): string {
|
||||
if (StringWrapper.contains(part, _polyfillHost)) {
|
||||
return this._colonHostPartReplacer(host, part, suffix);
|
||||
} else {
|
||||
return host + part + suffix + ', ' + part + ' ' + host + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
_colonHostPartReplacer(host: string, part: string, suffix: string): string {
|
||||
return host + StringWrapper.replace(part, _polyfillHost, '') + suffix;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert combinators like ::shadow and pseudo-elements like ::content
|
||||
* by replacing with space.
|
||||
*/
|
||||
_convertShadowDOMSelectors(cssText: string): string {
|
||||
for (var i = 0; i < _shadowDOMSelectorsRe.length; i++) {
|
||||
cssText = StringWrapper.replaceAll(cssText, _shadowDOMSelectorsRe[i], ' ');
|
||||
}
|
||||
return cssText;
|
||||
}
|
||||
|
||||
// change a selector like 'div' to 'name div'
|
||||
_scopeRules(cssRules, scopeSelector: string, hostSelector: string): string {
|
||||
var cssText = '';
|
||||
if (isPresent(cssRules)) {
|
||||
for (var i = 0; i < cssRules.length; i++) {
|
||||
var rule = cssRules[i];
|
||||
if (DOM.isStyleRule(rule) || DOM.isPageRule(rule)) {
|
||||
cssText += this._scopeSelector(rule.selectorText, scopeSelector, hostSelector,
|
||||
this.strictStyling) + ' {\n';
|
||||
cssText += this._propertiesFromRule(rule) + '\n}\n\n';
|
||||
} else if (DOM.isMediaRule(rule)) {
|
||||
cssText += '@media ' + rule.media.mediaText + ' {\n';
|
||||
cssText += this._scopeRules(rule.cssRules, scopeSelector, hostSelector);
|
||||
cssText += '\n}\n\n';
|
||||
} else {
|
||||
// KEYFRAMES_RULE in IE throws when we query cssText
|
||||
// when it contains a -webkit- property.
|
||||
// if this happens, we fallback to constructing the rule
|
||||
// from the CSSRuleSet
|
||||
// https://connect.microsoft.com/IE/feedbackdetail/view/955703/accessing-csstext-of-a-keyframe-rule-that-contains-a-webkit-property-via-cssom-generates-exception
|
||||
try {
|
||||
if (isPresent(rule.cssText)) {
|
||||
cssText += rule.cssText + '\n\n';
|
||||
}
|
||||
} catch(x) {
|
||||
if (DOM.isKeyframesRule(rule) && isPresent(rule.cssRules)) {
|
||||
cssText += this._ieSafeCssTextFromKeyFrameRule(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cssText;
|
||||
}
|
||||
|
||||
_ieSafeCssTextFromKeyFrameRule(rule): string {
|
||||
var cssText = '@keyframes ' + rule.name + ' {';
|
||||
for (var i = 0; i < rule.cssRules.length; i++) {
|
||||
var r = rule.cssRules[i];
|
||||
cssText += ' ' + r.keyText + ' {' + r.style.cssText + '}';
|
||||
}
|
||||
cssText += ' }';
|
||||
return cssText;
|
||||
}
|
||||
|
||||
_scopeSelector(selector: string, scopeSelector: string, hostSelector: string,
|
||||
strict: boolean): string {
|
||||
var r = [], parts = selector.split(',');
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i];
|
||||
p = p.trim();
|
||||
if (this._selectorNeedsScoping(p, scopeSelector)) {
|
||||
p = strict && !StringWrapper.contains(p, _polyfillHostNoCombinator) ?
|
||||
this._applyStrictSelectorScope(p, scopeSelector) :
|
||||
this._applySelectorScope(p, scopeSelector, hostSelector);
|
||||
}
|
||||
ListWrapper.push(r, p);
|
||||
}
|
||||
return r.join(', ');
|
||||
}
|
||||
|
||||
_selectorNeedsScoping(selector: string, scopeSelector: string): boolean {
|
||||
var re = this._makeScopeMatcher(scopeSelector);
|
||||
return !isPresent(RegExpWrapper.firstMatch(re, selector));
|
||||
}
|
||||
|
||||
_makeScopeMatcher(scopeSelector: string): RegExp {
|
||||
var lre = RegExpWrapper.create('\\[');
|
||||
var rre = RegExpWrapper.create('\\]');
|
||||
scopeSelector = StringWrapper.replaceAll(scopeSelector, lre, '\\[');
|
||||
scopeSelector = StringWrapper.replaceAll(scopeSelector, rre, '\\]');
|
||||
return RegExpWrapper.create('^(' + scopeSelector + ')' + _selectorReSuffix, 'm');
|
||||
}
|
||||
|
||||
_applySelectorScope(selector: string, scopeSelector: string, hostSelector: string): string {
|
||||
// Difference from webcomponentsjs: scopeSelector could not be an array
|
||||
return this._applySimpleSelectorScope(selector, scopeSelector, hostSelector);
|
||||
}
|
||||
|
||||
// scope via name and [is=name]
|
||||
_applySimpleSelectorScope(selector: string, scopeSelector: string, hostSelector: string): string {
|
||||
if (isPresent(RegExpWrapper.firstMatch(_polyfillHostRe, selector))) {
|
||||
var replaceBy = this.strictStyling ? `[${hostSelector}]` : scopeSelector;
|
||||
selector = StringWrapper.replace(selector, _polyfillHostNoCombinator, replaceBy);
|
||||
return StringWrapper.replaceAll(selector, _polyfillHostRe, replaceBy + ' ');
|
||||
} else {
|
||||
return scopeSelector + ' ' + selector;
|
||||
}
|
||||
}
|
||||
|
||||
// return a selector with [name] suffix on each simple selector
|
||||
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name]
|
||||
_applyStrictSelectorScope(selector: string, scopeSelector: string): string {
|
||||
var isRe = RegExpWrapper.create('\\[is=([^\\]]*)\\]');
|
||||
scopeSelector = StringWrapper.replaceAllMapped(scopeSelector, isRe, (m) => m[1]);
|
||||
var splits = [' ', '>', '+', '~'],
|
||||
scoped = selector,
|
||||
attrName = '[' + scopeSelector + ']';
|
||||
for (var i = 0; i < splits.length; i++) {
|
||||
var sep = splits[i];
|
||||
var parts = scoped.split(sep);
|
||||
scoped = ListWrapper.map(parts, function(p) {
|
||||
// remove :host since it should be unnecessary
|
||||
var t = StringWrapper.replaceAll(p.trim(), _polyfillHostRe, '');
|
||||
if (t.length > 0 &&
|
||||
!ListWrapper.contains(splits, t) &&
|
||||
!StringWrapper.contains(t, attrName)) {
|
||||
var re = RegExpWrapper.create('([^:]*)(:*)(.*)');
|
||||
var m = RegExpWrapper.firstMatch(re, t);
|
||||
if (isPresent(m)) {
|
||||
p = m[1] + attrName + m[2] + m[3];
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}).join(sep);
|
||||
}
|
||||
return scoped;
|
||||
}
|
||||
|
||||
_insertPolyfillHostInCssText(selector: string): string {
|
||||
selector = StringWrapper.replaceAll(selector, _colonHostContextRe, _polyfillHostContext);
|
||||
selector = StringWrapper.replaceAll(selector, _colonHostRe, _polyfillHost);
|
||||
return selector;
|
||||
}
|
||||
|
||||
_propertiesFromRule(rule): string {
|
||||
var cssText = rule.style.cssText;
|
||||
// TODO(sorvell): Safari cssom incorrectly removes quotes from the content
|
||||
// property. (https://bugs.webkit.org/show_bug.cgi?id=118045)
|
||||
// don't replace attr rules
|
||||
var attrRe = RegExpWrapper.create('[\'"]+|attr');
|
||||
if (rule.style.content.length > 0 &&
|
||||
!isPresent(RegExpWrapper.firstMatch(attrRe, rule.style.content))) {
|
||||
var contentRe = RegExpWrapper.create('content:[^;]*;');
|
||||
cssText = StringWrapper.replaceAll(cssText, contentRe, 'content: \'' +
|
||||
rule.style.content + '\';');
|
||||
}
|
||||
// TODO(sorvell): we can workaround this issue here, but we need a list
|
||||
// of troublesome properties to fix https://github.com/Polymer/platform/issues/53
|
||||
//
|
||||
// inherit rules can be omitted from cssText
|
||||
// TODO(sorvell): remove when Blink bug is fixed:
|
||||
// https://code.google.com/p/chromium/issues/detail?id=358273
|
||||
//var style = rule.style;
|
||||
//for (var i = 0; i < style.length; i++) {
|
||||
// var name = style.item(i);
|
||||
// var value = style.getPropertyValue(name);
|
||||
// if (value == 'initial') {
|
||||
// cssText += name + ': initial; ';
|
||||
// }
|
||||
//}
|
||||
return cssText;
|
||||
}
|
||||
}
|
||||
|
||||
var _cssContentNextSelectorRe = RegExpWrapper.create(
|
||||
'polyfill-next-selector[^}]*content:[\\s]*?[\'"](.*?)[\'"][;\\s]*}([^{]*?){', 'im');
|
||||
var _cssContentRuleRe = RegExpWrapper.create(
|
||||
'(polyfill-rule)[^}]*(content:[\\s]*[\'"](.*?)[\'"])[;\\s]*[^}]*}', 'im');
|
||||
var _cssContentUnscopedRuleRe = RegExpWrapper.create(
|
||||
'(polyfill-unscoped-rule)[^}]*(content:[\\s]*[\'"](.*?)[\'"])[;\\s]*[^}]*}', 'im');
|
||||
var _polyfillHost = '-shadowcsshost';
|
||||
// note: :host-context pre-processed to -shadowcsshostcontext.
|
||||
var _polyfillHostContext = '-shadowcsscontext';
|
||||
var _parenSuffix = ')(?:\\((' +
|
||||
'(?:\\([^)(]*\\)|[^)(]*)+?' +
|
||||
')\\))?([^,{]*)';
|
||||
var _cssColonHostRe = RegExpWrapper.create('(' + _polyfillHost + _parenSuffix, 'im');
|
||||
var _cssColonHostContextRe = RegExpWrapper.create('(' + _polyfillHostContext + _parenSuffix, 'im');
|
||||
var _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
|
||||
var _shadowDOMSelectorsRe = [
|
||||
RegExpWrapper.create('>>>'),
|
||||
RegExpWrapper.create('::shadow'),
|
||||
RegExpWrapper.create('::content'),
|
||||
// Deprecated selectors
|
||||
RegExpWrapper.create('/deep/'), // former >>>
|
||||
RegExpWrapper.create('/shadow-deep/'), // former /deep/
|
||||
RegExpWrapper.create('/shadow/'), // former ::shadow
|
||||
];
|
||||
var _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$';
|
||||
var _polyfillHostRe = RegExpWrapper.create(_polyfillHost, 'im');
|
||||
var _colonHostRe = RegExpWrapper.create(':host', 'im');
|
||||
var _colonHostContextRe = RegExpWrapper.create(':host-context', 'im');
|
||||
|
||||
function _cssToRules(cssText: string) {
|
||||
return DOM.cssToRules(cssText);
|
||||
}
|
||||
|
||||
function _withCssRules(cssText: string, callback: Function) {
|
||||
// Difference from webcomponentjs: remove the workaround for an old bug in Chrome
|
||||
if (isBlank(callback)) return;
|
||||
var rules = _cssToRules(cssText);
|
||||
callback(rules);
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
import {XHR} from 'angular2/src/services/xhr';
|
||||
import {StyleUrlResolver} from './style_url_resolver';
|
||||
import {UrlResolver} from 'angular2/src/services/url_resolver';
|
||||
|
||||
import {ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {
|
||||
isBlank,
|
||||
isPresent,
|
||||
RegExp,
|
||||
RegExpWrapper,
|
||||
StringWrapper,
|
||||
normalizeBlank,
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {
|
||||
Promise,
|
||||
PromiseWrapper,
|
||||
} from 'angular2/src/facade/async';
|
||||
|
||||
/**
|
||||
* Inline @import rules in the given CSS.
|
||||
*
|
||||
* When an @import rules is inlined, it's url are rewritten.
|
||||
*/
|
||||
export class StyleInliner {
|
||||
_xhr: XHR;
|
||||
_urlResolver: UrlResolver;
|
||||
_styleUrlResolver: StyleUrlResolver;
|
||||
|
||||
constructor(xhr: XHR, styleUrlResolver: StyleUrlResolver, urlResolver: UrlResolver) {
|
||||
this._xhr = xhr;
|
||||
this._urlResolver = urlResolver;
|
||||
this._styleUrlResolver = styleUrlResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline the @imports rules in the given CSS text.
|
||||
*
|
||||
* The baseUrl is required to rewrite URLs in the inlined content.
|
||||
*
|
||||
* @param {string} cssText
|
||||
* @param {string} baseUrl
|
||||
* @returns {*} a Promise<string> when @import rules are present, a string otherwise
|
||||
*/
|
||||
// TODO(vicb): Union types: returns either a Promise<string> or a string
|
||||
// TODO(vicb): commented out @import rules should not be inlined
|
||||
inlineImports(cssText: string, baseUrl: string) {
|
||||
return this._inlineImports(cssText, baseUrl, []);
|
||||
}
|
||||
|
||||
_inlineImports(cssText: string, baseUrl: string, inlinedUrls: List<string>) {
|
||||
var partIndex = 0;
|
||||
var parts = StringWrapper.split(cssText, _importRe);
|
||||
|
||||
if (parts.length === 1) {
|
||||
// no @import rule found, return the original css
|
||||
return cssText;
|
||||
}
|
||||
|
||||
var promises = [];
|
||||
|
||||
while (partIndex < parts.length - 1) {
|
||||
// prefix is the content before the @import rule
|
||||
var prefix = parts[partIndex];
|
||||
// rule is the parameter of the @import rule
|
||||
var rule = parts[partIndex + 1];
|
||||
var url = _extractUrl(rule);
|
||||
if (isPresent(url)) {
|
||||
url = this._urlResolver.resolve(baseUrl, url);
|
||||
}
|
||||
var mediaQuery = _extractMediaQuery(rule);
|
||||
var promise;
|
||||
|
||||
if (isBlank(url)) {
|
||||
promise = PromiseWrapper.resolve(`/* Invalid import rule: "@import ${rule};" */`);
|
||||
} else if (ListWrapper.contains(inlinedUrls, url)) {
|
||||
// The current import rule has already been inlined, return the prefix only
|
||||
// Importing again might cause a circular dependency
|
||||
promise = PromiseWrapper.resolve(prefix);
|
||||
} else {
|
||||
ListWrapper.push(inlinedUrls, url);
|
||||
promise = PromiseWrapper.then(
|
||||
this._xhr.get(url),
|
||||
(css) => {
|
||||
// resolve nested @import rules
|
||||
css = this._inlineImports(css, url, inlinedUrls);
|
||||
if (PromiseWrapper.isPromise(css)) {
|
||||
// wait until nested @import are inlined
|
||||
return css.then((css) => {
|
||||
return prefix + this._transformImportedCss(css, mediaQuery, url) + '\n'
|
||||
}) ;
|
||||
} else {
|
||||
// there are no nested @import, return the css
|
||||
return prefix + this._transformImportedCss(css, mediaQuery, url) + '\n';
|
||||
}
|
||||
},
|
||||
(error) => `/* failed to import ${url} */\n`
|
||||
);
|
||||
}
|
||||
ListWrapper.push(promises, promise);
|
||||
partIndex += 2;
|
||||
}
|
||||
|
||||
return PromiseWrapper.all(promises).then(function (cssParts) {
|
||||
var cssText = cssParts.join('');
|
||||
if (partIndex < parts.length) {
|
||||
// append then content located after the last @import rule
|
||||
cssText += parts[partIndex];
|
||||
}
|
||||
return cssText;
|
||||
});
|
||||
}
|
||||
|
||||
_transformImportedCss(css: string, mediaQuery: string, url: string): string {
|
||||
css = this._styleUrlResolver.resolveUrls(css, url);
|
||||
return _wrapInMediaRule(css, mediaQuery);
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts the url from an import rule, supported formats:
|
||||
// - 'url' / "url",
|
||||
// - url(url) / url('url') / url("url")
|
||||
function _extractUrl(importRule: string): string {
|
||||
var match = RegExpWrapper.firstMatch(_urlRe, importRule);
|
||||
if (isBlank(match)) return null;
|
||||
return isPresent(match[1]) ? match[1] : match[2];
|
||||
}
|
||||
|
||||
// Extracts the media query from an import rule.
|
||||
// Returns null when there is no media query.
|
||||
function _extractMediaQuery(importRule: string): string {
|
||||
var match = RegExpWrapper.firstMatch(_mediaQueryRe, importRule);
|
||||
if (isBlank(match)) return null;
|
||||
var mediaQuery = match[1].trim();
|
||||
return (mediaQuery.length > 0) ? mediaQuery: null;
|
||||
}
|
||||
|
||||
// Wraps the css in a media rule when the media query is not null
|
||||
function _wrapInMediaRule(css: string, query: string): string {
|
||||
return (isBlank(query)) ? css : `@media ${query} {\n${css}\n}`;
|
||||
}
|
||||
|
||||
var _importRe = RegExpWrapper.create('@import\\s+([^;]+);');
|
||||
var _urlRe = RegExpWrapper.create(
|
||||
'url\\(\\s*?[\'"]?([^\'")]+)[\'"]?|' + // url(url) or url('url') or url("url")
|
||||
'[\'"]([^\'")]+)[\'"]' // "url" or 'url'
|
||||
);
|
||||
var _mediaQueryRe = RegExpWrapper.create('[\'"][^\'"]+[\'"]\\s*\\)?\\s*(.*)');
|
|
@ -1,38 +0,0 @@
|
|||
// Some of the code comes from WebComponents.JS
|
||||
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
|
||||
|
||||
import {RegExp, RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
|
||||
import {UrlResolver} from 'angular2/src/services/url_resolver';
|
||||
|
||||
/**
|
||||
* Rewrites URLs by resolving '@import' and 'url()' URLs from the given base URL.
|
||||
*/
|
||||
export class StyleUrlResolver {
|
||||
_resolver: UrlResolver;
|
||||
|
||||
constructor(resolver: UrlResolver) {
|
||||
this._resolver = resolver;
|
||||
}
|
||||
|
||||
resolveUrls(cssText: string, baseUrl: string) {
|
||||
cssText = this._replaceUrls(cssText, _cssUrlRe, baseUrl);
|
||||
cssText = this._replaceUrls(cssText, _cssImportRe, baseUrl);
|
||||
return cssText;
|
||||
}
|
||||
|
||||
_replaceUrls(cssText: string, re: RegExp, baseUrl: string) {
|
||||
return StringWrapper.replaceAllMapped(cssText, re, (m) => {
|
||||
var pre = m[1];
|
||||
var url = StringWrapper.replaceAll(m[2], _quoteRe, '');
|
||||
var post = m[3];
|
||||
|
||||
var resolvedUrl = this._resolver.resolve(baseUrl, url);
|
||||
|
||||
return pre + "'" + resolvedUrl + "'" + post;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var _cssUrlRe = RegExpWrapper.create('(url\\()([^)]*)(\\))');
|
||||
var _cssImportRe = RegExpWrapper.create('(@import[\\s]+(?!url\\())[\'"]([^\'"]*)[\'"](.*;)');
|
||||
var _quoteRe = RegExpWrapper.create('[\'"]');
|
|
@ -1,78 +0,0 @@
|
|||
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, el} from 'angular2/test_lib';
|
||||
import {setterFactory} from 'angular2/src/render/dom/compiler/property_setter_factory';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
export function main() {
|
||||
var div;
|
||||
beforeEach( () => {
|
||||
div = el('<div></div>');
|
||||
});
|
||||
describe('property setter factory', () => {
|
||||
|
||||
it('should return a setter for a property', () => {
|
||||
var setterFn = setterFactory('title');
|
||||
setterFn(div, 'Hello');
|
||||
expect(div.title).toEqual('Hello');
|
||||
|
||||
var otherSetterFn = setterFactory('title');
|
||||
expect(setterFn).toBe(otherSetterFn);
|
||||
});
|
||||
|
||||
it('should return a setter for an attribute', () => {
|
||||
var setterFn = setterFactory('attr.role');
|
||||
setterFn(div, 'button');
|
||||
expect(DOM.getAttribute(div, 'role')).toEqual('button');
|
||||
setterFn(div, null);
|
||||
expect(DOM.getAttribute(div, 'role')).toEqual(null);
|
||||
expect(() => {
|
||||
setterFn(div, 4);
|
||||
}).toThrowError("Invalid role attribute, only string values are allowed, got '4'");
|
||||
|
||||
var otherSetterFn = setterFactory('attr.role');
|
||||
expect(setterFn).toBe(otherSetterFn);
|
||||
});
|
||||
|
||||
it('should return a setter for a class', () => {
|
||||
var setterFn = setterFactory('class.active');
|
||||
setterFn(div, true);
|
||||
expect(DOM.hasClass(div, 'active')).toEqual(true);
|
||||
setterFn(div, false);
|
||||
expect(DOM.hasClass(div, 'active')).toEqual(false);
|
||||
|
||||
var otherSetterFn = setterFactory('class.active');
|
||||
expect(setterFn).toBe(otherSetterFn);
|
||||
});
|
||||
|
||||
it('should return a setter for a style', () => {
|
||||
var setterFn = setterFactory('style.width');
|
||||
setterFn(div, '40px');
|
||||
expect(DOM.getStyle(div, 'width')).toEqual('40px');
|
||||
setterFn(div, null);
|
||||
expect(DOM.getStyle(div, 'width')).toEqual('');
|
||||
|
||||
var otherSetterFn = setterFactory('style.width');
|
||||
expect(setterFn).toBe(otherSetterFn);
|
||||
});
|
||||
|
||||
it('should return a setter for a style with a unit', () => {
|
||||
var setterFn = setterFactory('style.height.px');
|
||||
setterFn(div, 40);
|
||||
expect(DOM.getStyle(div, 'height')).toEqual('40px');
|
||||
setterFn(div, null);
|
||||
expect(DOM.getStyle(div, 'height')).toEqual('');
|
||||
|
||||
var otherSetterFn = setterFactory('style.height.px');
|
||||
expect(setterFn).toBe(otherSetterFn);
|
||||
});
|
||||
|
||||
it('should return a setter for innerHtml', () => {
|
||||
var setterFn = setterFactory('innerHtml');
|
||||
setterFn(div, '<span></span>');
|
||||
expect(DOM.getInnerHTML(div)).toEqual('<span></span>');
|
||||
|
||||
var otherSetterFn = setterFactory('innerHtml');
|
||||
expect(setterFn).toBe(otherSetterFn);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
|
@ -1,282 +0,0 @@
|
|||
import {describe, it, expect, beforeEach, ddescribe, iit, xit, el} from 'angular2/test_lib';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {SelectorMatcher} from 'angular2/src/render/dom/compiler/selector';
|
||||
import {CssSelector} from 'angular2/src/render/dom/compiler/selector';
|
||||
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
export function main() {
|
||||
describe('SelectorMatcher', () => {
|
||||
var matcher, matched, selectableCollector, s1, s2, s3, s4;
|
||||
|
||||
function reset() {
|
||||
matched = ListWrapper.create();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
reset();
|
||||
s1 = s2 = s3 = s4 = null;
|
||||
selectableCollector = (selector, context) => {
|
||||
ListWrapper.push(matched, selector);
|
||||
ListWrapper.push(matched, context);
|
||||
}
|
||||
matcher = new SelectorMatcher();
|
||||
});
|
||||
|
||||
it('should select by element name case insensitive', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('someTag'), 1);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('SOMEOTHERTAG')[0], selectableCollector)).toEqual(false);
|
||||
expect(matched).toEqual([]);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('SOMETAG')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1]);
|
||||
});
|
||||
|
||||
it('should select by class name case insensitive', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('.someClass'), 1);
|
||||
matcher.addSelectables(s2 = CssSelector.parse('.someClass.class2'), 2);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('.SOMEOTHERCLASS')[0], selectableCollector)).toEqual(false);
|
||||
expect(matched).toEqual([]);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('.SOMECLASS')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1]);
|
||||
|
||||
reset();
|
||||
expect(matcher.match(CssSelector.parse('.someClass.class2')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1,s2[0],2]);
|
||||
});
|
||||
|
||||
it('should select by attr name case insensitive independent of the value', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('[someAttr]'), 1);
|
||||
matcher.addSelectables(s2 = CssSelector.parse('[someAttr][someAttr2]'), 2);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('[SOMEOTHERATTR]')[0], selectableCollector)).toEqual(false);
|
||||
expect(matched).toEqual([]);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('[SOMEATTR]')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1]);
|
||||
|
||||
reset();
|
||||
expect(matcher.match(CssSelector.parse('[SOMEATTR=someValue]')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1]);
|
||||
|
||||
reset();
|
||||
expect(matcher.match(CssSelector.parse('[someAttr][someAttr2]')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1,s2[0],2]);
|
||||
});
|
||||
|
||||
it('should select by attr name only once if the value is from the DOM', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('[some-decor]'), 1);
|
||||
|
||||
var elementSelector = new CssSelector();
|
||||
var element = el('<div attr></div>');
|
||||
var empty = DOM.getAttribute(element, 'attr');
|
||||
elementSelector.addAttribute('some-decor', empty);
|
||||
matcher.match(elementSelector, selectableCollector);
|
||||
expect(matched).toEqual([s1[0],1]);
|
||||
});
|
||||
|
||||
it('should select by attr name and value case insensitive', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('[someAttr=someValue]'), 1);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]')[0], selectableCollector)).toEqual(false);
|
||||
expect(matched).toEqual([]);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1]);
|
||||
});
|
||||
|
||||
it('should select by element name, class name and attribute name with value', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('someTag.someClass[someAttr=someValue]'), 1);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]')[0], selectableCollector)).toEqual(false);
|
||||
expect(matched).toEqual([]);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]')[0], selectableCollector)).toEqual(false);
|
||||
expect(matched).toEqual([]);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]')[0], selectableCollector)).toEqual(false);
|
||||
expect(matched).toEqual([]);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr]')[0], selectableCollector)).toEqual(false);
|
||||
expect(matched).toEqual([]);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1]);
|
||||
});
|
||||
|
||||
it('should select by many attributes and independent of the value', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('input[type=text][control]'), 1);
|
||||
|
||||
var cssSelector = new CssSelector();
|
||||
cssSelector.setElement('input');
|
||||
cssSelector.addAttribute('type', 'text');
|
||||
cssSelector.addAttribute('control', 'one');
|
||||
|
||||
expect(matcher.match(cssSelector, selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0], 1]);
|
||||
});
|
||||
|
||||
it('should select independent of the order in the css selector', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('[someAttr].someClass'), 1);
|
||||
matcher.addSelectables(s2 = CssSelector.parse('.someClass[someAttr]'), 2);
|
||||
matcher.addSelectables(s3 = CssSelector.parse('.class1.class2'), 3);
|
||||
matcher.addSelectables(s4 = CssSelector.parse('.class2.class1'), 4);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('[someAttr].someClass')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1,s2[0],2]);
|
||||
|
||||
reset();
|
||||
expect(matcher.match(CssSelector.parse('.someClass[someAttr]')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1,s2[0],2]);
|
||||
|
||||
reset();
|
||||
expect(matcher.match(CssSelector.parse('.class1.class2')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s3[0],3,s4[0],4]);
|
||||
|
||||
reset();
|
||||
expect(matcher.match(CssSelector.parse('.class2.class1')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s4[0],4,s3[0],3]);
|
||||
});
|
||||
|
||||
it('should not select with a matching :not selector', () => {
|
||||
matcher.addSelectables(CssSelector.parse('p:not(.someClass)'), 1);
|
||||
matcher.addSelectables(CssSelector.parse('p:not([someAttr])'), 2);
|
||||
matcher.addSelectables(CssSelector.parse(':not(.someClass)'), 3);
|
||||
matcher.addSelectables(CssSelector.parse(':not(p)'), 4);
|
||||
matcher.addSelectables(CssSelector.parse(':not(p[someAttr])'), 5);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('p.someClass[someAttr]')[0], selectableCollector)).toEqual(false);
|
||||
expect(matched).toEqual([]);
|
||||
});
|
||||
|
||||
it('should select with a non matching :not selector', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('p:not(.someClass)'), 1);
|
||||
matcher.addSelectables(s2 = CssSelector.parse('p:not(.someOtherClass[someAttr])'), 2);
|
||||
matcher.addSelectables(s3 = CssSelector.parse(':not(.someClass)'), 3);
|
||||
matcher.addSelectables(s4 = CssSelector.parse(':not(.someOtherClass[someAttr])'), 4);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('p[someOtherAttr].someOtherClass')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1,s2[0],2,s3[0],3,s4[0],4]);
|
||||
});
|
||||
|
||||
it('should select with one match in a list', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('input[type=text], textbox'), 1);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('textbox')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[1],1]);
|
||||
|
||||
reset();
|
||||
expect(matcher.match(CssSelector.parse('input[type=text]')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched).toEqual([s1[0],1]);
|
||||
});
|
||||
|
||||
it('should not select twice with two matches in a list', () => {
|
||||
matcher.addSelectables(s1 = CssSelector.parse('input, .someClass'), 1);
|
||||
|
||||
expect(matcher.match(CssSelector.parse('input.someclass')[0], selectableCollector)).toEqual(true);
|
||||
expect(matched.length).toEqual(2);
|
||||
expect(matched).toEqual([s1[0],1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CssSelector.parse', () => {
|
||||
it('should detect element names', () => {
|
||||
var cssSelector = CssSelector.parse('sometag')[0];
|
||||
expect(cssSelector.element).toEqual('sometag');
|
||||
expect(cssSelector.toString()).toEqual('sometag');
|
||||
});
|
||||
|
||||
it('should detect class names', () => {
|
||||
var cssSelector = CssSelector.parse('.someClass')[0];
|
||||
expect(cssSelector.classNames).toEqual(['someclass']);
|
||||
|
||||
expect(cssSelector.toString()).toEqual('.someclass');
|
||||
});
|
||||
|
||||
it('should detect attr names', () => {
|
||||
var cssSelector = CssSelector.parse('[attrname]')[0];
|
||||
expect(cssSelector.attrs).toEqual(['attrname', '']);
|
||||
|
||||
expect(cssSelector.toString()).toEqual('[attrname]');
|
||||
});
|
||||
|
||||
it('should detect attr values', () => {
|
||||
var cssSelector = CssSelector.parse('[attrname=attrvalue]')[0];
|
||||
expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']);
|
||||
expect(cssSelector.toString()).toEqual('[attrname=attrvalue]');
|
||||
});
|
||||
|
||||
it('should detect multiple parts', () => {
|
||||
var cssSelector = CssSelector.parse('sometag[attrname=attrvalue].someclass')[0];
|
||||
expect(cssSelector.element).toEqual('sometag');
|
||||
expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']);
|
||||
expect(cssSelector.classNames).toEqual(['someclass']);
|
||||
|
||||
expect(cssSelector.toString()).toEqual('sometag.someclass[attrname=attrvalue]');
|
||||
});
|
||||
|
||||
it('should detect multiple attributes', () => {
|
||||
var cssSelector = CssSelector.parse('input[type=text][control]')[0];
|
||||
expect(cssSelector.element).toEqual('input');
|
||||
expect(cssSelector.attrs).toEqual(['type', 'text', 'control', '']);
|
||||
|
||||
expect(cssSelector.toString()).toEqual('input[type=text][control]');
|
||||
});
|
||||
|
||||
it('should detect :not', () => {
|
||||
var cssSelector = CssSelector.parse('sometag:not([attrname=attrvalue].someclass)')[0];
|
||||
expect(cssSelector.element).toEqual('sometag');
|
||||
expect(cssSelector.attrs.length).toEqual(0);
|
||||
expect(cssSelector.classNames.length).toEqual(0);
|
||||
|
||||
var notSelector = cssSelector.notSelector;
|
||||
expect(notSelector.element).toEqual(null);
|
||||
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
|
||||
expect(notSelector.classNames).toEqual(['someclass']);
|
||||
|
||||
expect(cssSelector.toString()).toEqual('sometag:not(.someclass[attrname=attrvalue])');
|
||||
});
|
||||
|
||||
it('should detect :not without truthy', () => {
|
||||
var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)')[0];
|
||||
expect(cssSelector.element).toEqual("*");
|
||||
|
||||
var notSelector = cssSelector.notSelector;
|
||||
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
|
||||
expect(notSelector.classNames).toEqual(['someclass']);
|
||||
|
||||
expect(cssSelector.toString()).toEqual('*:not(.someclass[attrname=attrvalue])');
|
||||
});
|
||||
|
||||
it('should throw when nested :not', () => {
|
||||
expect(() => {
|
||||
CssSelector.parse('sometag:not(:not([attrname=attrvalue].someclass))')[0];
|
||||
}).toThrowError('Nesting :not is not allowed in a selector');
|
||||
});
|
||||
|
||||
it('should detect lists of selectors', () => {
|
||||
var cssSelectors = CssSelector.parse('.someclass,[attrname=attrvalue], sometag');
|
||||
expect(cssSelectors.length).toEqual(3);
|
||||
|
||||
expect(cssSelectors[0].classNames).toEqual(['someclass']);
|
||||
expect(cssSelectors[1].attrs).toEqual(['attrname', 'attrvalue']);
|
||||
expect(cssSelectors[2].element).toEqual('sometag');
|
||||
});
|
||||
|
||||
it('should detect lists of selectors with :not', () => {
|
||||
var cssSelectors = CssSelector.parse('input[type=text], :not(textarea), textbox:not(.special)');
|
||||
expect(cssSelectors.length).toEqual(3);
|
||||
|
||||
expect(cssSelectors[0].element).toEqual('input');
|
||||
expect(cssSelectors[0].attrs).toEqual(['type', 'text']);
|
||||
|
||||
expect(cssSelectors[1].element).toEqual('*');
|
||||
expect(cssSelectors[1].notSelector.element).toEqual('textarea');
|
||||
|
||||
expect(cssSelectors[2].element).toEqual('textbox');
|
||||
expect(cssSelectors[2].notSelector.classNames).toEqual(['special']);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue