blob: 6fee0b03ef9ff1ac643e7534a2ad6a2f52b08f97 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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.
//
////////////////////////////////////////////////////////////////////////////////
package mx.utils
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.system.Capabilities;
import mx.core.IFlexModuleFactory;
import mx.core.mx_internal;
import mx.managers.ISystemManager;
import mx.managers.SystemManagerGlobals;
import mx.styles.CSSDimension;
import mx.styles.CSSOSVersion;
import mx.styles.IStyleManager2;
import mx.styles.StyleManager;
use namespace mx_internal;
[ExcludeClass]
/**
* @private
* Parser for CSS Media Query syntax. Not a full-fledged parser.
* Doesn't report syntax errors, assumes you have your attributes
* and identifiers spelled correctly, etc.
* Media query parser now supports os-version selectors such as X, X.Y or X.Y.Z
* Note that version with 2 or 3 parts must be quoted
Examples:
(os-platform: "ios") AND (min-os-version: 7)
(os-platform: "android") AND (min-os-version: "4.1.2")
non standard selectors:
-flex-device-diagonal
-flex-min-device-diagonal
*
* @langversion 3.0
* @playerversion Flash 10.2
* @playerversion AIR 2.6
* @productversion Flex 4.5
*/
public class MediaQueryParser extends EventDispatcher
{
/**
* @private
* Table of known media types
*
* @langversion 3.0
* @playerversion Flash 10.2
* @playerversion AIR 2.6
* @productversion Flex 4.5
*/
public static var platformMap:Object =
{
AND: "android",
IOS: "ios",
MAC: "macintosh",
WIN: "windows",
LNX: "linux",
QNX: "qnx"
}
/**
* @private
*/
private static var _instance:MediaQueryParser;
/**
* Single shared instance of the parser
*
* @langversion 3.0
* @playerversion Flash 10.2
* @playerversion AIR 2.6
* @productversion Flex 4.5
*/
public static function get instance():MediaQueryParser
{
return _instance;
}
/**
* @private
*/
public static function set instance(value:MediaQueryParser):void
{
if (!_instance)
_instance = value;
}
/**
* @private
* Constructor
*
* @langversion 3.0
* @playerversion Flash 10.2
* @playerversion AIR 2.6
* @productversion Flex 4.5
*/
public function MediaQueryParser(moduleFactory:IFlexModuleFactory = null)
{
applicationDpi = DensityUtil.getRuntimeDPI();
if (moduleFactory)
{
if (moduleFactory.info()["applicationDPI"] != null)
applicationDpi = moduleFactory.info()["applicationDPI"];
if (moduleFactory is ISystemManager){
sm = ISystemManager(moduleFactory);
if (sm.stage)
sm.stage.addEventListener(Event.RESIZE, stage_resizeHandler, false);
}
}
osPlatform = getPlatform();
osVersion = getOSVersion();
// compute device DPI
deviceDPI = Capabilities.screenDPI;
// compute width, height and diagonal
computeDeviceDimensions( );
}
/**
* Queries that were true
*/
mx_internal var goodQueries:Object = {};
/**
* Queries that were false
*/
mx_internal var badQueries:Object = {};
/**
* system manager for the MQP to compute device dimensions
*/
private var sm: ISystemManager;
/** flags are set if device-width / device height are used in any media query.
* This is an optimization, so that we know when it's necessary to regenerate styles
* */
private var usesDeviceWidth: Boolean = false ;
private var usesDeviceHeight: Boolean =false;
private var usesDeviceDiagonal: Boolean = false;
/**
* @private
* Main entry point.
*
* @param expression A syntactically correct CSS Media Query
* @returns true if valid for this media, false otherwise
*
* @langversion 3.0
* @playerversion Flash 10.2
* @playerversion AIR 2.6
* @productversion Flex 4.5
*/
public function parse(expression:String):Boolean
{
// remove whitespace
expression = StringUtil.trim(expression);
// degenerate expressions
if (expression == "") return true;
// known queries
if (goodQueries[expression]) return true;
if (badQueries[expression]) return false;
// force to lower case cuz case-insensitive
var originalExpression:String = expression;
expression = expression.toLowerCase();
//TODO : be smart and do not do a lowercase to do this test
if (expression == "all") return true;
// get a list of queries. If any pass then
// we're good
var mediaQueries:Array = expression.split(", ");
var n:int = mediaQueries.length;
for (var i:int = 0; i < n; i++)
{
var result:Boolean;
var mediaQuery:String = mediaQueries[i];
var notFlag:Boolean = false;
// eat only
if (mediaQuery.indexOf("only ") == 0)
mediaQuery = mediaQuery.substr(5);
// remember if this is a "not" expression
if (mediaQuery.indexOf("not ") == 0)
{
notFlag = true;
mediaQuery = mediaQuery.substr(4);
}
// get a list of the parts of the query.
// it should be media type, optionally
// followed by "and" followed by
// optional media feature expressions
var expressions:Array = tokenizeMediaQuery(mediaQuery);
var numExpressions:int = expressions.length;
if (expressions[0] == "all" || expressions[0] == type)
{
if (numExpressions == 1 && !notFlag)
{
goodQueries[originalExpression] = true;
return true;
}
// bail if "and" and no media features (invalid query)
if (numExpressions == 2) return false;
// kick off the type and "and"
expressions.shift();
expressions.shift();
// see if the media features match
result = evalExpressions(expressions);
// early exit if it returned true;
if ((result && !notFlag) || (!result && notFlag))
{
goodQueries[originalExpression] = true;
return true;
}
}
// if we didn't match on media type and we have a notFlag
// then we match
else if (notFlag)
{
goodQueries[originalExpression] = true;
return true;
}
}
badQueries[originalExpression] = true;
return false;
}
// break up the expression into pieces
private function tokenizeMediaQuery(mediaQuery:String):Array
{
var tokens:Array = [];
// if leading off with "(" then
// "all and" is implied
var pos:int = mediaQuery.indexOf("(");
if (pos == 0)
{
tokens.push("all");
tokens.push("and");
}
else if (pos == -1)
{
// no parens means the whole thing should
// be the media type
return [ mediaQuery ];
}
var parenLevel:int = 0;
var inComment:Boolean = false;
var n:int = mediaQuery.length;
var expression:Array = [];
// walk through each character looking for the pieces
for (var i:int = 0; i < n; i++)
{
var c:String = mediaQuery.charAt(i);
if (StringUtil.isWhitespace(c) && expression.length == 0)
{
// eat extra whitespace between tokens
continue;
}
else
{
// this piece should be the media type
if (c == '/' && i < n - 1 && mediaQuery.charAt(i + 1) == '*')
{
inComment = true;
i++;
continue;
}
if (inComment)
{
if (c == '*' && i < n - 1 && mediaQuery.charAt(i + 1) == '/')
{
inComment = false;
i++;
}
continue;
}
else if (c == "(") // Not sure whether these should be in the “else” here?
parenLevel++;
else if (c == ")")
parenLevel--;
else
{
expression.push(c);
}
// If we found whitespace and not in a paren, or just closed a paren,
// then that's the end of an expression
if (parenLevel == 0 && (StringUtil.isWhitespace(c) || (c == ")")))
{
if (c != ")")
expression.length--;
tokens.push(expression.join(""));
expression.length = 0; // reset
}
}
}
return tokens;
}
// take a media feature expression and evaluate it
private function evalExpressions(expressions:Array):Boolean
{
var n:int = expressions.length;
for (var i:int = 0; i < n; i++)
{
var expr:String = expressions[i];
// skip over "and"
if (expr == "and")
continue;
// break into two pieces
var parts:Array = expr.split(":");
var key: String = parts[0];
var min:Boolean = false;
var max:Boolean = false;
var flex: Boolean = false;
// process custom selectors
if (key.indexOf("-flex-") == 0){
flex =true;
key = key.substr(6);
}
// look for min
if (key.indexOf("min-") == 0)
{
min = true;
key = key.substr(4);
}
// look for max
else if (key.indexOf("max-") == 0)
{
max = true;
key = key.substr(4);
}
// collapse hypens into camelcase ;
if (key.indexOf("-") > 0)
key = deHyphenate(key, flex);
if ( key == "deviceWidth")
usesDeviceWidth = true;
else if (key =="deviceHeight" )
usesDeviceHeight = true;
else if (key == "flexDeviceDiagonal")
usesDeviceDiagonal = true;
// if only one part, then it only matters that this property exists
if (parts.length == 1)
{
if (!(key in this))
return false;
}
// if two parts, then make sure the property exists and value matches
if (parts.length == 2)
{
// if property doesn't exist, then bail
if (!(key in this))
return false;
var value: Object = normalize(parts[1], this[key]) ;
var cmp: int = compareValues(this[key], value) ;
// handle min (we don't check if min is allowed for this property)
if (min)
{
if (cmp < 0)
return false;
}
// handle max (we don't check if min is allowed for this property)
else if (max)
{
if (cmp > 0)
return false;
}
// bail if the value doesn't match
else if (cmp != 0)
{
return false;
}
}
}
// all parts matched so return true
return true;
}
// strip off unit if currentValue is Number or int
// now supports versions (X.Y.Z) and numbers with units
private function normalize(s:String, currentValue: Object ):Object
{
var index:int;
// strip leading white space
if (s.charAt(0) == " ")
s = s.substr(1);
// for the numbers we currently handle, we
// might find dpi or ppi on it, that we just strip off.
// We don't handle dpcm yet.
if (currentValue is Number)
{
index = s.indexOf("dpi");
if (index != -1)
{
s = s.substr(0, index);
}
return Number(s);
}
else if (currentValue is int)
{
return int(s);
}
// string or CSS value
// strip quotes of strings
if (s.indexOf('"') == 0) {
if (s.lastIndexOf('"') == s.length - 1)
s = s.substr(1, s.length - 2);
else
s = s.substr(1);
}
// string , return
if (currentValue is String)
{
return s;
}
else if (currentValue is CSSOSVersion) {
return new CSSOSVersion(s) ;
}
else if (currentValue is CSSDimension) {
var matches: Array = s.match(/([\d\.]+)(in|cm|dp|pt|px|)$/); // decimal number following by either units or no unit
if (matches!= null && matches.length == 3) {
var unit: String = matches[2];
var refDPI: Number = unit == CSSDimension.UNIT_DP ? applicationDpi : deviceDPI ; // DPI use applicationDPI for conversion
return new CSSDimension(Number(matches[1]), refDPI,unit );
}
else {
throw new Error("Unknown unit in css media query:" + s); //TODO NLS Error message
return s;
}
}
return s;
}
/** @private
* Compares current value with test values, using currentValue type to determine comparison function
* accepts number, int, string and CSSOSVersion.
* Will accept LexicalUnit in the future
*
* @param currentValue
* @param testValue
* @return -1 if currentValue < testValue, 1 if currentValue > testValue and 0 if equal
*/
private function compareValues ( currentValue: Object, testValue: Object): int
{
if (currentValue is CSSOSVersion)
return CSSOSVersion(currentValue).compareTo(CSSOSVersion(testValue)) ;
else if (currentValue is CSSDimension)
return CSSDimension(currentValue).compareTo(CSSDimension(testValue));
else // scalar compare operators
if ( currentValue == testValue)
return 0;
else if ( currentValue < testValue)
return -1;
else
return 1;
}
// collapse "-" to camelCase
private function deHyphenate(s:String, flex: Boolean):String
{
var i:int = s.indexOf("-");
var part: String;
var c: String;
while (i > 0)
{
part = s.substr(i + 1);
s = s.substr(0, i);
c = (part.charAt(0)).toUpperCase();
s += c + part.substr(1);
i = s.indexOf("-");
}
// if flex, camel case and prefix with flex
if (flex){
c = (s.charAt(0)).toUpperCase();
s = "flex" + c + s.substr(1);
}
return s;
}
private function getPlatform():String
{
var s:String = Capabilities.version.substr(0, 3);
// if there is a friendly name, then use it
if (platformMap.hasOwnProperty(s))
return platformMap[s] as String;
// otherwise match against the 3 characters.
// use lower case because match are case
// insensitive and we lower case the entire
// expression
return s.toLowerCase();
}
/** @private
* returns a CSSOSVersion suitable for MediaQueryParser for the current device operating system version.
* */
private function getOSVersion():CSSOSVersion {
return new CSSOSVersion(Platform.osVersion) ;
}
/** @private recompute device dimension
*
* @return true if any dimension that is used in media queries has changed, and styles need to be regenerated
* we ignore changes to deviceDiagonal on purpose, so that only changing
*/
private function computeDeviceDimensions(): Boolean
{
if (sm) {
var w: Number = sm.stage.stageWidth;
var h: Number = sm.stage.stageHeight;
var diag: Number = Math.sqrt(w * w + h * h);
// we need to update styles if device-width is used and has changed or device-height is used and has changed
// for example after switching orientation or going fullscreen
// we ignore changes to device diagonal on purpose
var needToUpdateStyles: Boolean = (usesDeviceWidth && w != deviceWidth.pixelValue) || (usesDeviceHeight && h != deviceHeight.pixelValue) ;
deviceWidth = new CSSDimension(w, deviceDPI);
deviceHeight = new CSSDimension(h, deviceDPI);
flexDeviceDiagonal = new CSSDimension( diag, deviceDPI );
return needToUpdateStyles;
}
return false;
}
private function stage_resizeHandler(event: Event): void
{
if (computeDeviceDimensions()) {
// reinit query cache then reload styles
goodQueries = {};
badQueries = {};
reinitApplicationStyles();
}
}
private function reinitApplicationStyles( ):void {
var styleManager: IStyleManager2 = StyleManager.getStyleManager(sm);
styleManager.stylesRoot = null;
styleManager.initProtoChainRoots();
var sms: Array = SystemManagerGlobals.topLevelSystemManagers;
var n: int = sms.length;
var i: int;
// Type as Object to avoid dependency on SystemManager.
var sm: ISystemManager;
var cm: Object;
// Regenerate all the proto chains
// for all objects in the application.
for (i = 0; i < n; i++) {
sm = sms[i];
cm = sm.getImplementation("mx.managers::ISystemManagerChildManager");
cm.regenerateStyleCache(true);
}
for (i = 0; i < n; i++) {
sm = sms[i];
cm = sm.getImplementation("mx.managers::ISystemManagerChildManager");
cm.notifyStyleChangeInChildren(null, true); // all styles
}
}
/* real device DPI, use for converting physical units */
private var deviceDPI: Number ;
// the type of the media
public var type:String = "screen";
// the resolution of the media
public var applicationDpi:Number;
// the platform of the media
public var osPlatform:String;
// the platform os version of the media
public var osVersion: CSSOSVersion;
/**
* Physical device width.
* matches "device-width" selector.
*/
[Bindable]
public var deviceWidth: CSSDimension ;
/**
* Physical device height.
* matches "device-height" selector
*/
[Bindable]
public var deviceHeight: CSSDimension;
/**
* Physical device diagonal.
* matches "-flex-device-diagonal" selector
* prefixed by "-flex" because it's not W3C standard
*
*/
public var flexDeviceDiagonal: CSSDimension;
}
}