/*
This file contains proprietary software owned by Motorola Mobility, Inc.
No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
(c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved.
*/
var Montage = require("montage/core/core").Montage,
Component = require("montage/ui/component").Component,
cssShorthandMap = require("js/panels/CSSPanel/css-shorthand-map").CSS_SHORTHAND_MAP,
nj = require("js/lib/NJUtils").NJUtils;
/*
Styles Manager
________________________________________
Interface for dealing with stylesheets
Properties:
- Stage stylesheet
- Default user stylesheet (e.g. styles.css)
Methods:
- Rules:
- Get matching rules for element
- Add rule
- Delete rule
- Enable rule
- Disable rule
- Styles:
- Add styles of existing rules
- Delete styles of existing rules
- Enable style
- Disable style
- Stylesheets:
- Add local or external stylesheets (needs file I/O for creating external sheets)
- Delete stylesheets
- Enable stylesheets
- Disable stylesheets
use case : set background color
- needs to know most specific rule WITH that property
-
*/
var stylesController = exports.StylesController = Montage.create(Component, {
///// Initialize after the active document has been set, and
///// bind the document to prop w/ setter. The setter calls to find
///// the stage and default css files.
handleAppLoaded : {
value: function() {
///// Bind app's activeDocument property to
///// styles controller's _activeDocument property
Object.defineBinding(this, "activeDocument", {
boundObject: this.application.ninja,
boundObjectPropertyPath: "currentDocument",
oneway: true
});
}
},
deserializedFromTemplate : {
value: function() {
this.eventManager.addEventListener( "appLoaded", this, false);
},
enumerable : false
},
///// Active document gets automatically set when the
///// document controller changes it
_activeDocument : {
value : null,
enumerable : false
},
activeDocument : {
get : function() {
return this._activeDocument;
},
set : function(document) {
///// If the document is null set default stylesheets to null
if(!document) {
return false;
}
///// setting document via binding
this._activeDocument = document;
///// Stage stylesheet should always be found
this._stageStylesheet = this.getSheetFromElement(this.CONST.STAGE_SHEET_ID);
// Returns null if sheet not found (as in non-ninja projects)
// Setter will handle null case
this.defaultStylesheet = this.getSheetFromElement(this.CONST.DEFAULT_SHEET_ID);
this.userStyleSheets = nj.toArray(document._document.styleSheets).filter(function(sheet) {
return sheet !== this._stageStylesheet;
}, this);
NJevent('styleSheetsReady', this);
},
enumerable : false
},
userStyleSheets : {
value : null
},
_stageStylesheet : {
value : null
},
_defaultStylesheet : {
value : null
},
defaultStylesheet : {
get : function() {
return this._defaultStylesheet;
},
set : function(sheet) {
if(sheet) {
this._defaultStylesheet = sheet;
} else {
///// Use the last stylesheet in the document as the default
var sheets = this._activeDocument._document.styleSheets,
lastIndex = sheets.length-1;
///// If the only sheet is the stage stylesheet, this will be true
///// in which case, we want to create a stylesheet to hold the
///// user's style rules
if(sheets[lastIndex] === this._stageStyleSheet) {
this._defaultStylesheet = this.createStylesheet('nj-default');
} else {
this._defaultStylesheet = sheets[lastIndex];
}
}
}
},
/* ----------------- Rule methods ----------------- */
///// Add Rule
///// Passed in rule will be appended to the default stylesheet
///// The rule can be in the form of a string (one argument), or
///// the selector string and declaration string (two arguments), or
///// the selector string and a declaration object.
///// Optionally pass in the rule index (defaults to end of sheet)
/*
Signature 1 :
addRule( "#div1", "color:blue; width:100px;", 3)
[str] [str] [num]
Signature 2 (w/ styles object literal):
addRule( "#div1", { color:"blue", width:"100px" }, 3)
[str] [obj] [num]
Signature 3 (w/ full rule as one string) :
addRule( "#div1 { color:blue; width:100px; }", 3)
[str] [num]
*/
addRule : {
value : function(selector, declaration, stylesheet, index) {
//console.log("Add rule");
var rulesLength = this._defaultStylesheet.rules.length,
argType = (typeof declaration),
ruleText = selector,
stylesheet = stylesheet || this._defaultStylesheet,
property, rule;
index = index || (argType === 'number') ? declaration : rulesLength;
if(argType === 'string') {
ruleText += '{' + declaration + '}';
} else if(argType === 'object') {
ruleText += '{' + this.cssFromObject(declaration) + '}';
}
stylesheet.insertRule(ruleText, index);
this.styleSheetModified(stylesheet);
rule = stylesheet.rules[index];
///// attach specificity to rule object
///// if rule is css keyframes, return rule and don't attach specificity
if (rule instanceof WebKitCSSKeyframesRule) {
return rule;
}
rule[this.CONST.SPECIFICITY_KEY] = this.getSpecificity(rule.selectorText);
///// return the rule we just inserted
return rule;
}
},
///// Create Override Rule
///// Takes a given rule and creates a rule with a selector of equal
///// or greater specificity, and inserts it after the original rule
///// This function will use a class to create the overriding selector,
///// and the class will have to be applied to the element in order for
///// the rule to stick
///// Returns an object containing this classname and the rule itself
createOverrideRule : {
value : function(ruleToOverride, element) {
///// Locally-scoped function to de-clutter variable declarations
function getSelector(el, rule) {
return this._getMostSpecificSelectorForElement(el, rule[this.CONST.SPECIFICITY_KEY]).selector;
}
var selectorToOverride = getSelector.bind(this)(element, ruleToOverride),
overrideData, rule;
///// Get the overriding selector and className
overrideData = this.createOverrideSelector(selectorToOverride, element.nodeName);
///// Create new rule with selector and insert it after the rule we're overriding
rule = this.addRule(overrideData.selector + ' { }', this.getRuleIndex(ruleToOverride)+1);
return {
className : overrideData.className,
rule : rule
};
}
},
createOverrideSelector : {
value: function(selectorToOverride, classPrefix, className) {
var tokens = selectorToOverride.split(/\s/),
newClass = className || this.generateClassName(classPrefix, true),
lastToken, pseudoSplit, base, pseudo, newToken, newSelector;
///// Creating an overriding selector by replacing the last
///// class, attribute or type selector in passed-in rule's selector
///// Grab the last token
lastToken = tokens[tokens.length-1];
pseudoSplit = lastToken.split(':');
///// The last token can have pseudo class. Let's preserve it
base = pseudoSplit[0];
pseudo = (pseudoSplit[1]) ? ':'+pseudoSplit[1] : '';
///// Now, all we want to do is replace the last token with a
///// generated class name, except if the last token is an ID selector,
///// in which case we append the generated class name to the ID selector
if(base.indexOf('#') !== -1) {
newToken = base + '.' + newClass + pseudo;
} else {
///// Replace last class or attribute selector
///// Get everything right before the last class or attribute selector
///// to support compound selector values: (i.e. .firstClass.secondClass)
newToken = base.substring(0, Math.max(base.lastIndexOf('.'), base.lastIndexOf('[')));
///// Append the generated class
newToken += '.' + newClass + pseudo;
}
///// Now we can build the new selector by replacing the last token
tokens[tokens.length-1] = newToken;
newSelector = tokens.join(' ');
return {
className : newClass,
selector : newSelector
};
}
},
///// Delete Rule
///// Deletes the passed-in rule from its stylesheet
///// Argument can be the index of the rule, or the rule itself
///// If the index is passed, the sheet must be passed in
deleteRule : {
value : function(rule, sheet) {
var index;
if(typeof rule === 'number') {
///// 1st arg is the index
index = rule;
} else {
///// derive the index of the rule
index = this.getRuleIndex(rule);
///// the sheet is a property of the rule
sheet = rule.parentStyleSheet;
}
if(index > -1) {
sheet.deleteRule(index);
}
this.styleSheetModified(sheet);
return index;
}
},
///// Get Dominant Rule For Style
///// Given an element, this method returns the dominant rule
///// for the particular style property.
///// Optionally, it returns an override object if no single-target
///// rule is found.
///// An override object consists of a rule to override, and a
///// flag for using !important or not
getDominantRuleForElement : {
value : function(element, property, returnOverrideObject, useStageStyleSheet) {
var matchedRules = this.getMatchingRules(element, true, useStageStyleSheet),
doc = element.ownerDocument,
useImportant = false,
inLineStyleRule, rulesWithProperty, importantRules, dominantRule;
///// First, since an element's style attribute is a CSSStyleDeclaration
///// and not a CSSStyleRule, we want to create an imitation rule object
///// to act like any returned by getMatchedCSSRules
inLineStyleRule = {
isInlineStyle : true,
style : element.style
};
///// Now splice it into the matched rules
///// By inserting the inline style at the beginning,
///// we keep the correct order of specificity
matchedRules.splice(0, 0, inLineStyleRule);
///// Now, let's see which of these rules defines the property
///// or shorthand
rulesWithProperty = matchedRules.filter(function(rule) {
return this.hasProperty(rule, property, true);
}, this);
//debugger;
/* POST-FILTERING for property
At this point, we have (in order of selector specificity) either
1) 1 or more rules with property
- Get subset of rules with "!important" flag on property
- Of these, we choose the first in the list (highest specificity, ever! jk)
- Does it affect multiple elements? If yes, create override
- None have "!important" flag - use initial list of rules with property
- Get
2) No rules with the property (i.e., no chance of property collision)
- use the original rule list to find the most specific, single-target rule
- start with the highest specificity to minimize calls to querySelectorAll()
- if there is no single-target rule, create a new rule to apply this style
*/
///// Is the property defined anywhere?
if(rulesWithProperty.length > 0) {
///// Ok, we've got rule(s) with the property.
///// Let's look for the properties with !important
importantRules = rulesWithProperty.filter(function(rule) {
return !!rule.style.getPropertyPriority(property);
});
if(importantRules.length) {
useImportant = true;
//dominantRule = this._getFirstSingleTargetRule(importantRules, doc);
dominantRule = importantRules[0];
} else {
//dominantRule = this._getFirstSingleTargetRule(rulesWithProperty, doc);
dominantRule = rulesWithProperty[0];
}
} else { // no rules with property
///// In this case, we don't want to use the inline style
///// Important flag is irrelevant because the style property isn't defined
//dominantRule = this._getFirstSingleTargetRule(matchedRules.slice(1), doc);
return this._getFirstSingleTargetRule(matchedRules.slice(1), doc);
}
if(this.matchesMultipleElements(dominantRule, doc) && returnOverrideObject) {
///// Oh, darn, we didn't find a single-target rule!
///// We can return the required data to create an override
///// rule to the calling method
return {
useImportant : useImportant,
ruleToOverride : dominantRule
};
}
return dominantRule;
}
},
///// Get Dominant Rule For Group
///// Given an array of elements, this function will return the dominant rule
///// for a given property. If no one rule exists which will allow for the
///// style to apply to the element, an override object will be returned,
///// from which an overriding rule can be created.
getDominantRuleForGroup : {
value : function(elements, property, forceOverride) {
var selectorsToOverride = [],
commonRules, dominantRules, useImportant;
///// Get the common rules for all elements
commonRules = this.getCommonRules(elements);
///// Find which rules target ONLY the elements passed in
///// I.E. passed-in elements are not a sub-selection of rule's target
commonRules = commonRules.filter(function(rule) {
return this.matchesElementsExclusively(rule, elements);
}, this);
///// OK, now if we still have common rules, we must determine if
///// this is the appropriate rule for to apply the style property to each
///// element.
///// This means we have to determine the dominant rules for each element for
///// the style property in question, and ensure that the common rule has
///// higher specificity, and if not, create an overriding rule.
elements.forEach(function(el) {
var dominantRule = this.getDominantRuleForElement(el, property), selector;
if(dominantRule && this.hasProperty(dominantRule, property, true)) {
///// ok, we've got the dominant rule with the property
///// for this rule, we only care about the selector
///// which has the highest specificity and targets the element
///// (and, inline styles don't have selectors, so )
if(!dominantRule.isInlineStyle) {
selector = this._getMostSpecificSelectorForElement(el, dominantRule[this.CONST.SPECIFICITY_KEY]);
selectorsToOverride.push(selector);
}
///// TODO: write hasImportant method which can also check for shorthand properties
if(!useImportant && (dominantRule.style.getPropertyPriority(property) || dominantRule.isInlineStyle)) {
useImportant = true;
}
}
}, this);
///// if any of the selectors
if(commonRules.length) {
selectorsToOverride.filter(function(selectorObj) {
}, this);
}
}
},
///// Disable Rule
///// Disables a rule by giving it a known garbage selector
disableRule : {
value : function(rule, sheet) {
rule = (typeof rule === 'number') ? sheet.rules[rule] : rule;
rule.selectorText += this.CONST.GARBAGE_SELECTOR;
return rule;
}
},
///// Enable Rule
///// Enables a rule by removing the known garbage selector
enableRule : {
value : function(rule, sheet) {
rule = (typeof rule === 'number') ? sheet.rules[rule] : rule;
///// remove any occurances of the garbage selector
rule.selectorText.replace(this.CONST.GARBAGE_SELECTOR, '');
return rule;
}
},
///// Has Property
///// Checks to see if a rule has the property defined in
///// its declaration.
///// Optionally checks for shortand property
hasProperty : {
value: function(rule, property, checkForShorthand) {
var properties = [property],
shorthands = cssShorthandMap[property];
///// If shorthand properties are defined, add to the array
///// of which properties to check for
if(shorthands) {
properties.concat(shorthands);
}
///// return true if any property exists in rule
return properties.some(function(prop) {
return !!rule.style.getPropertyValue(prop);
}, this);
}
},
///// Matches Multiple Elements
///// Checks to see if the rule affects multiple elements
matchesMultipleElements : {
value: function(rule, document) {
if(rule.isInlineStyle) {
return true;
}
var doc = document || this._activeDocument;
///// TODO: somehow cache the number of elements affected
///// by the rule, because querySelectorAll() is expensive
return !!(doc.querySelectorAll(rule.selectorText).length > 1);
}
},
///// Matches Elements Exclusively
///// Checks to see if passed-in rule targets ONLY the elements
///// passed in.
matchesElementsExclusively : {
value : function(rule, elements) {
var doc = elements[0].ownerDocument;
///// find all targets of rule's selector,
///// if any target is not in passed-in array, return false
return nj.toArray(doc.querySelectorAll(rule.selectorText)).every(function(el) {
return elements.indexOf(el) !== -1;
});
}
},
///// Set Rule Selector
///// Allows user to change the selector of given rule
///// while attaching new specificity value to rule object
setRuleSelector : {
value : function(rule, selector) {
rule.selectorText = selector;
rule[this.CONST.SPECIFICITY_KEY] = this.getSpecificity(selector);
this.styleSheetModified(rule.parentStyleSheet);
return rule;
}
},
///// Get Rule Index
///// Returns the index of the passed-in rule.
///// Returns -1 if not found.
///// A rule's index is useful to know for deleting, inserting
///// and determining rule precedence
getRuleIndex : {
value : function(rule) {
var rules = nj.toArray(rule.parentStyleSheet.rules),
i;
return rules.indexOf(rule);
}
},
///// Get All Matching Rules
///// Returns an array of css rules for an element
///// Optionally sorted by specificity, and can omit pseudo elements
getMatchingRules : {
value: function(element, omitPseudos, useStageStyleSheet) {
var pseudos = [null],
rules = [],
win = element.ownerDocument.defaultView,
self = this;
if(!omitPseudos) {
pseudos.concat(['link', 'visited', 'active', 'hover', 'focus', 'first-letter',
'first-line', 'first-child', 'before', 'after', 'lang', 'target']);
}
try {
pseudos.forEach(function(pseudo) {
rules = rules.concat(nj.toArray(win.getMatchedCSSRules(element, pseudo)).filter(function(rule) {
//// useStageStyleSheet flag indicates whether to only return rules from the stylesheet,
//// or only use rules for other stylesheets
var sheetId = (rule.parentStyleSheet) ? rule.parentStyleSheet.ownerNode.id : null,
isStageStyleSheet = sheetId === this.CONST.STAGE_SHEET_ID;
///// filter out (return false) depending on flag
if(useStageStyleSheet && !isStageStyleSheet) { return false; }
if(!useStageStyleSheet && isStageStyleSheet) { return false; }
///// Non-filter code - just assigning specificity to the rule
if(!rule[this.CONST.SPECIFICITY_KEY]) {
rule[this.CONST.SPECIFICITY_KEY] = this.getSpecificity(rule.selectorText);
}
return true;
}, this));
}, this);
} catch(ERROR) {
console.warn('StylesController::getMatchingRules - Un-attached element queried.');
}
///// Function for sorting by specificity values
function sorter(ruleA, ruleB) {
var a, b, order, sheetAIndex, sheetBIndex, ruleAIndex, ruleBIndex;
///// get the specificity arrays
a = this._getMostSpecificSelectorForElement(element, ruleA[this.CONST.SPECIFICITY_KEY]);
b = this._getMostSpecificSelectorForElement(element, ruleB[this.CONST.SPECIFICITY_KEY]);
///// use the most specific selectors (first in arrays),
///// determine whether the selector applies to the element
///// if not, move on to
order = this.compareSpecificity(a.specificity, b.specificity);
if(order === 0) {
//debugger;
/// Tie. Sway one way or other based on stylesheet/rule order
sheetAIndex = nj.toArray(win.document.styleSheets).indexOf(ruleA.parentStyleSheet);
sheetBIndex = nj.toArray(win.document.styleSheets).indexOf(ruleB.parentStyleSheet);
/// If tied again (same sheet), determine which is further down in the sheet
if(sheetAIndex === sheetBIndex) {
ruleAIndex = this.getRuleIndex(ruleA); ruleBIndex = this.getRuleIndex(ruleB);
return ruleAIndex < ruleBIndex ? 1 : (ruleAIndex > ruleBIndex) ? -1 : 0;
}
}
return order;
}
rules.sort(sorter.bind(this));
return rules;
}
},
///// Get Common Rules
///// Returns an array of rules that are common to all the elements
///// in passed-in element array.
getCommonRules : {
value : function(elements) {
var itemIndex = -1,
currentEl, currentRuleList, nextEl, nextRuleList, commonRules;
do {
///// Get current element's matched rules
currentEl = elements[++itemIndex];
currentRuleList = this.getMatchingRules(currentEl, true);
///// Get next element's matched rules
nextEl = elements[itemIndex+1];
nextRuleList = this.getMatchingRules(nextEl, true);
///// use filter to see if any rules exist in the next set of rules
commonRules = currentRuleList.filter(function(rule) {
return nextRuleList.indexOf(rule) !== -1;
});
} while (itemIndex+2 < elements.length && commonRules.length > 0);
return commonRules;
}
},
///// Get Most Specific Selector For Element
///// Given a selector+specificity array, find the most specific
///// selector for the passed-in element
_getMostSpecificSelectorForElement : {
value : function(element, specArr) {
if(specArr.length === 1) {
return specArr[0];
}
var matchingElements, i;
for(i = 0; i < specArr.length; i++) {
matchingElements = element.ownerDocument.querySelectorAll(specArr[i].selector);
if(nj.toArray(matchingElements).indexOf(element) !== -1) {
return specArr[i];
}
}
///// reached end of specificity array with no match - should be impossible
console.error('StylesController::_getMostSpecificSelectorForElement - no matching selectors in specificity array.');
}
},
///// Get First Single Target Rule
///// Loops through the array of rules sequentially, returning the first
///// single-target rule (i.e. first rule which affects only one element)
///// Returns null if no single target rule is found
_getFirstSingleTargetRule : {
value : function(rules, document) {
var i;
for(i = 0; i < rules.length; i++) {
if(!this.matchesMultipleElements(rules[i], document)) {
return rules[i];
}
}
return null;
}
},
///// Compare Specificity
///// Takes two specificity objects and returns:
///// -1 if first is more specific than second
///// +1 if second is more speficic than first
///// 0 if equal in specificity
compareSpecificity : {
value : function(a, b) {
var specA = a,
specB = b,
order;
[this.CONST.SPEC_ID_KEY,this.CONST.SPEC_CLASS_KEY,this.CONST.SPEC_TYPE_KEY].some(function(t) {
order = specA[t] < specB[t] ? 1 : (specA[t] > specB[t]) ? -1 : 0;
return order !== 0;
}, this);
return order;
}
},
///// Get specificity
///// Creates array of objects, ordered by specificity for each
///// selector in the passed-in selectorText.
getSpecificity : {
value : function(selector) {
var arr = selector.split(',').map(function(sel) {
return {
selector : sel,
specificity : this.calculateSpecificity(sel)
};
}, this);
///// now sort by specificity
return arr.sort(function(a, b) {
return this.compareSpecificity(a.specificity, b.specificity);
}.bind(this));
}
},
///// Calculate specificity
///// Returns the specificity value of passed-in selector
///// WARNING: Do not pass in grouped selectors!
///// Helpful for determining precedence of style rules
///// Calculation javascript code courtesy of Graham Bradley:
///// http://gbradley.com/2009/10/02/css-specificity-in-javascript
///// Used with author's permission
calculateSpecificity : {
value : function(selector) {
var s = selector.replace(/\([^\)]+\)/,''),
obj = {};
///// function for counting matches for different
///// selector patterns
function m(reg) {
var matches = s.match(reg);
return matches ? matches.length : 0;
}
obj[this.CONST.SPEC_ID_KEY] = m(/#[\d\w-_]+/g); /// match id selector
obj[this.CONST.SPEC_CLASS_KEY] = m(/[\.:\[][^\.:\[+>]+/g); /// match class selector
obj[this.CONST.SPEC_TYPE_KEY] = m(/(^|[\s\+>])\w+/g); /// match tag selector
return obj;
}
},
/* ----------------- Style methods ----------------- */
///// Add style
///// Set style property and value on provided rule
///// with optional priority (!important)
///// Returns the browser's value of passed-in property
setStyle : {
value: function(rule, property, value, useImportant) {
var dec = rule.style, priority;
///// Remove property for passive validation (sets it to null)
dec.removeProperty(property);
priority = (useImportant) ? this.IMPORTANT_FLAG : null;
///// Use CSS declaration's setProperty()
///// method to apply/test the new value
dec.setProperty(property, value, priority);
this.styleSheetModified(rule.parentStyleSheet);
///// Return browser value for value we just set
return dec.getPropertyValue(property);
}
},
///// Add styles
///// Set multiple styles on provided rule
///// Returns a collection of browser values for the
///// passed-in properties
setStyles : {
value: function(rule, styles, useImportant) {
var browserValues = {}, property, value;
for(property in styles) {
if(styles.hasOwnProperty(property)) {
value = styles[property];
browserValues[property] = this.setStyle(rule, property, value, useImportant);
}
}
return browserValues;
}
},
///// Set Keyframe Style
///// For a given CSSKeyframesRule, we may add a style to the keyframe at
///// given index.
setKeyframeStyle : {
value : function(rule, keyframeIndex, property, value, useImportant) {
return this.setStyle(rule.cssRules[keyframeIndex], property, value, useImportant);
}
},
///// Set Keyframe Styles
///// For a given CSSKeyframesRule, we may add styles to the keyframe at
///// given index.
setKeyframeStyle : {
value : function(rule, keyframeIndex, property, value, useImportant) {
return this.setStyles(rule.cssRules[keyframeIndex], property, value, useImportant);
}
},
insertKeyframe : {
value : function() {
}
},
///// Get Animation Rule With Name
///// Returns the CSSKeyframesRule with given name
getAnimationRuleWithName : {
value: function(name, document) {
var doc = document || this._activeDocument._document,
animRules = this.getDocumentAnimationRules(doc),
rule, i;
for(i = 0; i < animRules.length; i++) {
rule = animRules[i];
if(rule.name === name) {
return rule;
}
}
return;
}
},
///// Get Document Animation Rules
///// Returns all CSSKeyframesRules in active document, or in
///// optionally passed-in document
///// If none are found, returns an empty array
getDocumentAnimationRules : {
value: function(document) {
var sheets = (document) ? document.styleSheets : this._activeDocument._document.styleSheets,
rules = [];
nj.toArray(sheets).forEach(function(sheet) {
rules = rules.concat(this.getStyleSheetAnimationRules(sheet));
}, this);
return rules;
}
},
///// Get Style Sheet Animation Rules
///// Returns all CSSKeyframesRules from the given stylesheet
///// If none are found, returns an empty array
getStyleSheetAnimationRules : {
value: function(sheet) {
var rules = [];
if(sheet.rules) {
rules = rules.concat(nj.toArray(sheet.rules).filter(function(rule) {
return rule instanceof WebKitCSSKeyframesRule;
}));
}
return rules;
}
},
///// Delete style
///// Removes the property from the style declaration/rule
///// Returns the rule
deleteStyle : {
value : function(rule, property) {
this.styleSheetModified(rule.parentStyleSheet);
rule.style.removeProperty(property);
return rule;
}
},
///// Delete styles
///// Removes all style properties in passed-in array or object
///// Returns the rule
deleteStyles : {
value : function(rule, properties) {
if(properties.constructor !== Array && typeof properties === 'object') {
properties = Object.keys(properties);
}
properties.forEach(function(prop) {
this.deleteStyle(rule, prop);
}, this);
return rule;
}
},
/* ----------------- Element methods ----------------- */
///// Set Element Style
///// Applies style to element via dominant rule logic:
///// We find the most specific rule that has the style property (or it's shorthand)
///// and does not affect multiple elements (we don't want to change that style on
///// all elements sharing a class, for example).
///// Here there are a few possibilities:
///// 1) We find the most specific, single-target matching rule with the property defined
///// - Great! Set the style on it.
///// 2) The style property is defined in a multi-target rule
///// - if there's an single-target rule with greater specificity, use it instead
///// - else, create rule using the multi-target rule's selector, but replace the
///// - last piece of the selector (.class, or 'div') with a new class, which should
///// - be appended to the element. Use this new rule to add the style property
///// - NOTE: Doing this ensures the new rule has equal or greater specificity,
///// - and as long as it is inserted after the original rule, we're good.
///// 3) The style property is not defined anywhere
///// - use most specific, single-target rule
///// - else (this means no single-target rule matches element), create a class
///// - for this element
///// For Undo/Redo: should return object detailing what actually happened
///// during the application of the style (created rule or amended rule)
setElementStyle : {
value : function(element, property, value, isStageElement) {
var doc = element.ownerDocument,
useImportant = false,
cache = this._getCachedRuleForProperty(element, property),
dominantRule, override, className, browserValue;
if(cache) {
///// We've cached the rule for this property!
//console.log('Styles Controller :: setElementStyle - We found the cached rule!');
dominantRule = cache;
} else {
///// Use Dominant Rule logic to find the right place to add the style
///// Pass "true" to method to return an override object, which
///// has the rule to override, and whether the !important flag is needed
dominantRule = this.getDominantRuleForElement(element, property, true, isStageElement);
}
///// Did we find a dominant rule?
if(!dominantRule) {
///// No. This means there was no rule with this property, and no
///// single-target rule we can use to add the style to.
///// There's is no chance of colliding with another rule, so we
///// create a new rule (class), and append it to the element
className = this.generateClassName(element.nodeName);
dominantRule = this.addRule('.'+className + '{}');
this.addClass(element, className);
} else if(dominantRule.ruleToOverride) {
///// Do we have to override a rule?
////// Yes. The override object has the rule we need to override
override = this.createOverrideRule(dominantRule.ruleToOverride, element);
useImportant = dominantRule.useImportant;
dominantRule = override.rule;
this.addClass(element, override.className);
}
///// set style method will return the value used by the browser after parsing
browserValue = this.setStyle(dominantRule, property, value, useImportant);
///// Only cache the dominant rule if the style value was valid, and not already cached
if(browserValue && !cache) {
this._setCachedRuleForProperty(element, property, dominantRule);
}
return browserValue;
}
},
setGroupStyle : {
value : function(elements, property, value) {
var doc = elements[0].ownerDocument,
useImportant = false,
dominantRules;
dominantRules = elements.map(function(el) {
return this.getDominantRuleForElement(el, property, true);
}, this);
}
},
///// Set Element Styles
///// Applies passed-in styles to the element via dominant rule logic
///// Styles must be in object format with the property as the key
setElementStyles : {
value : function(element, styles, isStageElement) {
for(var property in styles) {
if(styles.hasOwnProperty(property)) {
this.setElementStyle(element, property, styles[property], isStageElement);
}
}
}
},
setGroupStyles : {
value : function(elements, styles) {
var properties = Object.keys(styles),
newClass = this.generateClassName(null, true),
selectors;
///// TODO: move this: Locally-scoped function to de-clutter variable declarations
function getSelector(el, rule) {
return this._getMostSpecificSelectorForElement(el, rule[this.CONST.SPECIFICITY_KEY]).selector;
}
selectors = elements.map(function(el) {
///// for each element, we want to find the most specific selector
var matchingRules = this.getMatchingRules(el, true);
this.addClass(el, newClass);
if(matchingRules.length === 0) {
return null;
}
var mostSpecificRule = matchingRules[0], // TODO: iterate over properties to find most specific
selectorToOverride = getSelector.bind(this)(el, mostSpecificRule),
override = this.createOverrideSelector(selectorToOverride, null, newClass);
return override.selector;
}, this);
selectors.filter(function(item) {
return item !== null;
});
this.addRule(selectors.join(', '), styles);
}
},
///// Get Element Style
///// Gets the style value that is currently applied to the element
///// Uses Dominant Rule logic to determine the rule that has the property
///// and if not found, can optionally return the computed style instead of
///// null.
getElementStyle : {
value : function(element, property, fallbackOnComputed, isStageElement) {
var cache = this._getCachedRuleForProperty(element, property),
dominantRule = cache || this.getDominantRuleForElement(element, property, false, isStageElement),
value = (dominantRule) ? dominantRule.style.getPropertyValue(property) : null;
//console.log('Getting element style for: "' + property + '"');
if(value) {
///// if the dominant rule with the property defined was found, cache the rule (if not already cached)
if(!cache) {
this._setCachedRuleForProperty(element, property, dominantRule);
}
} else if(fallbackOnComputed) {
///// The dominant rule might not have the style property defined - why?
///// If no rules have the property defined, we can use the
///// most-specific single-target rule as the dominant rule (for setting styles)
return element.ownerDocument.defaultView.getComputedStyle(element).getPropertyValue(property);
}
return value;
}
},
///// Get Element Animation Rule
///// Returns the CSSKeyframesRule applied to an element
getElementAnimationRule : {
value: function(element) {
var animationName = this.getElementStyle(element, '-webkit-animation-name');
if(!animationName) {
return null;
}
return this.getAnimationRuleWithName(animationName);
}
},
///// Create Rule From Inline Style
///// Creates a rule for an inline style with a specified, or partially random selector.
createRuleFromInlineStyle : {
value : function(element, selector, makeDominant) {
var declaration = element.style, rule;
if(makeDominant) {
///// iterate through declaration and set Element Style
nj.toArray(declaration).forEach(function(prop) {
this.setElementStyle(element, prop, declaration.getPropertyValue(prop));
}, this);
} else {
rule = this.addRule(selector || '.'+this.generateClassName(element.nodeName), element.getAttribute('style'));
}
return rule;
}
},
///// Add Class
///// Adds class to element
addClass : {
value : function(element, className) {
element.classList.add(className);
}
},
/* ----------------- Stylesheet methods ----------------- */
///// Create a stylesheet via style tag in active document, or
///// optionally passed-in document
createStylesheet : {
value: function(id, document) {
var doc = document || this._activeDocument._document,
sheetElement, sheet;
sheetElement = nj.make('style', {
type : 'text/css',
rel : 'stylesheet',
id : id || "",
media : 'screen',
title : 'Temp'
});
doc.head.appendChild(sheetElement);
sheet = this.getSheetFromElement(sheetElement, doc);
this.styleSheetModified(sheet);
return sheet;
}
},
///// Gets the stylesheet object associated with passed-in
///// element or element id, with option context (document)
///// (For and