3 changed files with 462 additions and 688 deletions
@ -1,686 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2025 The Thingsboard Authors |
|
||||
* |
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||
* you may not use this file except in compliance with the License. |
|
||||
* You may obtain a copy of the License at |
|
||||
* |
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
* |
|
||||
* Unless required by applicable law or agreed to in writing, software |
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||
* See the License for the specific language governing permissions and |
|
||||
* limitations under the License. |
|
||||
*/ |
|
||||
/* eslint-disable */ |
|
||||
|
|
||||
/* jshint unused:false */ |
|
||||
/* global base64_decode, CSSWizardView, window, console, jQuery */ |
|
||||
var fi = function() { |
|
||||
|
|
||||
this.cssImportStatements = []; |
|
||||
this.cssKeyframeStatements = []; |
|
||||
|
|
||||
this.cssRegex = new RegExp("([\\s\\S]*?){([\\s\\S]*?)}", "gi"); |
|
||||
this.cssMediaQueryRegex = "((@media [\\s\\S]*?){([\\s\\S]*?}\\s*?)})"; |
|
||||
this.cssKeyframeRegex = "((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})"; |
|
||||
this.combinedCSSRegex = "((\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})"; //to match css & media queries together
|
|
||||
this.cssCommentsRegex = "(\\/\\*[\\s\\S]*?\\*\\/)"; |
|
||||
this.cssImportStatementRegex = new RegExp("@import .*?;", "gi"); |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
Strip outs css comments and returns cleaned css string |
|
||||
|
|
||||
@param css, the original css string to be stipped out of comments |
|
||||
|
|
||||
@return cleanedCSS contains no css comments |
|
||||
*/ |
|
||||
fi.prototype.stripComments = function(cssString) { |
|
||||
var regex = new RegExp(this.cssCommentsRegex, "gi"); |
|
||||
|
|
||||
return cssString.replace(regex, ""); |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
Parses given css string, and returns css object |
|
||||
keys as selectors and values are css rules |
|
||||
eliminates all css comments before parsing |
|
||||
|
|
||||
@param source css string to be parsed |
|
||||
|
|
||||
@return object css |
|
||||
*/ |
|
||||
fi.prototype.parseCSS = function(source) { |
|
||||
|
|
||||
if (source === undefined) { |
|
||||
return []; |
|
||||
} |
|
||||
|
|
||||
var css = []; |
|
||||
//strip out comments
|
|
||||
//source = this.stripComments(source);
|
|
||||
|
|
||||
//get import statements
|
|
||||
|
|
||||
while (true) { |
|
||||
var imports = this.cssImportStatementRegex.exec(source); |
|
||||
if (imports !== null) { |
|
||||
this.cssImportStatements.push(imports[0]); |
|
||||
css.push({ |
|
||||
selector: "@imports", |
|
||||
type: "imports", |
|
||||
styles: imports[0], |
|
||||
}); |
|
||||
} else { |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
source = source.replace(this.cssImportStatementRegex, ""); |
|
||||
//get keyframe statements
|
|
||||
var keyframesRegex = new RegExp(this.cssKeyframeRegex, "gi"); |
|
||||
var arr; |
|
||||
while (true) { |
|
||||
arr = keyframesRegex.exec(source); |
|
||||
if (arr === null) { |
|
||||
break; |
|
||||
} |
|
||||
css.push({ |
|
||||
selector: "@keyframes", |
|
||||
type: "keyframes", |
|
||||
styles: arr[0], |
|
||||
}); |
|
||||
} |
|
||||
source = source.replace(keyframesRegex, ""); |
|
||||
|
|
||||
//unified regex
|
|
||||
var unified = new RegExp(this.combinedCSSRegex, "gi"); |
|
||||
|
|
||||
while (true) { |
|
||||
arr = unified.exec(source); |
|
||||
if (arr === null) { |
|
||||
break; |
|
||||
} |
|
||||
var selector = ""; |
|
||||
if (arr[2] === undefined) { |
|
||||
selector = arr[5].split("\r\n").join("\n").trim(); |
|
||||
} else { |
|
||||
selector = arr[2].split("\r\n").join("\n").trim(); |
|
||||
} |
|
||||
|
|
||||
/* |
|
||||
fetch comments and associate it with current selector |
|
||||
*/ |
|
||||
var commentsRegex = new RegExp(this.cssCommentsRegex, "gi"); |
|
||||
var comments = commentsRegex.exec(selector); |
|
||||
if (comments !== null) { |
|
||||
selector = selector.replace(commentsRegex, "").trim(); |
|
||||
} |
|
||||
|
|
||||
//determine the type
|
|
||||
if (selector.indexOf("@media") !== -1) { |
|
||||
//we have a media query
|
|
||||
var cssObject = { |
|
||||
selector: selector, |
|
||||
type: "media", |
|
||||
subStyles: this.parseCSS(arr[3] + "\n}"), //recursively parse media query inner css
|
|
||||
}; |
|
||||
if (comments !== null) { |
|
||||
cssObject.comments = comments[0]; |
|
||||
} |
|
||||
css.push(cssObject); |
|
||||
} else { |
|
||||
//we have standart css
|
|
||||
var rules = this.parseRules(arr[6]); |
|
||||
var style = { |
|
||||
selector: selector, |
|
||||
rules: rules, |
|
||||
}; |
|
||||
if (selector === "@font-face") { |
|
||||
style.type = "font-face"; |
|
||||
} |
|
||||
if (comments !== null) { |
|
||||
style.comments = comments[0]; |
|
||||
} |
|
||||
css.push(style); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return css; |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
parses given string containing css directives |
|
||||
and returns an array of objects containing ruleName:ruleValue pairs |
|
||||
|
|
||||
@param rules, css directive string example |
|
||||
\n\ncolor:white;\n font-size:18px;\n |
|
||||
*/ |
|
||||
fi.prototype.parseRules = function(rules) { |
|
||||
//convert all windows style line endings to unix style line endings
|
|
||||
rules = rules.split("\r\n").join("\n"); |
|
||||
var ret = []; |
|
||||
|
|
||||
// Split all rules but keep semicolon for base64 url data
|
|
||||
rules = rules.split(/;(?![^\(]*\))/); |
|
||||
|
|
||||
//proccess rules line by line
|
|
||||
for (var i = 0; i < rules.length; i++) { |
|
||||
var line = rules[i]; |
|
||||
|
|
||||
//determine if line is a valid css directive, ie color:white;
|
|
||||
line = line.trim(); |
|
||||
if (line.indexOf(":") !== -1) { |
|
||||
//line contains :
|
|
||||
line = line.split(":"); |
|
||||
var cssDirective = line[0].trim(); |
|
||||
var cssValue = line.slice(1).join(":").trim(); |
|
||||
|
|
||||
//more checks
|
|
||||
if (cssDirective.length < 1 || cssValue.length < 1) { |
|
||||
continue; //there is no css directive or value that is of length 1 or 0
|
|
||||
// PLAIN WRONG WHAT ABOUT margin:0; ?
|
|
||||
} |
|
||||
|
|
||||
//push rule
|
|
||||
ret.push({ |
|
||||
directive: cssDirective, |
|
||||
value: cssValue, |
|
||||
}); |
|
||||
} else { |
|
||||
//if there is no ':', but what if it was mis splitted value which starts with base64
|
|
||||
if (line.trim().substr(0, 7) == "base64,") { //hack :)
|
|
||||
ret[ret.length - 1].value += line.trim(); |
|
||||
} else { |
|
||||
//add rule, even if it is defective
|
|
||||
if (line.length > 0) { |
|
||||
ret.push({ |
|
||||
directive: "", |
|
||||
value: line, |
|
||||
defective: true, |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return ret; //we are done!
|
|
||||
}; |
|
||||
/* |
|
||||
just returns the rule having given directive |
|
||||
if not found returns false; |
|
||||
*/ |
|
||||
fi.prototype.findCorrespondingRule = function(rules, directive, value) { |
|
||||
if (value === undefined) { |
|
||||
value = false; |
|
||||
} |
|
||||
var ret = false; |
|
||||
for (var i = 0; i < rules.length; i++) { |
|
||||
if (rules[i].directive == directive) { |
|
||||
ret = rules[i]; |
|
||||
if (value === rules[i].value) { |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
return ret; |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
Finds styles that have given selector, compress them, |
|
||||
and returns them |
|
||||
*/ |
|
||||
fi.prototype.findBySelector = function(cssObjectArray, selector, contains) { |
|
||||
if (contains === undefined) { |
|
||||
contains = false; |
|
||||
} |
|
||||
|
|
||||
var found = []; |
|
||||
for (var i = 0; i < cssObjectArray.length; i++) { |
|
||||
if (contains === false) { |
|
||||
if (cssObjectArray[i].selector === selector) { |
|
||||
found.push(cssObjectArray[i]); |
|
||||
} |
|
||||
} else { |
|
||||
if (cssObjectArray[i].selector.indexOf(selector) !== -1) { |
|
||||
found.push(cssObjectArray[i]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
if (found.length < 2) { |
|
||||
return found; |
|
||||
} else { |
|
||||
var base = found[0]; |
|
||||
for (i = 1; i < found.length; i++) { |
|
||||
this.intelligentCSSPush([base], found[i]); |
|
||||
} |
|
||||
return [base]; //we are done!! all properties merged into base!
|
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
deletes cssObjects having given selector, and returns new array |
|
||||
*/ |
|
||||
fi.prototype.deleteBySelector = function(cssObjectArray, selector) { |
|
||||
var ret = []; |
|
||||
for (var i = 0; i < cssObjectArray.length; i++) { |
|
||||
if (cssObjectArray[i].selector !== selector) { |
|
||||
ret.push(cssObjectArray[i]); |
|
||||
} |
|
||||
} |
|
||||
return ret; |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
Compresses given cssObjectArray and tries to minimize |
|
||||
selector redundence. |
|
||||
*/ |
|
||||
fi.prototype.compressCSS = function(cssObjectArray) { |
|
||||
var compressed = []; |
|
||||
var done = {}; |
|
||||
for (var i = 0; i < cssObjectArray.length; i++) { |
|
||||
var obj = cssObjectArray[i]; |
|
||||
if (done[obj.selector] === true) { |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
var found = this.findBySelector(cssObjectArray, obj.selector); //found compressed
|
|
||||
if (found.length !== 0) { |
|
||||
compressed.push(found[0]); |
|
||||
done[obj.selector] = true; |
|
||||
} |
|
||||
} |
|
||||
return compressed; |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
Received 2 css objects with following structure |
|
||||
{ |
|
||||
rules : [{directive:"", value:""}, {directive:"", value:""}, ...] |
|
||||
selector : "SOMESELECTOR" |
|
||||
} |
|
||||
|
|
||||
returns the changed(new,removed,updated) values on css1 parameter, on same structure |
|
||||
|
|
||||
if two css objects are the same, then returns false |
|
||||
|
|
||||
if a css directive exists in css1 and css2, and its value is different, it is included in diff |
|
||||
if a css directive exists in css1 and not css2, it is then included in diff |
|
||||
if a css directive exists in css2 but not css1, then it is deleted in css1, it would be included in diff but will be marked as type='DELETED' |
|
||||
|
|
||||
@object css1 css object |
|
||||
@object css2 css object |
|
||||
|
|
||||
@return diff css object contains changed values in css1 in regards to css2 see test input output in /test/data/css.js |
|
||||
*/ |
|
||||
fi.prototype.cssDiff = function(css1, css2) { |
|
||||
if (css1.selector !== css2.selector) { |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
//if one of them is media query return false, because diff function can not operate on media queries
|
|
||||
if ((css1.type === "media" || css2.type === "media")) { |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
var diff = { |
|
||||
selector: css1.selector, |
|
||||
rules: [], |
|
||||
}; |
|
||||
var rule1, rule2; |
|
||||
for (var i = 0; i < css1.rules.length; i++) { |
|
||||
rule1 = css1.rules[i]; |
|
||||
//find rule2 which has the same directive as rule1
|
|
||||
rule2 = this.findCorrespondingRule(css2.rules, rule1.directive, rule1.value); |
|
||||
if (rule2 === false) { |
|
||||
//rule1 is a new rule in css1
|
|
||||
diff.rules.push(rule1); |
|
||||
} else { |
|
||||
//rule2 was found only push if its value is different too
|
|
||||
if (rule1.value !== rule2.value) { |
|
||||
diff.rules.push(rule1); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//now for rules exists in css2 but not in css1, which means deleted rules
|
|
||||
for (var ii = 0; ii < css2.rules.length; ii++) { |
|
||||
rule2 = css2.rules[ii]; |
|
||||
//find rule2 which has the same directive as rule1
|
|
||||
rule1 = this.findCorrespondingRule(css1.rules, rule2.directive); |
|
||||
if (rule1 === false) { |
|
||||
//rule1 is a new rule
|
|
||||
rule2.type = "DELETED"; //mark it as a deleted rule, so that other merge operations could be true
|
|
||||
diff.rules.push(rule2); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (diff.rules.length === 0) { |
|
||||
return false; |
|
||||
} |
|
||||
return diff; |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
Merges 2 different css objects together |
|
||||
using intelligentCSSPush, |
|
||||
|
|
||||
@param cssObjectArray, target css object array |
|
||||
@param newArray, source array that will be pushed into cssObjectArray parameter |
|
||||
@param reverse, [optional], if given true, first parameter will be traversed on reversed order |
|
||||
effectively giving priority to the styles in newArray |
|
||||
*/ |
|
||||
fi.prototype.intelligentMerge = function(cssObjectArray, newArray, reverse) { |
|
||||
if (reverse === undefined) { |
|
||||
reverse = false; |
|
||||
} |
|
||||
|
|
||||
for (var i = 0; i < newArray.length; i++) { |
|
||||
this.intelligentCSSPush(cssObjectArray, newArray[i], reverse); |
|
||||
} |
|
||||
for (i = 0; i < cssObjectArray.length; i++) { |
|
||||
var cobj = cssObjectArray[i]; |
|
||||
if (cobj.type === "media" || (cobj.type === "keyframes")) { |
|
||||
continue; |
|
||||
} |
|
||||
cobj.rules = this.compactRules(cobj.rules); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
inserts new css objects into a bigger css object |
|
||||
with same selectors groupped together |
|
||||
|
|
||||
@param cssObjectArray, array of bigger css object to be pushed into |
|
||||
@param minimalObject, single css object |
|
||||
@param reverse [optional] default is false, if given, cssObjectArray will be reversly traversed |
|
||||
resulting more priority in minimalObject's styles |
|
||||
*/ |
|
||||
fi.prototype.intelligentCSSPush = function(cssObjectArray, minimalObject, reverse) { |
|
||||
var pushSelector = minimalObject.selector; |
|
||||
//find correct selector if not found just push minimalObject into cssObject
|
|
||||
var cssObject = false; |
|
||||
|
|
||||
if (reverse === undefined) { |
|
||||
reverse = false; |
|
||||
} |
|
||||
|
|
||||
if (reverse === false) { |
|
||||
for (var i = 0; i < cssObjectArray.length; i++) { |
|
||||
if (cssObjectArray[i].selector === minimalObject.selector) { |
|
||||
cssObject = cssObjectArray[i]; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
} else { |
|
||||
for (var j = cssObjectArray.length - 1; j > -1; j--) { |
|
||||
if (cssObjectArray[j].selector === minimalObject.selector) { |
|
||||
cssObject = cssObjectArray[j]; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (cssObject === false) { |
|
||||
cssObjectArray.push(minimalObject); //just push, because cssSelector is new
|
|
||||
} else { |
|
||||
if (minimalObject.type !== "media") { |
|
||||
for (var ii = 0; ii < minimalObject.rules.length; ii++) { |
|
||||
var rule = minimalObject.rules[ii]; |
|
||||
//find rule inside cssObject
|
|
||||
var oldRule = this.findCorrespondingRule(cssObject.rules, rule.directive); |
|
||||
if (oldRule === false) { |
|
||||
cssObject.rules.push(rule); |
|
||||
} else if (rule.type == "DELETED") { |
|
||||
oldRule.type = "DELETED"; |
|
||||
} else { |
|
||||
//rule found just update value
|
|
||||
|
|
||||
oldRule.value = rule.value; |
|
||||
} |
|
||||
} |
|
||||
} else { |
|
||||
cssObject.subStyles = minimalObject.subStyles; //TODO, make this intelligent too
|
|
||||
} |
|
||||
|
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
filter outs rule objects whose type param equal to DELETED |
|
||||
|
|
||||
@param rules, array of rules |
|
||||
|
|
||||
@returns rules array, compacted by deleting all unneccessary rules |
|
||||
*/ |
|
||||
fi.prototype.compactRules = function(rules) { |
|
||||
var newRules = []; |
|
||||
for (var i = 0; i < rules.length; i++) { |
|
||||
if (rules[i].type !== "DELETED") { |
|
||||
newRules.push(rules[i]); |
|
||||
} |
|
||||
} |
|
||||
return newRules; |
|
||||
}; |
|
||||
/* |
|
||||
computes string for ace editor using this.css or given cssBase optional parameter |
|
||||
|
|
||||
@param [optional] cssBase, if given computes cssString from cssObject array |
|
||||
*/ |
|
||||
fi.prototype.getCSSForEditor = function(cssBase, depth) { |
|
||||
if (depth === undefined) { |
|
||||
depth = 0; |
|
||||
} |
|
||||
var ret = ""; |
|
||||
if (cssBase === undefined) { |
|
||||
cssBase = this.css; |
|
||||
} |
|
||||
//append imports
|
|
||||
for (var i = 0; i < cssBase.length; i++) { |
|
||||
if (cssBase[i].type == "imports") { |
|
||||
ret += cssBase[i].styles + "\n\n"; |
|
||||
} |
|
||||
} |
|
||||
for (i = 0; i < cssBase.length; i++) { |
|
||||
var tmp = cssBase[i]; |
|
||||
if (tmp.selector === undefined) { //temporarily omit media queries
|
|
||||
continue; |
|
||||
} |
|
||||
var comments = ""; |
|
||||
if (tmp.comments !== undefined) { |
|
||||
comments = tmp.comments + "\n"; |
|
||||
} |
|
||||
|
|
||||
if (tmp.type == "media") { //also put media queries to output
|
|
||||
ret += comments + tmp.selector + "{\n"; |
|
||||
ret += this.getCSSForEditor(tmp.subStyles, depth + 1); |
|
||||
ret += "}\n\n"; |
|
||||
} else if (tmp.type !== "keyframes" && tmp.type !== "imports") { |
|
||||
ret += this.getSpaces(depth) + comments + tmp.selector + " {\n"; |
|
||||
ret += this.getCSSOfRules(tmp.rules, depth + 1); |
|
||||
ret += this.getSpaces(depth) + "}\n\n"; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//append keyFrames
|
|
||||
for (i = 0; i < cssBase.length; i++) { |
|
||||
if (cssBase[i].type == "keyframes") { |
|
||||
ret += cssBase[i].styles + "\n\n"; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return ret; |
|
||||
}; |
|
||||
|
|
||||
fi.prototype.getImports = function(cssObjectArray) { |
|
||||
var imps = []; |
|
||||
for (var i = 0; i < cssObjectArray.length; i++) { |
|
||||
if (cssObjectArray[i].type == "imports") { |
|
||||
imps.push(cssObjectArray[i].styles); |
|
||||
} |
|
||||
} |
|
||||
return imps; |
|
||||
}; |
|
||||
/* |
|
||||
given rules array, returns visually formatted css string |
|
||||
to be used inside editor |
|
||||
*/ |
|
||||
fi.prototype.getCSSOfRules = function(rules, depth) { |
|
||||
var ret = ""; |
|
||||
for (var i = 0; i < rules.length; i++) { |
|
||||
if (rules[i] === undefined) { |
|
||||
continue; |
|
||||
} |
|
||||
if (rules[i].defective === undefined) { |
|
||||
ret += this.getSpaces(depth) + rules[i].directive + " : " + rules[i].value + ";\n"; |
|
||||
} else { |
|
||||
ret += this.getSpaces(depth) + rules[i].value + ";\n"; |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
return ret || "\n"; |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
A very simple helper function returns number of spaces appended in a single string, |
|
||||
the number depends input parameter, namely input*2 |
|
||||
*/ |
|
||||
fi.prototype.getSpaces = function(num) { |
|
||||
var ret = ""; |
|
||||
for (var i = 0; i < num * 4; i++) { |
|
||||
ret += " "; |
|
||||
} |
|
||||
return ret; |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
Given css string or objectArray, parses it and then for every selector, |
|
||||
prepends this.cssPreviewNamespace to prevent css collision issues |
|
||||
|
|
||||
@returns css string in which this.cssPreviewNamespace prepended |
|
||||
*/ |
|
||||
fi.prototype.applyNamespacing = function(css, forcedNamespace) { |
|
||||
var cssObjectArray = css; |
|
||||
var namespaceClass = "." + this.cssPreviewNamespace; |
|
||||
if (forcedNamespace !== undefined) { |
|
||||
namespaceClass = forcedNamespace; |
|
||||
} |
|
||||
|
|
||||
if (typeof css === "string") { |
|
||||
cssObjectArray = this.parseCSS(css); |
|
||||
} |
|
||||
|
|
||||
for (var i = 0; i < cssObjectArray.length; i++) { |
|
||||
var obj = cssObjectArray[i]; |
|
||||
|
|
||||
//bypass namespacing for @font-face @keyframes @import
|
|
||||
if (obj.selector.indexOf("@font-face") > -1 || obj.selector.indexOf("keyframes") > -1 || obj.selector.indexOf("@import") > -1 || obj.selector.indexOf(".form-all") > -1 || obj.selector.indexOf("#stage") > -1) { |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
if (obj.type !== "media") { |
|
||||
var selector = obj.selector.split(","); |
|
||||
var newSelector = []; |
|
||||
for (var j = 0; j < selector.length; j++) { |
|
||||
if (selector[j].indexOf(".supernova") === -1) { //do not apply namespacing to selectors including supernova
|
|
||||
newSelector.push(namespaceClass + " " + selector[j]); |
|
||||
} else { |
|
||||
newSelector.push(selector[j]); |
|
||||
} |
|
||||
} |
|
||||
obj.selector = newSelector.join(","); |
|
||||
} else { |
|
||||
obj.subStyles = this.applyNamespacing(obj.subStyles, forcedNamespace); //handle media queries as well
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return cssObjectArray; |
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
given css string or object array, clears possible namespacing from |
|
||||
all of the selectors inside the css |
|
||||
*/ |
|
||||
fi.prototype.clearNamespacing = function(css, returnObj) { |
|
||||
if (returnObj === undefined) { |
|
||||
returnObj = false; |
|
||||
} |
|
||||
var cssObjectArray = css; |
|
||||
var namespaceClass = "." + this.cssPreviewNamespace; |
|
||||
if (typeof css === "string") { |
|
||||
cssObjectArray = this.parseCSS(css); |
|
||||
} |
|
||||
|
|
||||
for (var i = 0; i < cssObjectArray.length; i++) { |
|
||||
var obj = cssObjectArray[i]; |
|
||||
|
|
||||
if (obj.type !== "media") { |
|
||||
var selector = obj.selector.split(","); |
|
||||
var newSelector = []; |
|
||||
for (var j = 0; j < selector.length; j++) { |
|
||||
newSelector.push(selector[j].split(namespaceClass + " ").join("")); |
|
||||
} |
|
||||
obj.selector = newSelector.join(","); |
|
||||
} else { |
|
||||
obj.subStyles = this.clearNamespacing(obj.subStyles, true); //handle media queries as well
|
|
||||
} |
|
||||
} |
|
||||
if (returnObj === false) { |
|
||||
return this.getCSSForEditor(cssObjectArray); |
|
||||
} else { |
|
||||
return cssObjectArray; |
|
||||
} |
|
||||
|
|
||||
}; |
|
||||
|
|
||||
/* |
|
||||
creates a new style tag (also destroys the previous one) |
|
||||
and injects given css string into that css tag |
|
||||
*/ |
|
||||
fi.prototype.createStyleElement = function(id, css, format) { |
|
||||
if (format === undefined) { |
|
||||
format = false; |
|
||||
} |
|
||||
|
|
||||
if (this.testMode === false && format !== "nonamespace") { |
|
||||
//apply namespacing classes
|
|
||||
css = this.applyNamespacing(css); |
|
||||
} |
|
||||
|
|
||||
if (typeof css != "string") { |
|
||||
css = this.getCSSForEditor(css); |
|
||||
} |
|
||||
//apply formatting for css
|
|
||||
if (format === true) { |
|
||||
css = this.getCSSForEditor(this.parseCSS(css)); |
|
||||
} |
|
||||
|
|
||||
if (this.testMode !== false) { |
|
||||
return this.testMode("create style #" + id, css); //if test mode, just pass result to callback
|
|
||||
} |
|
||||
|
|
||||
var __el = document.getElementById(id); |
|
||||
if (__el) { |
|
||||
__el.parentNode.removeChild(__el); |
|
||||
} |
|
||||
|
|
||||
var head = document.head || document.getElementsByTagName("head")[0], |
|
||||
style = document.createElement("style"); |
|
||||
|
|
||||
style.id = id; |
|
||||
style.type = "text/css"; |
|
||||
|
|
||||
head.appendChild(style); |
|
||||
|
|
||||
if (style.styleSheet && !style.sheet) { |
|
||||
style.styleSheet.cssText = css; |
|
||||
} else { |
|
||||
style.appendChild(document.createTextNode(css)); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default fi; |
|
||||
|
|
||||
/* eslint-enable */ |
|
||||
@ -0,0 +1,460 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
interface CSSRule { |
||||
|
directive: string; |
||||
|
value: string; |
||||
|
defective?: boolean; |
||||
|
type?: string; |
||||
|
} |
||||
|
|
||||
|
interface CSSObject { |
||||
|
selector: string; |
||||
|
type?: 'media' | 'keyframes' | 'imports' | 'font-face'; |
||||
|
rules?: CSSRule[]; |
||||
|
subStyles?: CSSObject[]; |
||||
|
styles?: string; |
||||
|
comments?: string; |
||||
|
} |
||||
|
|
||||
|
export default class CSSParser { |
||||
|
cssPreviewNamespace: string = ''; |
||||
|
testMode: boolean | ((action: string, css: string) => string) = false; |
||||
|
|
||||
|
cssImportStatements: string[] = []; |
||||
|
|
||||
|
private readonly cssKeyframeRegex: string = '((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})'; |
||||
|
private readonly combinedCSSRegex: string = '((\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})'; |
||||
|
private readonly cssCommentsRegex: string = '(\\/\\*[\\s\\S]*?\\*\\/)'; |
||||
|
private readonly cssImportStatementRegex: RegExp = /@import .*?;/gi; |
||||
|
|
||||
|
/** |
||||
|
* Removes CSS comments from the provided CSS string. |
||||
|
* @param cssString - The CSS string to strip comments from. |
||||
|
* @returns The CSS string with comments removed. |
||||
|
*/ |
||||
|
stripComments(cssString: string): string { |
||||
|
const regex = new RegExp(this.cssCommentsRegex, 'gi'); |
||||
|
return cssString.replace(regex, ''); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Parses a CSS string into an array of CSS objects with selectors and rules. |
||||
|
* @param source - The CSS string to parse. |
||||
|
* @returns An array of CSS objects. |
||||
|
*/ |
||||
|
parseCSS(source?: string): CSSObject[] { |
||||
|
if (!source) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
const css: CSSObject[] = []; |
||||
|
let cssSource = source; |
||||
|
|
||||
|
const importStatementRegex = new RegExp(this.cssImportStatementRegex.source, this.cssImportStatementRegex.flags); |
||||
|
cssSource = cssSource.replace(importStatementRegex, (match) => { |
||||
|
this.cssImportStatements.push(match); |
||||
|
css.push({ selector: '@imports', type: 'imports', styles: match }); |
||||
|
return ''; |
||||
|
}); |
||||
|
|
||||
|
// Extract keyframe statements
|
||||
|
const keyframesRegex = new RegExp(this.cssKeyframeRegex, 'gi'); |
||||
|
cssSource = cssSource.replace(keyframesRegex, (match) => { |
||||
|
css.push({ selector: '@keyframes', type: 'keyframes', styles: match }); |
||||
|
return ''; |
||||
|
}); |
||||
|
|
||||
|
// Parse remaining CSS
|
||||
|
const unifiedRegex = new RegExp(this.combinedCSSRegex, 'gi'); |
||||
|
let match: RegExpExecArray | null; |
||||
|
while ((match = unifiedRegex.exec(cssSource)) !== null) { |
||||
|
const selector = (match[2] ?? match[5]).replace(/\r\n/g, '\n').trim(); |
||||
|
|
||||
|
// Extract comments
|
||||
|
const commentsRegex = new RegExp(this.cssCommentsRegex, 'gi'); |
||||
|
const comments = commentsRegex.exec(selector); |
||||
|
const cleanSelector = comments ? selector.replace(commentsRegex, '').trim() : selector; |
||||
|
|
||||
|
if (cleanSelector.includes('@media')) { |
||||
|
css.push({ |
||||
|
selector: cleanSelector, |
||||
|
type: 'media', |
||||
|
subStyles: this.parseCSS(match[3] + '\n}'), |
||||
|
...(comments && {comments: comments[0]}), |
||||
|
}); |
||||
|
} else { |
||||
|
const rules = this.parseRules(match[6]); |
||||
|
const style: CSSObject = { |
||||
|
selector: cleanSelector, |
||||
|
rules, |
||||
|
...(cleanSelector === '@font-face' && {type: 'font-face'}), |
||||
|
...(comments && {comments: comments[0]}), |
||||
|
}; |
||||
|
css.push(style); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return css; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Parses CSS rules into an array of rule objects. |
||||
|
* @param rules - The CSS rules string. |
||||
|
* @returns An array of rule objects with directive and value. |
||||
|
*/ |
||||
|
parseRules(rules: string): CSSRule[] { |
||||
|
const normalizedRules = rules.replace(/\r\n/g, '\n'); |
||||
|
const ruleList = normalizedRules.split(/;(?![^(]*\))/); |
||||
|
const result: CSSRule[] = []; |
||||
|
|
||||
|
for (const line of ruleList) { |
||||
|
const trimmedLine = line.trim(); |
||||
|
if (!trimmedLine) continue; |
||||
|
|
||||
|
if (trimmedLine.includes(':')) { |
||||
|
const [directive, ...valueParts] = trimmedLine.split(':'); |
||||
|
const value = valueParts.join(':').trim(); |
||||
|
if (directive.trim() && value) { |
||||
|
result.push({directive: directive.trim(), value}); |
||||
|
} |
||||
|
} else if (trimmedLine.startsWith('base64,')) { |
||||
|
if (result.length > 0) { |
||||
|
result[result.length - 1].value += trimmedLine; |
||||
|
} |
||||
|
} else if (trimmedLine) { |
||||
|
result.push({directive: '', value: trimmedLine, defective: true}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Finds a rule matching the given directive in the rules array. |
||||
|
* @param rules - The array of CSS rules. |
||||
|
* @param directive - The directive to search for. |
||||
|
* @param value - Optional value to match. |
||||
|
* @returns The matching rule or false if not found. |
||||
|
*/ |
||||
|
findCorrespondingRule(rules: CSSRule[], directive: string, value?: string): CSSRule | false { |
||||
|
return rules.find(rule => rule.directive === directive && (!value || rule.value === value)) || false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Finds CSS objects by selector, optionally merging duplicates. |
||||
|
* @param cssObjectArray - The array of CSS objects. |
||||
|
* @param selector - The selector to search for. |
||||
|
* @param contains - If true, matches selectors containing the string. |
||||
|
* @returns An array of matching CSS objects. |
||||
|
*/ |
||||
|
findBySelector(cssObjectArray: CSSObject[], selector: string, contains: boolean = false): CSSObject[] { |
||||
|
const found = cssObjectArray.filter(obj => contains ? obj.selector.includes(selector) : obj.selector === selector); |
||||
|
|
||||
|
if (found.length < 2) return found; |
||||
|
|
||||
|
const base = found[0]; |
||||
|
for (let i = 1; i < found.length; i++) { |
||||
|
this.intelligentCSSPush([base], found[i]); |
||||
|
} |
||||
|
return [base]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Deletes CSS objects with the given selector. |
||||
|
* @param cssObjectArray - The array of CSS objects. |
||||
|
* @param selector - The selector to delete. |
||||
|
* @returns A new array without the matching CSS objects. |
||||
|
*/ |
||||
|
deleteBySelector(cssObjectArray: CSSObject[], selector: string): CSSObject[] { |
||||
|
return cssObjectArray.filter(obj => obj.selector !== selector); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Compresses CSS objects by merging duplicates. |
||||
|
* @param cssObjectArray - The array of CSS objects to compress. |
||||
|
* @returns A compressed array of CSS objects. |
||||
|
*/ |
||||
|
compressCSS(cssObjectArray: CSSObject[]): CSSObject[] { |
||||
|
const compressed: CSSObject[] = []; |
||||
|
const done = new Set<string>(); |
||||
|
|
||||
|
for (const obj of cssObjectArray) { |
||||
|
if (done.has(obj.selector)) continue; |
||||
|
const found = this.findBySelector(cssObjectArray, obj.selector); |
||||
|
if (found.length) { |
||||
|
compressed.push(found[0]); |
||||
|
done.add(obj.selector); |
||||
|
} |
||||
|
} |
||||
|
return compressed; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computes the difference between two CSS objects. |
||||
|
* @param css1 - The first CSS object. |
||||
|
* @param css2 - The second CSS object. |
||||
|
* @returns A CSS object with the differences or false if no differences. |
||||
|
*/ |
||||
|
cssDiff(css1: CSSObject, css2: CSSObject): CSSObject | false { |
||||
|
if (css1.selector !== css2.selector || css1.type === 'media' || css2.type === 'media') { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
const diff: CSSObject = {selector: css1.selector, rules: []}; |
||||
|
const rules1 = css1.rules ?? []; |
||||
|
const rules2 = css2.rules ?? []; |
||||
|
|
||||
|
for (const rule1 of rules1) { |
||||
|
const rule2 = this.findCorrespondingRule(rules2, rule1.directive, rule1.value); |
||||
|
if (!rule2 || rule1.value !== rule2.value) { |
||||
|
diff.rules!.push(rule1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (const rule2 of rules2) { |
||||
|
if (!this.findCorrespondingRule(rules1, rule2.directive)) { |
||||
|
diff.rules!.push({...rule2, type: 'DELETED'}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return diff.rules!.length ? diff : false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Merges two CSS object arrays intelligently. |
||||
|
* @param cssObjectArray - The target CSS object array. |
||||
|
* @param newArray - The source CSS object array to merge. |
||||
|
* @param reverse - If true, prioritizes styles in newArray. |
||||
|
*/ |
||||
|
intelligentMerge(cssObjectArray: CSSObject[], newArray: CSSObject[], reverse: boolean = false): void { |
||||
|
for (const obj of newArray) { |
||||
|
this.intelligentCSSPush(cssObjectArray, obj, reverse); |
||||
|
} |
||||
|
for (const obj of cssObjectArray) { |
||||
|
if (obj.type !== 'media' && obj.type !== 'keyframes') { |
||||
|
obj.rules = this.compactRules(obj.rules ?? []); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Pushes a CSS object into an array, merging with existing selectors. |
||||
|
* @param cssObjectArray - The target CSS object array. |
||||
|
* @param minimalObject - The CSS object to push. |
||||
|
* @param reverse - If true, traverses array in reverse for priority. |
||||
|
*/ |
||||
|
intelligentCSSPush(cssObjectArray: CSSObject[], minimalObject: CSSObject, reverse: boolean = false): void { |
||||
|
const cssObject = (reverse ? cssObjectArray.slice().reverse() : cssObjectArray) |
||||
|
.find(obj => obj.selector === minimalObject.selector) ?? false; |
||||
|
|
||||
|
if (!cssObject) { |
||||
|
cssObjectArray.push(minimalObject); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (minimalObject.type !== 'media') { |
||||
|
for (const rule of minimalObject.rules ?? []) { |
||||
|
const oldRule = this.findCorrespondingRule(cssObject.rules ?? [], rule.directive); |
||||
|
if (!oldRule) { |
||||
|
cssObject.rules!.push(rule); |
||||
|
} else if (rule.type === 'DELETED') { |
||||
|
oldRule.type = 'DELETED'; |
||||
|
} else { |
||||
|
oldRule.value = rule.value; |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
cssObject.subStyles = minimalObject.subStyles; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Filters out rules marked as DELETED. |
||||
|
* @param rules - The array of CSS rules. |
||||
|
* @returns A compacted array of rules. |
||||
|
*/ |
||||
|
compactRules(rules: CSSRule[]): CSSRule[] { |
||||
|
return rules.filter(rule => rule.type !== 'DELETED'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Generates a formatted CSS string for an editor. |
||||
|
* @param cssBase - The CSS object array to format. |
||||
|
* @param depth - The indentation depth. |
||||
|
* @returns A formatted CSS string. |
||||
|
*/ |
||||
|
getCSSForEditor(cssBase?: CSSObject[], depth: number = 0): string { |
||||
|
const css = cssBase ?? this.parseCSS(''); |
||||
|
let result = ''; |
||||
|
|
||||
|
// Append imports
|
||||
|
for (const obj of css) { |
||||
|
if (obj.type === 'imports') { |
||||
|
result += `${obj.styles}\n\n`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Append styles
|
||||
|
for (const obj of css) { |
||||
|
if (!obj.selector) continue; |
||||
|
const comments = obj.comments ? `${obj.comments}\n` : ''; |
||||
|
|
||||
|
if (obj.type === 'media') { |
||||
|
result += `${comments}${obj.selector} {\n${this.getCSSForEditor(obj.subStyles, depth + 1)}}\n\n`; |
||||
|
} else if (obj.type !== 'keyframes' && obj.type !== 'imports') { |
||||
|
result += `${this.getSpaces(depth)}${comments}${obj.selector} {\n${this.getCSSOfRules(obj.rules ?? [], depth + 1)}${this.getSpaces(depth)}}\n\n`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Append keyframes
|
||||
|
for (const obj of css) { |
||||
|
if (obj.type === 'keyframes') { |
||||
|
result += `${obj.styles}\n\n`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Retrieves all import statements from a CSS object array. |
||||
|
* @param cssObjectArray - The CSS object array. |
||||
|
* @returns An array of import statement strings. |
||||
|
*/ |
||||
|
getImports(cssObjectArray: CSSObject[]): string[] { |
||||
|
return cssObjectArray.filter(obj => obj.type === 'imports').map(obj => obj.styles!); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Formats CSS rules into a string for an editor. |
||||
|
* @param rules - The array of CSS rules. |
||||
|
* @param depth - The indentation depth. |
||||
|
* @returns A formatted CSS rules string. |
||||
|
*/ |
||||
|
getCSSOfRules(rules: CSSRule[], depth: number): string { |
||||
|
let result = ''; |
||||
|
for (const rule of rules) { |
||||
|
if (!rule) continue; |
||||
|
if (rule.defective) { |
||||
|
result += `${this.getSpaces(depth)}${rule.value};\n`; |
||||
|
} else { |
||||
|
result += `${this.getSpaces(depth)}${rule.directive}: ${rule.value};\n`; |
||||
|
} |
||||
|
} |
||||
|
return result || '\n'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Generates indentation spaces based on depth. |
||||
|
* @param num - The indentation level. |
||||
|
* @returns A string of spaces. |
||||
|
*/ |
||||
|
getSpaces(num: number): string { |
||||
|
return ' '.repeat(num * 4); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Applies a namespace to CSS selectors to prevent collisions. |
||||
|
* @param css - The CSS string or object array. |
||||
|
* @param forcedNamespace - Optional custom namespace. |
||||
|
* @returns The namespaced CSS object array. |
||||
|
*/ |
||||
|
applyNamespacing(css: string | CSSObject[], forcedNamespace?: string): CSSObject[] { |
||||
|
const namespaceClass = forcedNamespace ?? `.${this.cssPreviewNamespace}`; |
||||
|
const cssObjectArray = typeof css === 'string' ? this.parseCSS(css) : css; |
||||
|
|
||||
|
for (const obj of cssObjectArray) { |
||||
|
if (['@font-face', 'keyframes', '@import', '.form-all', '#stage'].some(s => obj.selector.includes(s))) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (obj.type !== 'media') { |
||||
|
obj.selector = obj.selector.split(',') |
||||
|
.map(sel => sel.includes('.supernova') ? sel : `${namespaceClass} ${sel}`) |
||||
|
.join(','); |
||||
|
} else { |
||||
|
obj.subStyles = this.applyNamespacing(obj.subStyles ?? [], forcedNamespace); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return cssObjectArray; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Removes namespacing from CSS selectors. |
||||
|
* @param css - The CSS string or object array. |
||||
|
* @param returnObj - If true, returns the CSS object array. |
||||
|
* @returns The CSS string or object array with namespacing removed. |
||||
|
*/ |
||||
|
clearNamespacing(css: string | CSSObject[], returnObj: boolean = false): string | CSSObject[] { |
||||
|
const namespaceClass = `.${this.cssPreviewNamespace}`; |
||||
|
const cssObjectArray = typeof css === 'string' ? this.parseCSS(css) : css; |
||||
|
|
||||
|
for (const obj of cssObjectArray) { |
||||
|
if (obj.type !== 'media') { |
||||
|
obj.selector = obj.selector |
||||
|
.split(',') |
||||
|
.map(sel => sel.split(namespaceClass + ' ').join('')) |
||||
|
.join(','); |
||||
|
} else { |
||||
|
obj.subStyles = this.clearNamespacing(obj.subStyles ?? [], true) as CSSObject[]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return returnObj ? cssObjectArray : this.getCSSForEditor(cssObjectArray); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Creates a style element with the provided CSS. |
||||
|
* @param id - The ID for the style element. |
||||
|
* @param css - The CSS string or object array. |
||||
|
* @param format - If true, formats the CSS; if 'nonamespace', skips namespacing. |
||||
|
*/ |
||||
|
createStyleElement(id: string, css: string | CSSObject[], format: boolean | 'nonamespace' = false): void | string { |
||||
|
let cssString = typeof css === 'string' ? css : this.getCSSForEditor(css); |
||||
|
|
||||
|
if (this.testMode === false && format !== 'nonamespace') { |
||||
|
cssString = this.getCSSForEditor(this.applyNamespacing(css)); |
||||
|
} |
||||
|
|
||||
|
if (format === true) { |
||||
|
cssString = this.getCSSForEditor(this.parseCSS(cssString)); |
||||
|
} |
||||
|
|
||||
|
if (typeof this.testMode === 'function') { |
||||
|
return this.testMode(`create style #${id}`, cssString); |
||||
|
} |
||||
|
|
||||
|
const existingElement = document.getElementById(id); |
||||
|
existingElement?.remove(); |
||||
|
|
||||
|
if (!css) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const style = document.createElement('style'); |
||||
|
style.id = id; |
||||
|
|
||||
|
if ('styleSheet' in style && !('sheet' in style)) { |
||||
|
(style as any).styleSheet.cssText = cssString; |
||||
|
} else { |
||||
|
style.appendChild(document.createTextNode(cssString)); |
||||
|
} |
||||
|
|
||||
|
(document.head || document.getElementsByTagName('head')[0]).appendChild(style); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue