Welcome to mirror list, hosted at ThFree Co, Russian Federation.

rfs.js « lib - github.com/twbs/rfs.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: a3e6d3655a1b93368bc8ff4e2ed332fa61965a63 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
'use strict';

const postcss = require('postcss');
const valueParser = require('postcss-value-parser');

const BREAKPOINT_ERROR = 'breakpoint option is invalid, it should be set in `px`, `rem` or `em`.';
const BREAKPOINT_UNIT_ERROR = 'breakpointUnit option is invalid, it should be `px`, `rem` or `em`.';
const BASE_RFS_ERROR = 'baseValue option is invalid, it should be set in `px` or `rem`.';

const defaultOptions = {
  baseValue: 20,
  unit: 'rem',
  breakpoint: 1200,
  breakpointUnit: 'px',
  factor: 10,
  twoDimensional: false,
  unitPrecision: 5,
  remValue: 16,
  functionName: 'rfs',
  enableRfs: true,
  mode: 'min-media-query'
};

module.exports = class {
  constructor(opts) {
    this.opts = { ...defaultOptions, ...opts };

    if (typeof this.opts.baseValue !== 'number') {
      if (this.opts.baseValue.endsWith('px')) {
        this.opts.baseValue = Number.parseFloat(this.opts.baseValue);
      } else if (this.opts.baseValue.endsWith('rem')) {
        this.opts.baseValue = Number.parseFloat(this.opts.baseValue) * this.opts.remValue;
      } else {
        console.error(BASE_RFS_ERROR);
      }
    }

    if (typeof this.opts.breakpoint !== 'number') {
      if (this.opts.breakpoint.endsWith('px')) {
        this.opts.breakpoint = Number.parseFloat(this.opts.breakpoint);
      } else if (this.opts.breakpoint.endsWith('em')) {
        this.opts.breakpoint = Number.parseFloat(this.opts.breakpoint) * this.opts.remValue;
      } else {
        console.error(BREAKPOINT_ERROR);
      }
    }

    if (!['px', 'rem', 'em'].includes(this.opts.breakpointUnit)) {
      console.error(BREAKPOINT_UNIT_ERROR);
    }
  }

  toFixed(number, precision) {
    const multiplier = 10 ** (precision + 1);
    const wholeNumber = Math.floor(number * multiplier);

    return Math.round(wholeNumber / 10) * 10 / multiplier;
  }

  renderValue(value) {
    // Do not add unit if value is 0
    if (value === 0) {
      return value;
    }

    // Render value in desired unit
    return this.opts.unit === 'rem' ?
      `${this.toFixed(value / this.opts.remValue, this.opts.unitPrecision)}rem` :
      `${this.toFixed(value, this.opts.unitPrecision)}px`;
  }

  process(declarationValue, fluid) {
    const parsed = valueParser(declarationValue);

    // Function walk() will visit all the of the nodes in the tree,
    // invoking the callback for each.
    parsed.walk(node => {
      // Since we only want to transform rfs() values,
      // we can ignore anything else.
      if (node.type !== 'function' && node.value !== this.opts.functionName) {
        return;
      }

      const wordNodes = node.nodes.filter(node => node.type === 'word');

      for (const node of wordNodes) {
        node.value = node.value.replace(/^(-?\d*\.?\d+)(.*)/g, (match, value, unit) => {
          value = Number.parseFloat(value);

          // Return value if it's not a number or px/rem value
          if (Number.isNaN(value) || !['px', 'rem'].includes(unit)) {
            return match;
          }

          // Convert to px if in rem
          if (unit === 'rem') {
            value *= this.opts.remValue;
          }

          // Only add responsive function if needed
          if (!fluid || this.opts.baseValue >= Math.abs(value) || this.opts.factor <= 1 || !this.opts.enableRfs) {
            return this.renderValue(value);
          }

          // Calculate base and difference
          let baseValue = this.opts.baseValue + ((Math.abs(value) - this.opts.baseValue) / this.opts.factor);
          const diff = Math.abs(value) - baseValue;

          // Divide by remValue if needed
          if (this.opts.unit === 'rem') {
            baseValue /= this.opts.remValue;
          }

          const viewportUnit = this.opts.twoDimensional ? 'vmin' : 'vw';

          return value > 0 ?
            `calc(${this.toFixed(baseValue, this.opts.unitPrecision)}${this.opts.unit} + ${this.toFixed(diff * 100 / this.opts.breakpoint, this.opts.unitPrecision)}${viewportUnit})` :
            `calc(-${this.toFixed(baseValue, this.opts.unitPrecision)}${this.opts.unit} - ${this.toFixed(diff * 100 / this.opts.breakpoint, this.opts.unitPrecision)}${viewportUnit})`;
        });
      }

      // Now we will transform the existing rgba() function node
      // into a word node with the hex value
      node.type = 'word';
      node.value = valueParser.stringify(node.nodes);
    });

    return parsed.toString();
  }

  // Return the value without `rfs()` function
  // eg. `4px rfs(32px)` => `.25rem 2rem`
  value(value) {
    return this.process(value, false);
  }

  // Convert `rfs()` function to fluid css
  // eg. `4px rfs(32px)` => `.25rem calc(1.325rem + 0.9vw)`
  fluidValue(value) {
    return this.process(value, true);
  }

  renderMediaQuery() {
    const mediaQuery = {
      name: 'media'
    };

    const dimPrefix = this.opts.mode === 'min-media-query' ? 'min' : 'max';
    const dimConnector = this.opts.mode === 'min-media-query' ? ' and' : ',';
    const breakpoint = this.opts.breakpointUnit === 'px' ? this.opts.breakpoint : this.opts.breakpoint / this.opts.remValue;

    mediaQuery.params = this.opts.twoDimensional ?
      `(${dimPrefix}-width: ${breakpoint}${this.opts.breakpointUnit})${dimConnector} (${dimPrefix}-height: ${breakpoint}${this.opts.breakpointUnit})` :
      `(${dimPrefix}-width: ${breakpoint}${this.opts.breakpointUnit})`;

    return postcss.atRule(mediaQuery);
  }

  getOptions() {
    return this.opts;
  }
};