style-manager
Version:
Manage style, add/replace/delete rules, support media.
306 lines (241 loc) • 7.68 kB
JavaScript
import {getType, kebabCase} from '../../util';
const MEDIA_TYPES = [
'all',
'print',
'screen',
'speech',
'aural',
'braille',
'handheld',
'projection',
'tty',
'tv',
'embossed'
];
const MEDIA_FEATURES = [
'width',
'min-width',
'max-width',
'height',
'min-height',
'max-height',
'aspect-ratio',
'min-aspect-ratio',
'max-aspect-ratio',
'device-width',
'min-device-width',
'max-device-width',
'device-height',
'min-device-height',
'max-device-height',
'device-aspect-ratio',
'min-device-aspect-ratio',
'max-device-aspect-ratio',
'color',
'min-color',
'max-color',
'color-index',
'min-color-index',
'max-color-index',
'monochrome',
'min-monochrome',
'max-monochrome',
'resolution',
'min-resolution',
'max-resolution',
'scan', // progressive, interlace
'grid',
'orientation' // portrait, landscape
];
let checkType = (type) => {
if (MEDIA_TYPES.indexOf(type) < 0)
throw new Error(`Media type '${type}' is invalid, need one of ${MEDIA_TYPES.join(', ')}.`);
};
let checkFeature = (feature) => {
if (MEDIA_FEATURES.indexOf(feature) < 0)
throw new Error(`Media feature '${feature}' is invalid, need one of ${MEDIA_FEATURES.join(', ')}.`);
};
/*
media_query_list: <media_query> [, <media_query> ]*
media_query: [[only | not]? <media_type> [ and <expression> ]*] | <expression> [ and <expression> ]*
expression: ( <media_feature> [: <value>]? )
media_type: all | print | projection | screen ...
media_feature: width | min-width | max-width ...
*/
// 注意,真正插入 Document 中,此函数返回的值可能会被浏览器优化或者改变 features 顺序,比如:
// @media all and (width: 300px), all and (height: 400px) 会被优化成
// @media all and (width: 300px), (height: 400px)
// 所以插入 Document 中后就需要用原生的 media.mediaText 去获取系统中的 mediaText,而不能以此函数为准
/**
* 将一个对象或用户自定的的mediaText 解析成一个统一的 mediaText,
* 保证得到的 mediaText 在 features 相同的情况下,它也是一致的
*
* Object opt structure:
* only: Boolean
* not: Boolean - only 和 not 最多只能有一个为 true
* type: String - one of MEDIA_TYPES
* features: Object
*
* @param {Object|Array<Object>|String} opts
* @returns {String}
*
* @example
*
* input: {type: 'all', features: {width: {min: '30px', max: '200px'}}}
* input: {type: 'all', features: {maxWidth: '200px', minWidth: '30px'}}
* input: 'all and (max-width: 200px) and (min-width: 30px)'
*
* all output: all and (min-width: 30px) and (max-width: 200px)
*
*/
class MediaQuery {
constructor(modifier, type, features) {
this.modifier = modifier;
this.type = type;
this.features = features;
}
only() {
this.modifier = 'only';
}
reverse() {
this.modifier = 'not';
}
setType(type) {
checkType(type);
this.type = type;
}
setFeatures(features) {
this.features = parseObjectFeaturesToArray(features);
}
appendFeatures(features) {
this.features.push(...parseObjectFeaturesToArray(features));
}
toMediaText() {
let text = this.modifier;
let features = this.features;
let allFeatures = MEDIA_FEATURES;
text += (text ? ' ' : '') + this.type;
if (features.length) {
// 对 features 进行排序,保证输出的 text 的一致性
features = [].concat(features); // 克隆一份,保证原有顺序不变
features.sort((a, b) => allFeatures.indexOf(a.key) - allFeatures.indexOf(b.key));
text += ' and (' + features.map(f => f.key + (('value' in f) ? ': ' + f.value : '')).join(') and (') + ')';
}
return text;
}
}
export default class Media {
constructor(opts) {
this.list = [];
if (!opts) opts = {type: 'all'};
if (Array.isArray(opts)) {
opts.forEach(opt => this.addMediaQuery(opt));
} else if (typeof opts === 'string') {
opts.split(',').forEach(opt => this.addMediaQuery(opt));
} else {
this.addMediaQuery(opts);
}
}
get length() {
return this.list.length;
}
addMediaQuery(opt) {
let mq;
switch (getType(opt)) {
case 'object':
mq = parseObjectOptToQuery(opt);
break;
case 'string':
mq = parseStringOptToQuery(opt);
break;
default:
throw new Error('opt ' + opt + ' can not parse to media query.');
}
this.list.push(mq);
return mq;
}
/**
* @param {Number} index
*/
get(index) {
return this.list[index];
}
toMediaText() {
return this.list.map(query => query.toMediaText()).join(', ');
}
equals(mediaQuery) {
return this.toMediaText() === mediaQuery.toMediaText();
}
static normalize(opts) {
return new Media(opts).toMediaText();
}
}
Media.TYPES = MEDIA_TYPES;
Media.FEATURES = MEDIA_FEATURES;
// ============== 解析成统一的对象
function parseStringOptToQuery(opt) {
let parts = opt.trim().split(/\s+and\s+/);
let types = MEDIA_TYPES;
let type = 'all', modifier = '', features = {};
parts[0].replace(/^(?:(only|not)\s+)?([-\w]+)$/, (_, m, t) => {
if (types.indexOf(t) >= 0) {
if (m) modifier = m;
type = t;
parts.shift();
}
});
parts.forEach(part => {
let kv = part.replace(/^\(\s*(.*?)\s*\)$/, '$1').split(/\s*:\s*/);
if (kv.length === 1) features[kv[0]] = true;
else if (kv.length === 2) features[kv[0]] = kv[1];
else throw new Error('Parse media string error.');
});
features = parseObjectFeaturesToArray(features);
return new MediaQuery(modifier, type, features);
}
function parseObjectOptToQuery(opt) {
let type, modifier, features;
if (opt.type) {
checkType(opt.type);
type = opt.type;
} else {
type = 'all';
}
if (opt.not && opt.only)
throw new Error('Media type modifier "not" and "only" should only use one of them.');
modifier = opt.modifier || (opt.not ? 'not' : (opt.only ? 'only' : ''));
features = parseObjectFeaturesToArray(opt.features);
return new MediaQuery(modifier, type, features);
}
function parseObjectFeaturesToArray(features = {}) {
let result = [], keys;
features = normalizeObjectFeatures(features);
keys = Object.keys(features);
keys.forEach(key => {
checkFeature(key);
if (features[key] === true || features[key] === '') {
result.push({key});
} else {
result.push({key, value: features[key].toString()});
}
});
return result;
}
function flatObjectFeatureValue(key, feature, result) {
Object.keys(feature).forEach(subKey => result[subKey + '-' + key] = feature[subKey]);
}
function normalizeObjectFeatures(features = {}) {
let key, feat, result = {};
for (key in features) {
if (features.hasOwnProperty(key)) {
feat = features[key];
key = kebabCase(key);
if (getType(feat) === 'object') {
flatObjectFeatureValue(key, feat, result);
} else {
result[key] = feat;
}
}
}
return result;
}