UNPKG

style-manager

Version:

Manage style, add/replace/delete rules, support media.

240 lines (196 loc) 6.94 kB
import util from '../util'; const CSS_PSEUDO_ELEMENTS = [ 'after', 'before', 'first-line', 'first-letter', 'selection', 'placeholder' ]; export default class Rule { /** * @param {CSSRule|CssRule} cssRule * @param {Object} opts * @param {StyleManager} sm */ constructor(cssRule, opts, sm) { this.cssRule = cssRule; this.opts = opts; this.sm = sm; this._nextTickId = null; this._cbs = []; } remove() { this.sm.remove(this); } is(type) { return this.constructor.type === type; } _updateSheetLater(cb) { if (typeof cb === 'function') this._cbs.push(cb); if (this._nextTickId) clearTimeout(this._nextTickId); this._nextTickId = setTimeout(() => { let result = this._updateSheet(); for (let i = 0; i < this._cbs.length; i++) this._cbs[i](result); this._cbs.length = 0; }, 0); } _updateSheet() { let sm = this.sm; let cssText = this.constructor.parseOptsToCssText(this.opts); // 不用更新 if (cssText === this.getCssText()) return true; console.debug('Transform opts %o to css text %o', this.opts, cssText); let index = sm.index(this); try { this.cssRule = sm.insertCssText(cssText, index + 1); } catch (e) { console.warn('Update rule %o failed. insert cssText: %o, error: %o', this, cssText, e); return false; } // 插入成功才删除之前的 cssRule sm.deleteCssRule(index); return true; } /** * @param {Object} opts * @param {Function} [cb] */ setOpts(opts, cb) { util.assign(this.opts, opts); this._updateSheetLater(cb); } /** * @param {Object} opts * @param {Function} [cb] */ replaceOpts(opts, cb) { this.opts = opts || {}; this._updateSheetLater(cb); } /** * @param {Function} [cb] * @returns {Boolean} */ forceUpdateSheet(cb) { return this._updateSheetLater(cb); } getCssText() { return this.cssRule.cssText; } toString() { return this.cssRule.cssText; } //========== 工具函数: /** * 计算单个 CSS 选择器的权重 * @param {String} selector * * https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity * * Elements and pseudo-elements: 1 * Classes, attributes and pseudo-classes: 10 * Id: 100 * */ static calculateSelectorSpecificity(selector) { let specificity = 0; let parsed = selector .replace(/[\*\+>~]/g, ' ') // 去掉全局选择器和特殊分隔符号 .replace(/:not(\(.*?\))/g, '$1') // :not 只需要使用它内部的选择器即可 .replace(/\([^\)]*\)/g, '') // 去掉其它无用的括号 .replace(/\[[^\]]*\]/g, () => { // 计算属性选择器的权重,并去掉它们 specificity += 10; return ''; }) .replace(/:?:([\w-]+)/g, (_, m) => { // 计算伪类和伪元素的权值,计算完就把它们去掉 specificity += CSS_PSEUDO_ELEMENTS.indexOf(stripVendors(m)) < 0 ? 10 : 1; return ''; }) .replace(/([#\.]?)[\w-]+/g, (_, m) => { specificity += ({'#': 100, '.': 10})[m] || 1; return ''; }); if (parsed.trim() !== '') throw new Error(`Selector '${selector}' is invalid, parse result '${parsed}'.`); return specificity; } /** * @param {Object} style * @returns {string} */ static styleObjectToCssCode(style) { let keys = Object.keys(style).map(kebabCaseStyleKey); if (!keys.length) return ''; keys = sortStyleVendorKeys(keys); return keys.map(key => key + ': ' + style[key] + ';').join('\n'); } /** * @param {String} cssCode * @returns {Object} */ static cssCodeToStyleObject(cssCode) { let rules = cssCode.trim().split(';') .map(rule => rule.trim()) .filter(rule => rule && rule.indexOf(':') > 0); return rules.reduce((result, rule) => { let parts = rule.split(':'); result[parts.shift().trim()] = parts.join(':').trim(); return result; }, {}); } //========== 子类要必需实现的方法: /** * 此方法是给 RuleControl.createRuleByOpts 用的,根据用户输入的参数,解析得到 cssText, * RuleControl 再将这 cssText 插入 Stylesheet 中,生成 cssRule 创建 Rule * @param {Object} opts */ static parseOptsToCssText(opts) { return opts.cssText; } /** * 此方法是用在 RuleControl.createRuleByCssRule 中的,当首次从一个文档中已经存在的 Stylesheet 中解析 rules 时, * 具体的 Rule 应该用此方法将 cssRule 解析成 opts 并保存,方便之后取用 * @param {CSSRule|CssRule} cssRule */ static parseCssRuleToOpts(cssRule) { return {cssText: cssRule.cssText}; } //=========== 子类可选择实现的方法 /** * 此方法用在 RuleControl.createRuleByOpts,执行 parseOptsToCssText 并生成 cssRule 之前 * 用于对用户传来的 opts 做进一步加工成 Rule 可以直接使用的 opts * @param {Object} opts * @returns {Object} */ //static unifyUserOpts(opts) {} /** * 验证用户提供的 opts 是否合法 * @param {Object} opts * @returns {Boolean} */ //static validateUserOpts(opts) {} } /** * 带 vendor 前缀的要放非带 vendor 前缀的前面 * * @param {Array} styleKeys * @returns {Array} */ function sortStyleVendorKeys(styleKeys) { let vendorStyleKeys = styleKeys.filter(key => key[0] === '-'); // 没有带 vendor 前缀的属性,直接返回 if (!vendorStyleKeys.length) return styleKeys; let result = []; let vendorStyleKeysMirror = vendorStyleKeys.map(key => key.replace(/^-\w+-/, '')); styleKeys.forEach(key => { if (key[0] !== '-') { for (let i = 0, l = vendorStyleKeysMirror.length; i < l; i++) { if (vendorStyleKeysMirror[i] === key) result.push(vendorStyleKeys[i]); } result.push(key); } }); return styleKeys; } /** * 将 JS 中的驼峰式的样式 key 转成 中划线式的 * @param {String} styleKey * @returns {String} * */ function kebabCaseStyleKey(styleKey) { styleKey = util.kebabCase(styleKey); if (['o', 'ms', 'moz', 'webkit'].some(k => styleKey.indexOf(k + '-') === 0)) { return '-' + styleKey; } return styleKey; } function stripVendors(str) { return str.replace(/^-(o|ms|moz|webkit)-/, ''); }