689 lines
19 KiB
JavaScript
689 lines
19 KiB
JavaScript
/*
|
|
* Copyright © 2016-2019 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 */
|