JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
stylus helpers: add wfpl_columns()
[wfpl.git] / stylus_helpers.styl
1 // This program is in the public domain within the United States. Additionally,
2 // we waive copyright and related rights in the work worldwide through the CC0
3 // 1.0 Universal public domain dedication, which can be found at
4 // http://creativecommons.org/publicdomain/zero/1.0/
5
6 // This file contains helpers for using stylus in your project.
7 //
8 // Put something like this in your styl.styl:
9 //     @require 'inc/wfpl/stylus-helpers.styl'
10
11
12 // set units to px if it doesn't have a unit already
13 _px(i)
14         unit(i) == '' ? unit(i, px) : i
15
16 // transparently support vender-specific tags
17 border-radius()
18         -webkit-border-radius: arguments
19         border-radius: arguments
20 box-shadow()
21         -webkit-box-shadow: arguments
22         box-shadow: arguments
23 box-sizing()
24         -webkit-box-sizing: arguments
25         -moz-box-sizing: arguments
26         box-sizing: arguments
27 user-select()
28         -webkit-user-select: arguments
29         -moz-user-select: arguments
30         -ms-user-select: arguments
31         user-select: arguments
32
33 // map "whitespace:" to "white-space:"
34 whitespace()
35         white-space: arguments
36
37 // helper function
38 _pos(type, args)
39         position: unquote(type)
40         for i in (0...length(args))
41                 if not args[i] is a 'unit'
42                         {args[i]}: args[i + 1] is a 'unit' ? args[i + 1] : 0
43
44 // you can pass directions and units, or just directions. Examples:
45 //     absolute: top 20px left 0
46 //     fixed: top right
47 //     relative: top -4px
48 absolute()
49         _pos('absolute', arguments)
50 fixed()
51         _pos('fixed', arguments)
52 relative()
53         _pos('relative', arguments)
54
55 // Specify width and height on the same line
56 dimensions(w, h)
57         width: _px(w)
58         height: _px(h)
59
60 // generate (return) a "calc(Fpx + R%)" to scale linearly with parent
61 // args:
62 //   pb: pixel width of parent (when biggest)
63 //   ps: pixel width of parent (when smallest)
64 //   cb: pixel width of child (when biggest)
65 //   cs: pixel width of child (when smallest)
66 linear_scale_calc(pb, ps, cb, cs)
67         // math explained: (figuring out F and R in calc(Fpx + R%))
68         // f + r * pb = cb   // known_1: formula(pb) -> cb
69         //     f = cb - r * pb    // solve_for_f: subtract (r * pb) from both sides
70         // f + r * ps = cs   // known_2: formula(pb) -> cb
71         //     cb - r * pb + r * ps = cs    // substitute solved_for_f into known_2
72         //     -r * pb + r * ps = cs - cb   // subtract cb from both sides
73         //     r * (-pb + ps) = cs - cb     // factor out r from left side
74         //     r = (cs - cb) / (-pb + ps)   // divide both sides by (-pb + ps)
75         //     r = (cb - cs) / (pb - ps)    // multiply left side by -1/-1
76         r = (cb - cs) / (pb - ps)
77         f = cb - r * pb
78         unquote("calc(" + unit(f, "px") + ' + ' + unit(100 * r, "%") + ")")
79
80 // Make children of this element inline-blocks that are spaced evenly accross.
81 //
82 // To create a minimum distance between: don't use word-spacing, it's broken in
83 // firefox. Instead, try padding on the children and negative margin on the
84 // parent.
85 space_evenly(line_height = 1.2em)
86         text-align: justify
87         & > *
88                 display: inline-block
89                 relative: top line_height
90         &:before
91                 content: ''
92                 display: block
93                 width: 100%
94                 margin-bottom: -(line_height)
95         &:after
96                 content: ''
97                 display: inline-block
98                 width: 100%
99
100 // image layout: left column has normal, right column hover versions
101 //
102 // arguments: width/height are pixel dimensions of a single sprite
103 //
104 // markup: put classes n1, n2, etc on the items.
105 //
106 // Example
107 //
108 //     html:
109 //         <nav>
110 //             <ul>
111 //                 <li class="n0">home</li>
112 //                 <li class="n1">contact</li>
113 //             </ul>
114 //         </nav>
115 //     styl:
116 //         nav li
117 //             sprites_rollover "images/nav.png" 150 35 2
118 sprites_rollover(image, width, height, count, v_offset = 0, h_offset = 0)
119         width: unit(width, px)
120         height: unit(height, px)
121         if image[1]
122                 background: url(image[0])
123                 background: url(image[1]), linear-gradient(transparent, transparent);
124         else
125                 background-image: url(image)
126         background-position: top left
127         background-repeat: no-repeat;
128         for n in (0...count)
129                 &.n{n}
130                         y = - (@height * n) - unit(v_offset, px)
131                         background-position: (0 - unit(h_offset, px)) y
132                         &:hover
133                                 background-position: (0 - unit(h_offset, px) - @width) y
134 sprite_rollover(image, width, height, v_offset = 0, h_offset = 0)
135         sprites_rollover(image, width, height, 1, v_offset, h_offset)
136
137 // see sprites_rollover
138 sprites(image, height, count, v_offset = 0, h_offset = 0)
139         height: unit(height, px)
140         if image[1]
141                 background: url(image[0])
142                 background: url(image[1]), linear-gradient(transparent, transparent);
143         else
144                 background-image: url(image)
145         background-position: top left
146         background-repeat: no-repeat;
147         for n in (0...count)
148                 &.n{n}
149                         y = - (@height * n) - unit(v_offset, px)
150                         background-position: (0 - unit(h_offset, px)) y
151 sprite(image, height, v_offset = 0, h_offset = 0)
152         sprites(image, height, 1, v_offset, h_offset)
153
154 // Styling for a variable height element with an image background where the
155 // middle repeats vertically. You must split your image into three images, and
156 // name them with "-top", "-mid" and "-bot" suffixes (right before the file
157 // extension.) then pass a filepath as the first argument to this function
158 // which does not have a suffix.
159 //
160 // Example:
161 //
162 //     image files:
163 //         i/foo-top.png  (900x20)
164 //         i/foo-mid.png  (900xwhatever)
165 //         i/foo-bot.png  (900x30)
166 //
167 //     dom layout: <div id="expando"><div><div><div>...</div></div></div></div>
168 //
169 //     Stylus:
170 //
171 //         #expando
172 //            mangin: 10px auto;
173 //            width: 900px;
174 //            background_vertical_stretch("i/foo.png", 20, 30)
175 //
176 //     Notes:
177 //
178 //         You can (optionally) pass 1-4 additional arguments which
179 //         (effectively) add padding to the inner most div. The default is to
180 //         have the height and width of the inner div match exactly the outer
181 //         one. Parameters you specify are relative to that (not the section
182 //         filled with the "-mid" image).
183 background_vertical_stretch(image, top_height, bottom_height, top_pad = 0, right_pad = top_pad, bottom_pad = top_pad, left_pad = right_pad)
184         top_height = _px(top_height)
185         bottom_height = _px(bottom_height)
186         top_pad = _px(top_pad)
187         right_pad = _px(right_pad)
188         bottom_pad = _px(bottom_pad)
189         left_pad = _px(left_pad)
190         ext = extname(image)
191         path = pathjoin(dirname(image), basename(image, ext))
192
193         // bottom image (outermost block)
194         background: transparent url(path + '-bot' + ext)
195         padding-bottom: bottom_height + 1 // +1 to prevent margin collapsing
196         > *
197                 background: transparent url(path + '-top' + ext) 0 0 no-repeat
198                 padding-top: top_height + 1 // +1 to prevent margin collapsing
199         > * > *
200                 background: transparent url(path + '-mid' + ext) 0 0 repeat-y;
201                 margin-top: -1px; // correct for above +1 from top
202                 margin-bottom: -1px; // correct for above +1 from bottom
203                 padding: 1px; // to prevent margin collapsing
204         > * > * > *
205                 // all "2px" below are to correct for 1px above padding and below
206                 padding: 1px;
207                 margin-top: 2px - top_height + top_pad
208                 margin-left: left_pad - 2px
209                 margin-right: right_pad - 2px
210                 margin-bottom: 2px - bottom_height + bottom_pad
211                 background: transparent;
212
213 li_reset()
214         margin: 0
215         padding: 0
216         list-style: none
217
218 // Example:
219 //     input
220 //         +placeholder()
221 //             color: red
222 placeholder()
223         &::-webkit-input-placeholder
224                 {block}
225         &:-moz-placeholder // firefox 4-18
226                 {block}
227         &::-moz-placeholder // firefox 19+
228                 {block}
229         &:-ms-input-placeholder // ie
230                 {block}
231
232 // Example:
233 //     div.button
234 //         noselect()
235 noselect()
236         -webkit-touch-callout: none
237         -webkit-user-select: none
238         -khtml-user-select: none
239         -moz-user-select: none
240         -ms-user-select: none
241         user-select: none
242
243 // Do all the crazy math to get columns to fit properly
244 // and (optionally) scale for responsive designs.
245 //
246 // example:
247 //
248 //      columns = wfpl_columns({
249 //              type: 'node',
250 //              name: 'centerer',
251 //              margin: 15px,
252 //              width: 940px,
253 //              child: {
254 //                      type: 'node',
255 //                      name: 'middler',
256 //                      border-width: 1px,
257 //                      border-style: solid,
258 //                      border-color: black,
259 //                      padding-left: 30px,
260 //                      padding-right: 18px,
261 //                      child: {
262 //                              type: 'alternatives',
263 //                              full: {
264 //                                      type: 'series',
265 //                                      nav: {
266 //                                              type: 'node'
267 //                                              width: 219px
268 //                                              float: left
269 //                                      },
270 //                                      main: {
271 //                                              type: 'node'
272 //                                              margin-left: 48px,
273 //                                              width: 623px
274 //                                              float: left
275 //                                      }
276 //                              },
277 //                              with_sidebar: {
278 //                                      type: 'series',
279 //                                      nav: {
280 //                                              type: 'node',
281 //                                              width: 219px
282 //                                              float: left
283 //                                      },
284 //                                      main: {
285 //                                              type: 'node',
286 //                                              margin-left: 48px,
287 //                                              width: 343px
288 //                                              float: left
289 //                                      },
290 //                                      sidebar: {
291 //                                              type: 'alternatives',
292 //                                              plain: {
293 //                                                      type: 'node'
294 //                                                      margin-left: 30px,
295 //                                                      width: 250px
296 //                                                      float: left
297 //                                              },
298 //                                              bordered: {
299 //                                                      type: 'node'
300 //                                                      margin-left: 30px,
301 //                                                      border-width: 1px,
302 //                                                      border-style: solid
303 //                                                      border-color: red
304 //                                                      padding: 15px,
305 //                                                      width: 218px
306 //                                                      float: left
307 //                                              }
308 //                                      }
309 //                              }
310 //                      }
311 //              }
312 //      })
313 // output css from column calculations
314 //      for selector, css in columns.css
315 //              body > {selector}
316 //                      {css}
317 //      @media screen and (max-width: (columns.width))
318 //              // output responsive css from column calculations
319 //              for selector, css in columns.responsive_css
320 //                      body > {selector}
321 //                              {css}
322 //
323 //              // as big as it can be
324 //              body > .centerer
325 //                      width: auto
326 //
327 //              // make sure that borders (which won't scale) and rounding errors don't
328 //              // break the layout
329 //              body > .centerer.full > .main,
330 //              body > .centerer.with_sidebar > .sidebar.plain,
331 //              body > .centerer.with_sidebar > .sidebar.bordered
332 //                      margin-right: -10px
333 //
334 //      // switch to 1-column layout
335 //      @media screen and (max-width: (single_column_max))
336 //              for selector, css in columns.css
337 //                      body > {selector}
338 //                              if selector == '.centerer'
339 //                                      margin-top: 0
340 //                              else if selector in hide_in_one_column_mode
341 //                                      display: none
342 //                              else
343 //                                      border: none
344 //                                      float: none
345 //                                      width: auto
346 //                                      margin: 0
347 //                                      padding: 0
348 //                                      margin-top: columns['responsive_css']['.centerer']['margin']
349 wfpl_columns_helper(top, node, selector, parent_width, expected_width)
350         css_rules = {}
351         responsive_css_rules = {}
352         if node.type == 'node'
353                 left_width = 0px
354                 right_width = 0px
355                 width = null // inner
356                 outer_width = null
357                 if node.name
358                         if selector == ''
359                                 selector = '.' + node.name
360                         else
361                                 selector += ' > .' + node.name
362                 // callculate width and outer_width
363                 for k, v in node
364                         if match('^(margin|padding|border-width)$', k)
365                                 left_width += v
366                                 right_width += v
367                         if match('^((margin|padding)-left)|(border-left-width)$', k)
368                                 left_width += v
369                         if match('^((margin|padding)-right)|(border-right-width)$', k)
370                                 right_width += v
371                         if k != 'type' && k != 'child' && k != 'name' && k != 'outer-width' && k != 'outer_width'
372                                 css_rules[k] = v
373                 for k, v in node
374                         if k == 'width'
375                                 width = v
376                                 outer_width = left_width + v + right_width
377                         if k == 'outer-width' && k == 'outer_width'
378                                 outer_width = k
379                                 width = v - left_width - right_width
380                 if (!width) && (!outer_width)
381                         if parent_width
382                                 outer_width = parent_width
383                         else
384                                 error("Couldn't figure out width of " + selector);
385                 if outer_width && !width
386                         width = outer_width - left_width - right_width
387                 if width && !outer_width
388                         outer_width = width + left_width + right_width
389                 unless parent_width // should only happen at top level
390                         parent_width = outer_width
391                 top['css'][selector] = css_rules
392                 for k, v in node
393                         if k != 'outer-width' && k != 'outer_width' && k != 'border-width' && k != 'border-left-width' && k != 'border-right-width'
394                                 if typeof(v) == 'unit' && unit(v) == 'px'
395                                         responsive_css_rules[k] = floor(unit((v / parent_width) * 100, '%'), 4)
396                 top['responsive_css'][selector] = responsive_css_rules
397                 if 'child' in node
398                         child_width = wfpl_columns_helper(top, node.child, selector, width, width)
399                         _columns_recurser(rargs)
400                         if child_width != width
401                                 error(selector + " is the is wrong width. Expected: " + width + ", got: " + rargs.ret.w)
402                 top['widths'][selector] = width
403                 return outer_width
404         if node.type == 'alternatives'
405                 for name, child in node
406                         if name != 'type'
407                                 child_selector = selector + '.' + name
408                                 child_width = wfpl_columns_helper(top, child, child_selector, parent_width, expected_width)
409                                 if expected_width
410                                         if child_width != expected_width
411                                                 error(child_selector + " is wrong width. Expected: " + expected_width + ", got: " + child_width)
412                                 else
413                                         expected_width = child_width
414                                 unless parent_width
415                                         parent_width = child_width
416                 return expected_width
417         if node.type == 'series'
418                 series_width = 0px
419                 for name, child in node
420                         if name != 'type'
421                                 if selector == ''
422                                         child_selector = '.' + name
423                                 else
424                                         child_selector = selector + ' > .' + name
425                                 child_width = wfpl_columns_helper(top, child, child_selector, parent_width, null)
426                                 series_width += child_width
427                 if expected_width
428                         if series_width != expected_width
429                                 error(selector + " series total wrong. expected: " + expected_width + ", got: " + series_width);
430                 return series_width
431         return // never reached with valid data
432
433 wfpl_columns(hash)
434         top = { // contains settings, and is returned from porcelain
435                 responsive: responsive
436                 width: null
437                 css: {}
438                 responsive_css: {}
439                 widths: {}
440         }
441         top.width = wfpl_columns_helper(top, hash, '', null, null)
442         return top