/* <copyright> This file contains proprietary software owned by Motorola Mobility, Inc.<br/> No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.<br/> (c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved. </copyright> */ 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); //debugger; }, enumerable : false }, _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']); } 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); ///// 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 = win.document.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() { } }, ///// 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; } }, ///// 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 <link> and <style> tags) getSheetFromElement : { value : function(element, context) { var doc = context || this._activeDocument._document, el = (typeof element === 'string') ? nj.$(element, doc) : element; if(el && el.sheet) { return el.sheet; } return; } }, ///// Style Sheet Modified ///// Method to call whenever a stylesheet change is made ///// Dispatches an event, and keeps list of dirty style sheets styleSheetModified : { value: function(sheet, eventData) { var sheetSearch = this.dirtyStyleSheets.filter(function(sheetObj) { return sheetObj.stylesheet === sheet; }); ///// If the sheet doesn't already exist in the list of modified ///// sheets, dispatch dirty event and add the sheet to the list if(sheetSearch.length === 0) { NJevent('styleSheetDirty', eventData); this.dirtyStyleSheets.push({ document : sheet.ownerNode.ownerDocument, stylesheet : sheet }); } } }, ///// Dirty Style Sheets ///// List of modified style sheets dirtyStyleSheets : { value : [] }, ///// Clear Dirty Style Sheets ///// Refreshes the list of dirty style sheets ///// If optional document object is supplied, only the styles sheets ///// of a particular document are cleared ///// Useful to call after a "Save" or "Save All" event clearDirtyStyleSheets : { value: function(doc) { if(!doc) { this.dirtyStyleSheets = null; this.dirtyStyleSheets = []; } } }, /* ----------------- Utils ------------------- */ _generateRandomAlphaNumeric : { value : function(length) { var available = 'abcdefghijklmnopqrstuvwxyz0123456789', len = length || 4, chars = [], i; for(i = 0; i<len; i++) { chars[i] = available[Math.floor(Math.random() * available.length)]; } return chars.join(''); } }, ///// Generate Class Name ///// Returns class name using optional passed-in prefix or default class name ///// prefix, and randomly created string ///// If creating a class name for a group, pass in true to second argument ///// and a different default class prefix will be used (better semantics) generateClassName : { value : function(prefix, forGroup) { var className; if(prefix) { className = prefix; } else { className = (forGroup) ? this.CONST.GENERATED_GROUP_CLASS : this.CONST.GENERATED_CLASS; } return className.toLowerCase() + '-' + this._generateRandomAlphaNumeric(); } }, ///// CSS From Object ///// Returns css text from object with key/value pairs ///// representing css styles cssFromObject : { value : function(obj) { var cssText = ''; ///// For each key/value pair, create css text for(var prop in obj) { cssText += prop + ':' + obj[prop] + ';'; } return cssText; } }, /* ----------------- Element model (rule cache) related methods ----------------- */ ///// Get Cached Rule For Property ///// Returns the cached rule for the style property _getCachedRuleForProperty : { value : function(el, property) { if(!el.elementModel) { return false; } /// return false if there is no element model return el.elementModel[property]; }, enumerable : false }, ///// Set Cached Rule For Property ///// Sets the cached rule for the style property ///// Returns false if the element model doesn't exist _setCachedRuleForProperty : { value: function(el, property, rule) { if(!el.elementModel) { return false; } /// return null if there is no element model this._cacheHistory.push({ rule: rule, element: el, property: property }); el.elementModel[property] = rule; return true; }, enumerable: false }, ///// Cache History ///// Keeps a log of all cached rules on elements ///// This array can be used to nullify cached rules when ///// CSS rule changes happen _cacheHistory : { value: [] }, ///// Clear Cache ///// Nullifies all cached CSS rules on the element provided, or ///// on the entire cache history _clearCache: { value: function(element) { var itemsToNullify = this._cacheHistory, itemsToRemove = [], i; ///// If clearing the cache for an element, filter by element ///// and keep track of indices to remove from cache if(element) { itemsToNullify = itemsToNullify.filter(function(item, index) { if(item.element === element) { itemsToRemove.push(index); return true; } return false; }); } itemsToNullify.forEach(function(item) { //var identifier = item.element.nodeName; //identifier += '#'+item.element.id || '.'+item.element.className; //console.log("clearing cache for \"" + item.property +"\" and element \"" + identifier+ ""); if(item.element.elementModel) { item.element.elementModel[item.property] = null; } }); ///// Remove the nullified items from the cache ///// Start at the end to not mess up index references for(i = itemsToRemove.length-1; i >= 0; i--) { this._cacheHistory.splice(itemsToRemove[i], 1); } if(!element) { this._cacheHistory = null; this._cacheHistory = []; } } }, _removeCachedRuleForProperty : { value: function() { } }, /* ----------------- Constants ----------------- */ CONST : { value : { STAGE_SHEET_ID : 'nj-stage-stylesheet', DEFAULT_SHEET_ID : 'nj-default-stylesheet', GARBAGE_SELECTOR : 'ninja-garbage-selector', SPECIFICITY_KEY : 'specificity', SPEC_ID_KEY : 'id', SPEC_CLASS_KEY : 'class', SPEC_TYPE_KEY : 'type', IMPORTANT_FLAG : '!important', GENERATED_CLASS : 'gen', GENERATED_GROUP_CLASS : 'group' }, enumerable : false }, /* ----------------- Convenience functions for manual verifications ----------------- */ test : { value : { getStyleTest : function() { var properties = ['background-position', 'width', 'height']; var el = stylesController.activeDocument._document.getElementById('Div_1'); properties.forEach(function(prop) { console.log('Getting value for "' + prop + '": ' + stylesController.getElementStyle(el, prop, true)); }, this); }, addRulesTest : function() { var rules = [ 'div#Div_1 { background-color: black }', '#UserContent div#Div_1 { background-color: blue }', '#UserContent #Div_1 { background-color: white }', 'div div#Div_1 { background-color: red }' ]; rules.forEach(function(rule) { stylesController.addRule(rule); }); }, getMatchingRulesTest : function() { var el = stylesController.activeDocument._document.getElementById('Div_1'), mRules; this.addRulesTest(); mRules = stylesController.getMatchingRules(el, true); mRules.forEach(function(rule, i) { console.log('Rule ' + i + ' selector: ' + rule.selectorText); }); }, setElementStyleTest : function() { ///// This test will get the initial background style of the element, ///// apply a new style using setElementStyle, and print out the new ///// value. var el = stylesController.activeDocument._document.getElementById('Div_1'), bg; console.log('----- Set Element Style Test -----'); this.addRulesTest(); bg = stylesController.getElementStyle(el, 'background-color'); console.log('Initial background color is : ' + bg); stylesController.setElementStyle(el, 'background-color', '#CCCCCC'); bg = stylesController.getElementStyle(el, 'background-color'); console.log('Final background color is : ' + bg); console.log('......Set Element Style Test[END].....'); }, setElementStyle2Test : function() { // first, drag two divs on stage // create more specific, multi-target rule var rules = ['#UserContent div { background-color: blue }']; rules.forEach(function(rule) { stylesController.addRule(rule); }); var el = stylesController.activeDocument._document.getElementById('Div_1'); stylesController.setElementStyle(el, 'color', 'red'); ///// the #Div_1 rule created by tag tool should have the color style }, setElementStyle3Test : function() { ///// First, draw a div onto the stage var el = stylesController.activeDocument._document.getElementById('Div_1'); //// now add a multi-target rule overriding the bg color var rules = [ '#UserContent div { background-color: blue }' ]; ///// draw another div onto the stage rules.forEach(function(rule) { stylesController.addRule(rule); }); stylesController.setElementStyle(el, 'background-color', 'red'); }, setGroupStyleTest : function() { ///// draw 2 divs on stage var el1 = stylesController.activeDocument._document.getElementById('Div_1'); var el2 = stylesController.activeDocument._document.getElementById('Div_2'); var dominantRule = stylesController.getDominantRuleForGroup([el1, el2], 'color'); }, setElementStylesTest : function() { ///// draw a div on stage var el = stylesController.activeDocument._document.getElementById('Div_1'); mRules = stylesController.getMatchingRules(el, true); mRules.forEach(function(rule) { console.log('Deleting Rule ' + i + ' selector: ' + rule.selectorText); stylesController.deleteRule(rule); }); stylesController.setElementStyles(el, { 'width':'100px', 'height':'100px', 'position':'absolute' }); }, createOverrideRuleTest : function() { ///// Draw div on stage console.log('----- Create Override Rule Test -----'); var el = stylesController.activeDocument._document.getElementById('Div_1'), rule = stylesController.addRule('#UserContent div { background-color: blue }'), override; console.log('Old rule\'s selector: ' + rule.selectorText); override = stylesController.createOverrideRule(rule, el); console.log('New rule\'s selector: ' + override.rule.selectorText); }, deleteRulesTest : function() { // drag one div on stage var el = stylesController.activeDocument._document.getElementById('Div_1'); this.addRulesTest(); mRules = stylesController.getMatchingRules(el, true); mRules.forEach(function(rule) { console.log('Deleting Rule ' + i + ' selector: ' + rule.selectorText); stylesController.deleteRule(rule); }); }, matchesElementsExclusivelyTest : function() { /// drag two divs on stage var rule = stylesController.addRule('#Div_1, #Div_3 { color:black; }'); var el1 = stylesController.activeDocument._document.getElementById('Div_1'); var el2 = stylesController.activeDocument._document.getElementById('Div_2'); console.log('Does rule match elements exclusively? ' + stylesController.matchesElementsExclusively(rule, [el1, el2])); } } } });