├── .DS_Store ├── CNAME ├── LICENSE ├── README.md ├── aspecty.js ├── cssplus-logo.png ├── cssplus-logo.svg ├── cursory.js ├── index.html ├── index.js ├── package.json ├── scrollery.js ├── selectory.js ├── test ├── aspecty.html ├── cursory.html ├── scrollery.html ├── selectory.html ├── varsity.html └── xpathy.html ├── varsity.js └── xpathy.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomhodgins/cssplus/c4a6ec369801838d95074b62c7ddca3a6bd3bbab/.DS_Store -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | csspl.us -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tommy Hodgins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](http://i.imgur.com/mOpaudj.png) 2 | 3 | # CSSplus 4 | 5 | CSSplus is a collection of CSS Reprocessor plugins that dynamically update CSS variables. 6 | 7 | The plugins are designed to be used in tandem, though it's possible to pick and choose which plugins you want to include in each project. 8 | 9 | Included are the following plugins: 10 | 11 | - [**Aspecty:** aspect ratio property](#aspecty-an-aspect-ratio-property) 12 | - [**Cursory:** mouse/touch cursor variables](#cursory-mousetouch-cursor-variables) 13 | - [**Scrollery:** scroll position variables](#scrollery-scroll-position-variables) 14 | - [**Selectory:** a selector resolver](#selectory-a-selector-resolver) 15 | - [**Varsity:** scoped variables](#varsity-scoped-variables) 16 | - [**Xpathy:** XPath selectors in CSS](#xpathy-xpath-selectors-in-css) 17 | 18 | ## Usage 19 | 20 | 21 | ### NPM 22 | 23 | If you are using NPM you can include all CSSplus plugins by including the entire package: 24 | 25 | ```javascript 26 | const cssplus = require('cssplus') 27 | ``` 28 | 29 | This will import all CSSplus plugins and make them available to be used in your own code as: 30 | 31 | - `cssplus.aspecty` 32 | - `cssplus.cursory` 33 | - `cssplus.scrollery` 34 | - `cssplus.selectory` 35 | - `cssplus.varsity` 36 | - `cssplus.xpathy` 37 | 38 | But if you want to include the plugins individually, you can use the `module/submodule` syntax: 39 | 40 | ```javascript 41 | const selectory = require('cssplus/selectory') 42 | ``` 43 | 44 | And this means the Selectory plugin is available to be used in your code as: 45 | 46 | ```javascript 47 | selectory.load() 48 | ``` 49 | 50 | 51 | ### Global JavaScript 52 | 53 | To include CSSplus plugins globally (outside of a bundler like Webpack or Browserify) you must include a ` 57 | ``` 58 | 59 | To include all CSSplus plugins, you'll need to include links to the following files: 60 | 61 | ```html 62 | 63 | 64 | 65 | 66 | 67 | 68 | ``` 69 | 70 | 71 | ## Aspecty: an aspect ratio property 72 | 73 | Aspecty is a CSS reprocessor that adds support for an aspect-ratio property using JS. This plugin allows you to define a desired aspect-ratio for an element, based on its rendered width on the page. 74 | 75 | For any element with an aspect ratio defined, event listeners will be added to reprocess the styles on the following events: 76 | 77 | - `mouseenter` 78 | - `mouseleave` 79 | - `mousedown` 80 | - `mouseup` 81 | - `focus` 82 | - `blur` 83 | 84 | By default, Aspecty will reprocess aspect ratios by watching the following events: 85 | 86 | - `load` 87 | - `resize` 88 | - `input` 89 | - `click` 90 | 91 | To run Aspecty whenever you want, use the `aspecty.load()` function in JS. 92 | 93 | The aspect ratio property can be used in CSS with the property name `--aspect-ratio` and a ratio, expressed as width and height as unitless numbers, separated by a slash `/`: 94 | 95 | ```css 96 | --aspect-ratio: width/height; 97 | ``` 98 | 99 | You can use it in CSS like this: 100 | 101 | ```css 102 | div { 103 | background: lime; 104 | --aspect-ratio: 16/9; 105 | } 106 | ``` 107 | 108 | Aspecty will look through the document for any element matching the selector (in this case `div`) and create a new rule with a `height` value calculated based on each matching element's `offsetWidth` divided by the aspect ratio defined in CSS. To animate the effect of the `--aspect-ratio` property, which is actually applying via `height`, it is necessary to set a `transition` on the `height` property like this: 109 | 110 | ```css 111 | transition: height .2s ease-in-out; 112 | ``` 113 | Test available at: [test/aspecty.html](http://tomhodgins.github.io/cssplus/test/aspecty.html) 114 | 115 | ## Cursory: mouse/touch cursor variables 116 | 117 | Cursory is a CSS reprocessor that makes the following JS values available as CSS variables: 118 | 119 | - `cursorX` 120 | - `cursorY` 121 | - `innerWidth` 122 | - `innerHeight` 123 | - `clicked` 124 | 125 | These can be used as CSS variables with the following names: 126 | 127 | - `--cursorX` 128 | - `--cursorY` 129 | - `--innerWidth` 130 | - `--innerHeight` 131 | - `--clicked` 132 | 133 | These variables are updated at the following events: 134 | 135 | - `mousemove` 136 | - `touchmove` 137 | 138 | In addition, the `--clicked` variable is changed from `0` to `1` between the `mousedown` and `touchstart` events and the corresponding `mouseup` or `touchend` events. This allows you to use the `var(--clicked)` ratio as a `1` or `0` in your CSS `calc()` functions, or as a value for `opacity:;` fairly easily. 139 | 140 | To run Cursory whenever you want, use the `cursory.load()` function in JS. 141 | 142 | To make an element like `div` follow the cursor position when using `cursory`, use CSS with variables like this: 143 | 144 | ```css 145 | div { 146 | width: 10px; 147 | height: 10px; 148 | position: fixed; 149 | background: black; 150 | top: calc(var(--cursorY) * 1px); 151 | left: calc(var(--cursorX) * 1px); 152 | } 153 | ``` 154 | 155 | Test available at: [test/cursory.html](http://tomhodgins.github.io/cssplus/test/cursory.html) 156 | 157 | 158 | ## Scrollery: scroll position variables 159 | 160 | Scrollery is a CSS reprocessor that makes the following JS values available as CSS variables for any element you tell the plugin to watch: 161 | 162 | - `scrollWidth` 163 | - `scrollHeight` 164 | - `scrollLeft` 165 | - `scrollTop` 166 | 167 | To have `scrollery` watch an element, you need to give that element a unique identifier, as well as add the `data-scrollery` attribute. The plugin will use either the value of the `data-scrollery` attribute, or else the value of the `id` (if defined) for an element. 168 | 169 | By default, Scrollery will watch 0 elements. If you add a `data-scrollery` attribute to either the `` or `` element it will attach an event listener for the `scroll` event on the `window`, otherwise if you add the `data-scrollery` attribute to other elements it will add a `scroll` listener to that element. 170 | 171 | To run Scrollery whenever you want, use the `scrollery.load()` function in JS. 172 | 173 | ```html 174 |
175 | ``` 176 | 177 | And the following example are both equivalent, and resolve to a name of `example`: 178 | 179 | ```html 180 |
181 | ``` 182 | 183 | Once the plugin is aware of an element to watch, and the unique name of that element, it will make the above values available in the following format: `--name-value`, for `example`: 184 | 185 | - `--example-scrollWidth` 186 | - `--example-scrollHeight` 187 | - `--example-scrollTop` 188 | - `--example-scrollLeft` 189 | 190 | Test available at: [test/scrollery.html](http://tomhodgins.github.io/cssplus/test/scrollery.html) 191 | 192 | 193 | ## Selectory: a selector resolver 194 | 195 | Selectory is a CSS reprocessor that resolves selectors using JS. This plugin will read CSS selectors that end with a `[test]` attribute and use JavaScript to determine whether or not to apply that style to elements matching the other part of that selector. For example, the JS test `1 == 1` will always resolve to `true`, so a selector written for `div[test="1 == 1"] {}` will always apply to each `div` element. 196 | 197 | By default, Selectory will reprocess selectors by watching the following events: 198 | 199 | - `load` 200 | - `resize` 201 | - `input` 202 | - `click` 203 | 204 | To run Selectory whenever you want, use the `selectory.load()` function in JS. 205 | 206 | Other things you can do with Selectory include: 207 | 208 | Apply a rule to a `div` when it is wider than 300px: 209 | 210 | ```css 211 | div[test="this.offsetWidth > 300"] { 212 | background: orange; 213 | } 214 | ``` 215 | 216 | Apply a rule to an `input` when its `value=""` attribute is greater than `30`: 217 | 218 | ```css 219 | input[test="this.value > 30"] { 220 | background: lime; 221 | } 222 | 223 | ``` 224 | 225 | Apply a rule to an `input` when it has a `value=""` attribute zero characters long: 226 | 227 | ```css 228 | input[test="this.value.length == 0"] { 229 | background: purple; 230 | } 231 | ``` 232 | 233 | Apply a rule to an `input` when its `value=""` attribute is more than 5 characters long: 234 | 235 | ```css 236 | input[test="5 < this.value.length"] { 237 | background: turquoise; 238 | } 239 | ``` 240 | 241 | Apply a rule to an `h3` element when it contains at least one `span` element: 242 | 243 | ```css 244 | h3[test="(this.querySelector('span'))"] { 245 | color: red; 246 | } 247 | ``` 248 | 249 | It is limited what selectors you can use with Selectory, things like `:hover` and pseudo-classes tend not to work as well. As well the parsing only allows for 1 test per selector, and complex selectors may not work as intended. Using `selector[test=""] {}` with a simple selector is best. 250 | 251 | Test available at: [test/selectory.html](http://tomhodgins.github.io/cssplus/test/selectory.html) 252 | 253 | 254 | ## Varsity: scoped variables 255 | 256 | Varsity is a CSS reprocessor that makes the following JS values available as CSS variables for any element you tell the plugin to watch: 257 | 258 | - `offsetWidth` 259 | - `offsetHeight` 260 | - `offsetLeft` 261 | - `offsetTop` 262 | - `aspect-ratio` 263 | - `characters` 264 | - `children` 265 | - `value` 266 | 267 | By default, Varsity will reprocess selectors by watching the following events: 268 | 269 | - `load` 270 | - `resize` 271 | - `input` 272 | - `click` 273 | 274 | To run Varsity whenever you want, use the `varsity.load()` function in JS. 275 | 276 | To have `varsity` watch an element, you need to give that element a unique identifier, as well as add the `data-varsity` attribute. The plugin will use either the value of the `data-varsity` attribute, or else the value of the `id` (if defined) for an element. 277 | 278 | ```html 279 |
280 | ``` 281 | 282 | And the following example are both equivalent, and resolve to a name of `example`: 283 | 284 | ```html 285 |
286 | ``` 287 | 288 | Once the plugin is aware of an element to watch, and the unique name of that element, it will make the above values available in the following format: `--name-value`, for `example`: 289 | 290 | - `--example-offsetWidth` 291 | - `--example-offsetHeight` 292 | - `--example-offsetLeft` 293 | - `--example-offsetTop` 294 | - `--example-aspect-ratio` 295 | - `--example-characters` 296 | - `--example-children` 297 | - `--example-value` 298 | 299 | Test available at: [test/varsity.html](http://tomhodgins.github.io/cssplus/test/varsity.html) 300 | 301 | 302 | ## XPathy: XPath selectors in CSS 303 | 304 | XPathy is a CSS reprocessor that resolves selectors using XPath. This plugin will read CSS selectors that end with a `[xpath]` attribute and use JavaScript and XPath to determine whether or not to apply that style to elements matching the other part of that selector. For example, the XPath selector `//div` will always resolve to `div`, so a selector written for `div [xpath="//div"] {}` will always apply to each `div div {}` element. 305 | 306 | By default, XPathy will reprocess selectors by watching the following events: 307 | 308 | - `load` 309 | - `resize` 310 | - `input` 311 | - `click` 312 | 313 | To run XPathy whenever you want, use the `xpathy.load()` function in JS. 314 | 315 | Other things you can do with XPathy include: 316 | 317 | Select all `span` tags with the XPath `//span`: 318 | 319 | ```css 320 | [xpath="//span"] { 321 | color: violet; 322 | } 323 | ``` 324 | 325 | Select all elements with a class name of `demo-class` with the XPath `//*[@class='demo-class']`: 326 | 327 | ```css 328 | [xpath="//*[@class='demo-class']"] { 329 | color: lime; 330 | } 331 | ``` 332 | 333 | Select an element with a text content of 'Demo Content' with the XPath `//*[text()='Demo Content']`: 334 | 335 | ```css 336 | [xpath="//*[text()='Demo Content']"] { 337 | color: violet; 338 | } 339 | ``` 340 | 341 | Select the parent element of another element with the XPath `/..`: 342 | 343 | ```css 344 | [xpath="//*[@class='child']/.."] { 345 | border: 1px solid lime; 346 | } 347 | ``` 348 | 349 | Compare attribute values as numbers with operators like `>` and `<`: 350 | 351 | ```css 352 | [xpath="//*[@data-price > 3]"] { 353 | color: violet; 354 | } 355 | ``` 356 | 357 | Select elements based on the number of children they contain with an XPath like `//ul[li[4]]`: 358 | 359 | ```css 360 | [xpath="//ul[li[4]]"] { 361 | color: lime; 362 | } 363 | ``` 364 | 365 | Test available at: [test/xpathy.html](http://tomhodgins.github.io/cssplus/test/xpathy.html) 366 | 367 | 368 | ## Browser support 369 | 370 | These plugins are written in ES6, and intended to be used in modern browsers (Chrome, Safari, Firefox, Edge) without transpilation. Many of these plugins make use of CSS Custom Properties (CSS variables) for functionality, so any browser that doesn't support these features will have trouble with. 371 | 372 | As far as I am aware, the only browser that supports CSS Custom Properties but not the ES6 features used (where transpiling to ES5 might improve support) is for Mobile Safari (iOS) support. 373 | 374 | Currently all of these plugins are under active development and things are shifting around quite a bit so browser support may change with each release. 375 | 376 | 377 | ## Demos 378 | 379 | For 20+ CSSplus demos, check out the [CSSPlus demos collection](http://codepen.io/collection/XLbNKz/) on Codepen. -------------------------------------------------------------------------------- /aspecty.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | # Aspecty 4 | ## version 0.1.1 5 | 6 | Aspecty is a CSS reprocessor that adds support for an aspect-ratio property using JS. This plugin allows you to define a desired aspect-ratio for an element, based on its rendered width on the page. 7 | 8 | For any element with an aspect ratio defined, event listeners will be added to reprocess the styles on the following events: 9 | 10 | - `mouseenter` 11 | - `mouseleave` 12 | - `mousedown` 13 | - `mouseup` 14 | - `focus` 15 | - `blur` 16 | 17 | By default, Aspecty will reprocess aspect ratios by watching the following events: 18 | 19 | - `load` 20 | - `resize` 21 | - `input` 22 | - `click` 23 | 24 | To run Aspecty whenever you want, use the `aspecty.load()` function in JS. 25 | 26 | The aspect ratio property can be used in CSS with the property name `--aspect-ratio` and a ratio, expressed as width and height as unitless numbers, separated by a slash `/`: 27 | 28 | --aspect-ratio: width/height; 29 | 30 | You can use it in CSS like this: 31 | 32 | div { 33 | background: lime; 34 | --aspect-ratio: 16/9; 35 | } 36 | 37 | Aspecty will look through the document for any element matching the selector (in this case `div`) and create a new rule with a `height` value calculated based on each matching element's `offsetWidth` divided by the aspect ratio defined in CSS. 38 | 39 | - https://github.com/tomhodgins/cssplus 40 | 41 | Author: Tommy Hodgins 42 | 43 | License: MIT 44 | 45 | */ 46 | 47 | // Uses Node, AMD or browser globals to create a module 48 | (function (root, factory) { 49 | 50 | if (typeof define === 'function' && define.amd) { 51 | 52 | // AMD: Register as an anonymous module 53 | define([], factory) 54 | 55 | } else if (typeof module === 'object' && module.exports) { 56 | 57 | // Node: Does not work with strict CommonJS, but 58 | // only CommonJS-like environments that support module.exports, 59 | // like Node 60 | module.exports = factory() 61 | 62 | } else { 63 | 64 | // Browser globals (root is window) 65 | root.aspecty = factory() 66 | 67 | } 68 | 69 | }(this, function() { 70 | 71 | const aspecty = {} 72 | 73 | aspecty.style = '' 74 | aspecty.count = 0 75 | 76 | aspecty.load = () => { 77 | 78 | // Find (or create) style tag to populate 79 | const style_tag = document.querySelector('[data-aspecty-style]') || (() => { 80 | 81 | const tag = document.createElement('style') 82 | tag.setAttribute('data-aspecty-style', '') 83 | document.head.appendChild(tag) 84 | return tag 85 | 86 | })() 87 | 88 | // Reset plugin styles and element count 89 | aspecty.style = '' 90 | aspecty.count = 0 91 | 92 | // Reset count on [data-aspecty] elements in DOM 93 | Array.from(document.querySelectorAll('[data-aspecty]'), tag => { 94 | 95 | tag.setAttribute('data-aspecty', '') 96 | 97 | }) 98 | 99 | aspecty.findRules() 100 | 101 | // Populate style tag with style 102 | if (style_tag.innerHTML !== `\n${aspecty.style}\n`) { 103 | 104 | style_tag.innerHTML = `\n${aspecty.style}\n` 105 | 106 | } 107 | 108 | } 109 | 110 | aspecty.findRules = () => { 111 | 112 | // For each stylesheet 113 | Array.from(document.styleSheets, sheet => { 114 | 115 | // For each rule 116 | sheet.cssRules && Array.from(sheet.cssRules, rule => { 117 | 118 | aspecty.process(rule) 119 | 120 | }) 121 | 122 | }) 123 | 124 | } 125 | 126 | aspecty.process = rule => { 127 | 128 | // If rule is a qualified rule, process it 129 | if (rule.type === 1) { 130 | 131 | aspecty.style += aspecty.transform(rule) 132 | 133 | } 134 | 135 | // If rule is an at-rule, find all qualified rules inside 136 | if (rule.type === 4) { 137 | 138 | let css_rules = '' 139 | 140 | // Remember media query text 141 | let mediaText = rule.media.mediaText 142 | 143 | // If there are qualified rules, find all rules 144 | rule.cssRules && Array.from(rule.cssRules, mediaRule => { 145 | 146 | css_rules += aspecty.transform(mediaRule) 147 | 148 | }) 149 | 150 | // If there is at least one new rule, wrap in at-rule with media text 151 | if (css_rules.length > 0) { 152 | 153 | aspecty.style += ` @media ${mediaText} {\n${css_rules.replace(/^(.*)$/gmi,' $1')}\n }\n` 154 | 155 | } 156 | 157 | } 158 | 159 | } 160 | 161 | aspecty.transform = rule => { 162 | 163 | let newRule = '' 164 | 165 | let selector = rule.selectorText.replace(/(.*)\s{/gi, '$1') 166 | let ruleText = rule.cssText.replace(/.*\{(.*)\}/gi, '$1') 167 | 168 | let elWidth = 0 169 | let width = 0 170 | let height = 0 171 | let specificity = '' 172 | 173 | // For each rule, search for `-aspect-ratio` 174 | ruleText.replace(/(--aspect-ratio:\s*)(\d\.*\d*\s*\/\s*\d\.*\d*)(\s*\!important)*\s*(?:;|\})/i, (string, property, value, important) => { 175 | 176 | // Extract width, height, and !important from value 177 | width = parseInt(value.split('/')[0]) 178 | height = parseInt(value.split('/')[1]) 179 | specificity = important || '' 180 | 181 | let selectorList = (selector.split(',')) 182 | 183 | selectorList.map(partial => { 184 | 185 | // For each element matching this selector 186 | Array.from(document.querySelectorAll(partial), (tag, i) => { 187 | 188 | elWidth = tag.offsetWidth || 0 189 | 190 | // If the matching element has a non-zero width 191 | if (elWidth) { 192 | 193 | // Increment the plugin element count 194 | aspecty.count++ 195 | 196 | // Create a new selector for our new CSS rule 197 | let newSelector = `${partial}[data-aspecty~="${aspecty.count}"]` 198 | 199 | // If element has no preexisting attribute, add event listeners 200 | if (!tag.getAttribute('data-aspecty')) { 201 | 202 | tag.addEventListener('mouseenter', aspecty.load) 203 | tag.addEventListener('mouseleave', aspecty.load) 204 | tag.addEventListener('mousedown', aspecty.load) 205 | tag.addEventListener('mouseup', aspecty.load) 206 | tag.addEventListener('focus', aspecty.load) 207 | tag.addEventListener('blur', aspecty.load) 208 | 209 | } 210 | 211 | // Mark matching element with attribute and plugin element count 212 | let currentAttr = tag.getAttribute('data-aspecty') 213 | tag.setAttribute('data-aspecty', `${currentAttr} ${aspecty.count}`) 214 | 215 | // Height for new rule from offsetWidth, divided by aspect ratio 216 | let newHeight = elWidth / (width/height) 217 | 218 | // Create new `height` declaration with new value and !important text 219 | let newRuleText = `height: ${newHeight}px${specificity};` 220 | 221 | // And add our new rule to the rule list 222 | newRule += `\n/* ${selector} */\n${newSelector} {\n ${newRuleText}\n}\n` 223 | 224 | } 225 | 226 | }) 227 | 228 | }) 229 | 230 | }) 231 | 232 | return newRule 233 | 234 | } 235 | 236 | // Update every `load`, `resize`, `input`, and `click` 237 | window.addEventListener('load', aspecty.load) 238 | window.addEventListener('resize', aspecty.load) 239 | window.addEventListener('input', aspecty.load) 240 | window.addEventListener('click', aspecty.load) 241 | 242 | return aspecty 243 | 244 | })) -------------------------------------------------------------------------------- /cssplus-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomhodgins/cssplus/c4a6ec369801838d95074b62c7ddca3a6bd3bbab/cssplus-logo.png -------------------------------------------------------------------------------- /cssplus-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /cursory.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | # Cursory 4 | ## version 0.1.1 5 | 6 | Cursory is a CSS reprocessor that makes the following JS values available as CSS variables: 7 | 8 | - `cursorX` 9 | - `cursorY` 10 | - `innerWidth` 11 | - `innerHeight` 12 | - `clicked` 13 | 14 | These can be used as CSS variables with the following names: 15 | 16 | - `--cursorX` 17 | - `--cursorY` 18 | - `--innerWidth` 19 | - `--innerHeight` 20 | - `--clicked` 21 | 22 | These variables are updated at the following events: 23 | 24 | - `mousemove` 25 | - `touchmove` 26 | 27 | In addition, the `--clicked` variable is changed from `0` to `1` between the `mousedown` and `touchstart` events and the corresponding `mouseup` or `touchend` events. 28 | 29 | To run Cursory whenever you want, use the `cursory()` function in JS. 30 | 31 | - https://github.com/tomhodgins/cssplus 32 | 33 | Author: Tommy Hodgins 34 | 35 | License: MIT 36 | 37 | */ 38 | 39 | // Uses Node, AMD or browser globals to create a module 40 | (function (root, factory) { 41 | 42 | if (typeof define === 'function' && define.amd) { 43 | 44 | // AMD: Register as an anonymous module 45 | define([], factory) 46 | 47 | } else if (typeof module === 'object' && module.exports) { 48 | 49 | // Node: Does not work with strict CommonJS, but 50 | // only CommonJS-like environments that support module.exports, 51 | // like Node 52 | module.exports = factory() 53 | 54 | } else { 55 | 56 | // Browser globals (root is window) 57 | root.cursory = factory() 58 | 59 | } 60 | 61 | }(this, function(e) { 62 | 63 | const cursory = {} 64 | 65 | cursory.style = '' 66 | 67 | cursory.load = (e) => { 68 | 69 | // Find (or create) style tag to populate 70 | const style_tag = document.querySelector('[data-cursory-style]') || (() => { 71 | 72 | const tag = document.createElement('style') 73 | tag.setAttribute('data-cursory-style', '') 74 | document.head.appendChild(tag) 75 | return tag 76 | 77 | })() 78 | 79 | cursory.style = '' 80 | 81 | cursory.process(e) 82 | 83 | // Populate style tag with current CSS string 84 | style_tag.innerHTML = `:root {\n${cursory.style.replace(/^/gm,' ')}\n}` 85 | 86 | } 87 | 88 | cursory.process = e => { 89 | 90 | let newRule = cursory.transform(e) 91 | 92 | if (newRule.length > 0) { 93 | 94 | cursory.style = newRule 95 | 96 | } 97 | 98 | } 99 | 100 | cursory.transform = e => { 101 | 102 | let newRule = '' 103 | 104 | // List cursor position, window dimensions, and click status 105 | newRule = ` 106 | 107 | --cursorX: ${e.clientX || (e.touches ? e.touches[0].clientX : innerWidth/2)}; 108 | --cursorY: ${e.clientY || (e.touches ? e.touches[0].clientY : innerHeight/2)}; 109 | --innerWidth: ${innerWidth}; 110 | --innerHeight: ${innerHeight}; 111 | --clicked: ${cursory.clicked}; 112 | 113 | ` 114 | 115 | return newRule 116 | 117 | } 118 | 119 | // Set status of `clicked` variable 120 | cursory.clicked = 0 121 | 122 | cursory.startClick = e => { 123 | cursory.clicked = 1 124 | cursory.load(e) 125 | } 126 | 127 | cursory.endClick = e => { 128 | cursory.clicked = 0 129 | cursory.load(e) 130 | } 131 | 132 | // Update on `load` and `resize` 133 | window.addEventListener('load', cursory.load) 134 | window.addEventListener('resize', cursory.load) 135 | 136 | // Update every `mousemove` and `touchmove` 137 | window.addEventListener('mousemove', cursory.load) 138 | window.addEventListener('touchmove', cursory.load) 139 | 140 | // Start click on `mousedown` and `touchstart` 141 | window.addEventListener('mousedown', cursory.startClick) 142 | window.addEventListener('touchstart', cursory.startClick) 143 | 144 | // End click on `mouseup` and `touchend` 145 | window.addEventListener('mouseup', cursory.endClick) 146 | window.addEventListener('touchend', cursory.endClick) 147 | 148 | return cursory 149 | 150 | })) 151 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CSSplus - A family of CSS Reprocessor Plugins 7 | 8 | 165 | 166 | 170 | 171 |

CSSplus

172 | A family of CSS Reprocessor Plugins 173 | 174 |

About CSSplus

175 |

CSSplus is a family of CSS reprocessing plugins that give you more power when writing CSS. It's called “CSSplus” because it's CSS plus JavaScript, the method most of these plugins use to add power to CSS is by having JavaScript dynamically update CSS variables with information about the browser and elements.

176 |

By creating a library of single-purpose plugins, CSSplus makes it simple to use only what you need. Each plugin has been designed with the other plugins in mind, so you can use them in tandem in any combination with each other without worry that they will conflict.

177 |

If you're looking for a little more power than what CSSplus offers or you want to use similar functionality in browsers that don't support CSS variables, everything CSSplus does (and more) is also possible with EQCSS, a CSS reprocessor that runs all browsers IE8 and up.

178 | 179 |

Plugins

180 |

Included are the following plugins:

181 | 189 | 190 |

How to use CSSplus

191 |

Github

192 |

You can download CSSPlus from Github: CSSplus on Github

193 | 194 |

NPM

195 |

If you are using CSSplus on NPM you can include all CSSplus plugins by including the entire package:

196 |
const cssplus = require('cssplus')
197 |

This will import all CSSplus plugins and make them available to be used in your own code as:

198 | 206 |

But if you want to include the plugins individually, you can use the module/submodule syntax:

207 |
const selectory = require('cssplus/selectory')
208 |

And this means the Selectory plugin is available to be used in your code as:

209 |
selectory.load()
210 | 211 |

Global JavaScript

212 |

To include CSSplus plugins globally (outside of a bundler like Webpack or Browserify) you must include a <script> tag to each plugin you want to use. If you want to include just Selectory for example you would include just the one file like this:

213 |
<script src=cssplus/selectory.js></script>
214 |

To include all CSSplus plugins, you'll need to include links to the following files:

215 | 223 | 224 |

Aspecty

225 |

Aspecty is a CSS reprocessor that adds support for an aspect-ratio property using JS. This plugin allows you to define a desired aspect-ratio for an element, based on its rendered width on the page.

226 |

By default, Aspecty will reprocess aspect ratios by watching the following events:

227 | 233 |

For any element with an aspect ratio defined, event listeners will be added to reprocess the styles on the following events:

234 | 242 |

To run Aspecty whenever you want, use the aspecty.load() function in JS.

243 |

The aspect ratio property can be used in CSS with the property name --aspect-ratio and a ratio, expressed as width and height as unitless numbers, separated by a slash /:

244 |
--aspect-ratio: width/height;
245 |

You can use it in CSS like this:

246 |
div {
247 |   background: lime;
248 |   --aspect-ratio: 16/9;
249 | }
250 |

Aspecty will look through the document for any element matching the selector (in this case div) and create a new rule with a height value calculated based on each matching element's offsetWidth divided by the aspect ratio defined in CSS. To animate the effect of the --aspect-ratio property, which is actually applying via height, it is necessary to set a transition on the height property like this:

251 |
transition: height .2s ease-in-out;
252 |

Test available at: test/aspecty.html

253 | 254 |

Cursory

255 |

Cursory is a CSS reprocessor that makes the following JS values available as CSS variables:

256 | 263 |

These can be used as CSS variables with the following names:

264 | 271 |

These variables are updated at the following events:

272 | 276 |

In addition, the --clicked variable is changed from 0 to 1 between the mousedown and touchstart events and the corresponding mouseup or touchend events. This allows you to use the var(--clicked) ratio as a 1 or 0 in your CSS calc() functions, or as a value for opacity:; fairly easily.

277 |

To run Cursory whenever you want, use the cursory() function in JS.

278 |

To make an element like div follow the cursor position when using cursory, use CSS with variables like this:

279 |
div {
280 |   width: 10px;
281 |   height: 10px;
282 |   position: fixed;
283 |   background: black;
284 |   top: calc(var(--cursorY) * 1px);
285 |   left: calc(var(--cursorX) * 1px);
286 | }
287 |

Test available at: test/cursory.html

288 | 289 |

Scrollery

290 |

Scrollery is a CSS reprocessor that makes the following JS values available as CSS variables for any element you tell the plugin to watch:

291 | 297 |

To have scrollery watch an element, you need to give that element a unique identifier, as well as add the data-scrollery attribute. The plugin will use either the value of the data-scrollery attribute, or else the value of the id (if defined) for an element.

298 |

By default, Scrollery will watch zero elements. If you add a data-scrollery attribute to either the <html> or <body> element it will attach an event listener for the scroll event on the window, otherwise if you add the data-scrollery attribute to other elements it will add a scroll listener to that element.

299 |

To run Scrollery whenever you want, use the scrollery() function in JS.

300 |
<div id=example data-scrollery></div>
301 |

And the following example are both equivalent, and resolve to a name of example:

302 |
<div data-scrollery=example></div>
303 |

Once the plugin is aware of an element to watch, and the unique name of that element, it will make the above values available in the following format: --name-value, for example:

304 | 310 |

Test available at: test/scrollery.html

311 | 312 |

Selectory

313 |

Selectory is a CSS reprocessor that resolves selectors using JS. This plugin will read CSS selectors that end with a [test] attribute and use JavaScript to determine whether or not to apply that style to elements matching the other part of that selector. For example, the JS test 1 == 1 will always resolve to true, so a selector written for div[test="1 == 1"] {} will always apply to each div element.

314 |

By default, Selectory will reprocess selectors by watching the following events:

315 | 321 |

To run Selectory whenever you want, use the selectory() function in JS.

322 |

Other things you can do with Selectory include, appling a rule to a div when it is wider than 300px:

323 |
div[test="this.offsetWidth > 300"] {
324 |   background: orange;
325 | }
326 |

Apply a rule to an input when its value="" attribute is greater than 30:

327 |
input[test="this.value > 30"] {
328 |   background: lime;
329 | }
330 |

Apply a rule to an input when it has a value="" attribute zero characters long:

331 |
input[test="this.value.length == 0"] {
332 |   background: purple;
333 | }
334 |

Apply a rule to an input when its value="" attribute is more than 5 characters long:

335 |
input[test="5 < this.value.length"] {
336 |   background: turquoise;
337 | }
338 |

Apply a rule to an h3 element when it contains at least one span element:

339 |
h3[test="(this.querySelector('span'))"] {
340 |   color: red;
341 | }
342 |

It is limited what selectors you can use with Selectory, things like :hover and pseudo-classes tend not to work as well. As well the parsing only allows for 1 test per selector, and complex selectors may not work as intended. Using selector[test=""] {} with a simple selector is best.

343 |

Test available at: test/selectory.html

344 | 345 |

Varsity

346 |

Varsity is a CSS reprocessor that makes the following JS values available as CSS variables for any element you tell the plugin to watch:

347 | 357 |

By default, Varsity will reprocess selectors by watching the following events:

358 | 364 |

To run Varsity whenever you want, use the varsity() function in JS.

365 |

To have varsity watch an element, you need to give that element a unique identifier, as well as add the data-varsity attribute. The plugin will use either the value of the data-varsity attribute, or else the value of the id (if defined) for an element.

366 |
<div id=example data-varsity></div>
367 |

And the following example are both equivalent, and resolve to a name of example:

368 |
<div data-varsity=example></div>
369 |

Once the plugin is aware of an element to watch, and the unique name of that element, it will make the above values available in the following format: --name-value, for example:

370 | 380 |

Test available at: test/varsity.html

381 | 382 |

XPathy

383 |

XPathy is a CSS reprocessor that resolves selectors using XPath. This plugin will read CSS selectors that end with a [xpath] attribute and use JavaScript and XPath to determine whether or not to apply that style to elements matching the other part of that selector. For example, the XPath selector //div will always resolve to div, so a selector written for div [xpath="//div"] {} will always apply to each div div {} element.

384 |

By default, XPathy will reprocess selectors by watching the following events:

385 | 391 |

To run XPathy whenever you want, use the xpathy.load() function in JS.

392 |

Other things you can do with XPathy include:

393 |

Select all span tags with the XPath //span:

394 |
[xpath="//span"] {
395 |   color: violet;
396 | }
397 |

Select all elements with a class name of demo-class with the XPath //*[@class='demo-class']:

398 |
[xpath="//*[@class='demo-class']"] {
399 |   color: lime;
400 | }
401 |

Select an element with a text content of Demo Content with the XPath //*[text()='Demo Content']:

402 |
[xpath="//*[text()='Demo Content']"] {
403 |   color: violet;
404 | }
405 |

Select the parent element of another element with the XPath /..:

406 |
[xpath="//*[@class='child']/.."] {
407 |   border: 1px solid lime;
408 | }
409 |

Compare attribute values as numbers with operators like > and <:

410 |
[xpath="//*[@data-price > 3]"] {
411 |   color: violet;
412 | }
413 |

Select elements based on the number of children they contain with an XPath like //ul[li[4]]:

414 |
[xpath="//ul[li[4]]"] {
415 |   color: lime;
416 | }
417 |

Test available at: test/xpathy.html

418 | 419 |

Demos

420 | 421 |

Cursor Based Box-Shadow

422 |

See the Pen Cursor Based Box-Shadow with Varsity + Cursory [CSS Variables] by Tommy Hodgins (@tomhodgins) on CodePen.

423 | 424 | 425 |

Cursory Cursor

426 |

See the Pen Cursory - CSS Variable Reprocessor for your Cursor by Tommy Hodgins (@tomhodgins) on CodePen.

427 | 428 | 429 | 430 | 431 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const aspecty = require('./aspecty.js') 2 | const cursory = require('./cursory.js') 3 | const scrollery = require('./scrollery.js') 4 | const selectory = require('./selectory.js') 5 | const varsity = require('./varsity.js') 6 | const xpathy = require('./xpathy.js') 7 | 8 | module.exports = { 9 | aspecty, 10 | cursory, 11 | scrollery, 12 | selectory, 13 | varsity, 14 | xpathy 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cssplus", 3 | "version": "0.1.1", 4 | "description": "CSSplus is a collection of CSS Reprocessor plugins that dynamically update CSS variables", 5 | "main": "index.js", 6 | "files": [ 7 | "aspecty.js", 8 | "cursory.js", 9 | "scrollery.js", 10 | "selectory.js", 11 | "varsity.js", 12 | "xpathy.js" 13 | ], 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/tomhodgins/cssplus.git" 20 | }, 21 | "keywords": [ 22 | "Aspecty", 23 | "Cursory", 24 | "Scrollery", 25 | "Selectory", 26 | "Varsity", 27 | "Xpathy", 28 | "CSS", 29 | "reprocessor", 30 | "selector", 31 | "resolver", 32 | "scoped", 33 | "variables", 34 | "cursor", 35 | "scroll", 36 | "selector", 37 | "css", 38 | "XPath", 39 | "aspect", 40 | "ratio", 41 | "variable" 42 | ], 43 | "author": "Tommy Hodgins", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/tomhodgins/cssplus/issues" 47 | }, 48 | "homepage": "https://github.com/tomhodgins/cssplus#readme" 49 | } 50 | -------------------------------------------------------------------------------- /scrollery.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | # Scrollery 4 | ## version 0.1.1 5 | 6 | Scrollery is a CSS reprocessor that makes the following JS values available as CSS variables for any element you tell the plugin to watch: 7 | 8 | - `scrollWidth` 9 | - `scrollHeight` 10 | - `scrollLeft` 11 | - `scrollTop` 12 | 13 | To have scrollery watch an element, you need to give that element a unique identifier, as well as add the `data-scrollery` attribute. The plugin will use either the value of the `data-scrollery` attribute, or else the value of the `id` (if defined) for an element. 14 | 15 | By default, Scrollery will watch 0 elements. If you add a `data-scrollery` attribute to either the `` or `` element it will attach an event listener for the `scroll` event on the `window`, otherwise if you add the `data-scrollery` attribute to other elements it will add a `scroll` listener to that element. 16 | 17 | - https://github.com/tomhodgins/cssplus 18 | 19 | Author: Tommy Hodgins 20 | 21 | License: MIT 22 | 23 | */ 24 | 25 | // Uses Node, AMD or browser globals to create a module 26 | (function (root, factory) { 27 | 28 | if (typeof define === 'function' && define.amd) { 29 | 30 | // AMD: Register as an anonymous module 31 | define([], factory) 32 | 33 | } else if (typeof module === 'object' && module.exports) { 34 | 35 | // Node: Does not work with strict CommonJS, but 36 | // only CommonJS-like environments that support module.exports, 37 | // like Node 38 | module.exports = factory() 39 | 40 | } else { 41 | 42 | // Browser globals (root is window) 43 | root.scrollery = factory() 44 | 45 | } 46 | 47 | }(this, function() { 48 | 49 | const scrollery = {} 50 | 51 | scrollery.style = '' 52 | 53 | scrollery.load = () => { 54 | 55 | // Find (or create) style tag to populate 56 | const style_tag = document.querySelector('[data-scrollery-style]') || (() => { 57 | 58 | const tag = document.createElement('style') 59 | tag.setAttribute('data-scrollery-style', '') 60 | document.head.appendChild(tag) 61 | return tag 62 | 63 | })() 64 | 65 | scrollery.style = '' 66 | 67 | scrollery.process() 68 | 69 | // Populate style tag with current CSS string 70 | style_tag.innerHTML = `:root {\n${scrollery.style.replace(/^/gm,' ')}\n}` 71 | 72 | } 73 | 74 | scrollery.process = () => { 75 | 76 | let css_rules = '' 77 | 78 | // For each [data-scrollery] element 79 | Array.from(document.querySelectorAll('[data-scrollery]'), (tag, i) => { 80 | 81 | // Add scroll event listeners on elements with [data-scrollery] attribute 82 | // If we haven't added an event listener yet 83 | if (tag.getAttribute('data-scrollery-listen') !== true) { 84 | 85 | // If listening to or 86 | if (tag === document.documentElement || tag === document.body) { 87 | 88 | // watch `scroll` on `window` 89 | window.addEventListener('scroll', scrollery.load) 90 | 91 | } else { 92 | 93 | // Otherwise listen to the `scroll` event on the element 94 | tag.addEventListener('scroll', scrollery.load) 95 | 96 | } 97 | 98 | // Mark that we've added an event listener here 99 | tag.setAttribute('data-scrollery-listen', 'true') 100 | 101 | } 102 | 103 | css_rules += scrollery.transform(tag, i) 104 | 105 | }) 106 | 107 | if (css_rules.length > 0) { 108 | 109 | scrollery.style += css_rules 110 | 111 | } 112 | 113 | } 114 | 115 | scrollery.transform = tag => { 116 | 117 | let newRule = '' 118 | 119 | // Set the name to the value of the data-varsity="" attribute, or the id="" if there is none 120 | let name = (tag.getAttribute('data-scrollery') || tag.id || i) 121 | 122 | // List properties of element as variables 123 | newRule += `\n/* ${name} */` 124 | + `\n--${name}-scrollWidth: ${tag.scrollWidth};` 125 | + `\n--${name}-scrollHeight: ${tag.scrollHeight};` 126 | + `\n--${name}-scrollLeft: ${tag.scrollLeft};` 127 | + `\n--${name}-scrollTop: ${tag.scrollTop};` 128 | + '\n' 129 | 130 | return newRule 131 | 132 | } 133 | 134 | // Update every `load` 135 | window.addEventListener('load', scrollery.load) 136 | window.addEventListener('resize', scrollery.load) 137 | 138 | return scrollery 139 | 140 | })) 141 | -------------------------------------------------------------------------------- /selectory.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | # Selectory 4 | ## version 0.1.1 5 | 6 | Selectory is a CSS reprocessor that resolves selectors using JS. This plugin will read CSS selectors that end with a `[test]` attribute and use JavaScript to determine whether or not to apply that style to elements matching the other part of that selector. For example, the JS test `1 == 1` will always resolve to `true`, so a selector written for `div[test="1 == 1"] {}` will always apply to each `div` element. 7 | 8 | By default, Selectory will reprocess selectors by watching the following events: 9 | 10 | - `load` 11 | - `resize` 12 | - `input` 13 | - `click` 14 | 15 | To run Selectory whenever you want, use the `selectory.load()` function in JS. 16 | 17 | - https://github.com/tomhodgins/cssplus 18 | 19 | Author: Tommy Hodgins 20 | 21 | License: MIT 22 | 23 | */ 24 | 25 | // Uses Node, AMD or browser globals to create a module 26 | (function (root, factory) { 27 | 28 | if (typeof define === 'function' && define.amd) { 29 | 30 | // AMD: Register as an anonymous module 31 | define([], factory) 32 | 33 | } else if (typeof module === 'object' && module.exports) { 34 | 35 | // Node: Does not work with strict CommonJS, but 36 | // only CommonJS-like environments that support module.exports, 37 | // like Node 38 | module.exports = factory() 39 | 40 | } else { 41 | 42 | // Browser globals (root is window) 43 | root.selectory = factory() 44 | 45 | } 46 | 47 | }(this, function() { 48 | 49 | const selectory = {} 50 | 51 | selectory.style = '' 52 | selectory.count = 0 53 | 54 | selectory.load = () => { 55 | 56 | // Find (or create) style tag to populate 57 | const style_tag = document.querySelector('[data-selectory-style]') || (() => { 58 | 59 | const tag = document.createElement('style') 60 | tag.setAttribute('data-selectory-style', '') 61 | document.head.appendChild(tag) 62 | return tag 63 | 64 | })() 65 | 66 | // Reset plugin styles and element count 67 | selectory.style = '' 68 | selectory.count = 0 69 | 70 | // Reset count on [data-selectory] elements in DOM 71 | Array.from(document.querySelectorAll('[data-selectory]'), tag => { 72 | 73 | tag.setAttribute('data-selectory', '') 74 | 75 | }) 76 | 77 | selectory.findRules() 78 | 79 | // Populate style tag with style 80 | style_tag.innerHTML = `\n${selectory.style}\n` 81 | 82 | } 83 | 84 | selectory.findRules = () => { 85 | 86 | // For each stylesheet 87 | Array.from(document.styleSheets, sheet => { 88 | 89 | // For each rule 90 | sheet.cssRules && Array.from(sheet.cssRules, rule => { 91 | 92 | selectory.process(rule) 93 | 94 | }) 95 | 96 | }) 97 | 98 | } 99 | 100 | selectory.process = rule => { 101 | 102 | // If rule is a qualified rule, process it 103 | if (rule.type === 1) { 104 | 105 | selectory.style += selectory.transform(rule) 106 | 107 | } 108 | 109 | // If rule is an at-rule, find all qualified rules inside 110 | if (rule.type === 4) { 111 | 112 | let css_rules = '' 113 | 114 | // Remember media query text 115 | let mediaText = rule.media.mediaText 116 | 117 | // If there are qualified rules, find all rules 118 | rule.cssRules && Array.from(rule.cssRules, mediaRule => { 119 | 120 | css_rules += selectory.transform(mediaRule) 121 | 122 | }) 123 | 124 | // If there is at least one new rule, wrap in at-rule with media text 125 | if (css_rules.length > 0) { 126 | 127 | selectory.style += `\n@media ${mediaText} {\n ${css_rules.replace(/^(.*)$/gmi,' $1')}\n}\n` 128 | 129 | } 130 | 131 | } 132 | 133 | } 134 | 135 | selectory.transform = rule => { 136 | 137 | let newRule = '' 138 | 139 | let selector = rule.selectorText.replace(/(.*)\s{/gi, '$1') 140 | let ruleText = rule.cssText.replace(/.*\{(.*)\}/gi, '$1') 141 | 142 | // Start a new list of matching selectors 143 | let ruleList = [] 144 | 145 | let selectorList = selector.split(',') 146 | 147 | // If `[test=` is present anywhere in the selector 148 | if (selector && selector.indexOf('[test=') !== -1) { 149 | 150 | // Extract the full selector name and test 151 | selector.replace(/^(.*)\[test=("(?:[^"]*)"|'(?:[^']*)')\].*/i, (string, selectorText, test) => { 152 | 153 | test = test.replace(/^'([^']*)'$/m, '$1') 154 | test = test.replace(/^"([^"]*)"$/m, '$1') 155 | 156 | // Use asterisk if last character is a space, +, >, or ~ 157 | selectorText = selectorText.replace(/[ \+\>\~]$/m, ' *') 158 | 159 | // Use asterisk (*) if selectorText is an empty string 160 | selectorText = selectorText === '' ? '*' : selectorText 161 | 162 | // For each tag matching the selector (minus the test) 163 | Array.from(document.querySelectorAll(selectorText), (tag, i) => { 164 | 165 | // Create a new function with our test 166 | const func = new Function(`return (${test})`) 167 | 168 | // Run the test with our matching element 169 | if (func.call(tag)) { 170 | 171 | // Increment the plugin element count 172 | selectory.count++ 173 | 174 | // Create a new selector for our new CSS rule 175 | let newSelector = selector.replace(/^(.*\[)(test=(?:"[^"]*"|'[^']*'))(\].*)$/mi, (string, before, test, after) => { 176 | console.log(after) 177 | return `${before}data-selectory~="${selectory.count}"${after}` 178 | 179 | }) 180 | 181 | // Mark matching element with attribute and plugin element count 182 | let currentAttr = tag.getAttribute('data-selectory') 183 | tag.setAttribute('data-selectory', `${currentAttr} ${selectory.count}`) 184 | 185 | // And add our new attribute to the selector list for that rule 186 | ruleList.push(newSelector) 187 | 188 | } 189 | 190 | }) 191 | 192 | }) 193 | 194 | } 195 | 196 | // If at least one element passed the test 197 | if (ruleList.length > 0) { 198 | 199 | newRule = `\n/* ${selector} */\n${ruleList} {\n ${ruleText.replace(/(; )([^\{])/g,';\n $2')}\n}\n` 200 | 201 | } 202 | 203 | return newRule 204 | 205 | } 206 | 207 | // Update every `load`, `resize`, `input`, and `click` 208 | window.addEventListener('load', selectory.load) 209 | window.addEventListener('resize', selectory.load) 210 | window.addEventListener('input', selectory.load) 211 | window.addEventListener('click', selectory.load) 212 | 213 | return selectory 214 | 215 | })) 216 | -------------------------------------------------------------------------------- /test/aspecty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Aspecty Testing 5 | 6 |
7 | 8 | 18 | 19 | -------------------------------------------------------------------------------- /test/cursory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cursory Testing 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /test/scrollery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scrollery Testing 6 | 7 | 8 | 9 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /test/selectory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selectory Testing 5 | 6 |
7 | 8 | 9 | 25 | 26 | -------------------------------------------------------------------------------- /test/varsity.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Varsity Testing 5 | 6 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /test/xpathy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Xpathy Testing 5 | 6 |

Selecting by tag name

7 | 10 | 11 |

Selecting by attribute

12 | 15 | 16 |

Selecting by text content

17 | 20 | 21 |

Selecting parent element

22 | 26 | 27 |

Compare attribute value as numbers

28 | 35 | 36 |

Counting children

37 | 60 | 61 | 97 | 98 | -------------------------------------------------------------------------------- /varsity.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | # Varsity 4 | ## version 0.1.1 5 | 6 | Varsity is a CSS reprocessor that makes the following JS values available as CSS variables for any element you tell the plugin to watch: 7 | 8 | - `offsetWidth` 9 | - `offsetHeight` 10 | - `offsetLeft` 11 | - `offsetTop` 12 | - `aspect-ratio` 13 | - `characters` 14 | - `children` 15 | - `value` 16 | 17 | By default, Varsity will reprocess selectors by watching the following events: 18 | 19 | - `load` 20 | - `resize` 21 | - `input` 22 | - `click` 23 | 24 | To run Varsity whenever you want, use the `varsity()` function in JS. 25 | 26 | To have varsity watch an element, you need to give that element a unique identifier, as well as add the `data-varsity` attribute. The plugin will use either the value of the `data-varsity` attribute, or else the value of the `id` (if defined) for an element. 27 | 28 | ``` 29 |
30 | 31 | 32 | 33 |
34 | ``` 35 | 36 | Once the plugin is aware of an element to watch, and the unique name of that element, it will make the above values available in the following format: `--name-value`, for example: 37 | 38 | - `--example-offsetWidth` 39 | - `--example-offsetHeight` 40 | - `--example-offsetLeft` 41 | - `--example-offsetTop` 42 | - `--example-aspect-ratio` 43 | - `--example-characters` 44 | - `--example-children` 45 | - `--example-value` 46 | 47 | - https://github.com/tomhodgins/cssplus 48 | 49 | Author: Tommy Hodgins 50 | 51 | License: MIT 52 | 53 | */ 54 | 55 | // Uses Node, AMD or browser globals to create a module 56 | (function (root, factory) { 57 | 58 | if (typeof define === 'function' && define.amd) { 59 | 60 | // AMD: Register as an anonymous module 61 | define([], factory) 62 | 63 | } else if (typeof module === 'object' && module.exports) { 64 | 65 | // Node: Does not work with strict CommonJS, but 66 | // only CommonJS-like environments that support module.exports, 67 | // like Node 68 | module.exports = factory() 69 | 70 | } else { 71 | 72 | // Browser globals (root is window) 73 | root.varsity = factory() 74 | 75 | } 76 | 77 | }(this, function() { 78 | 79 | const varsity = {} 80 | 81 | varsity.style = '' 82 | 83 | varsity.load = () => { 84 | 85 | // Find (or create) style tag to populate 86 | const style_tag = document.querySelector('[data-varsity-style]') || (() => { 87 | 88 | const tag = document.createElement('style') 89 | tag.setAttribute('data-varsity-style', '') 90 | document.head.appendChild(tag) 91 | return tag 92 | 93 | })() 94 | 95 | // Reset plugin styles 96 | varsity.style = '' 97 | 98 | varsity.process() 99 | 100 | // Populate style tag with current CSS string 101 | style_tag.innerHTML = `:root {\n${varsity.style.replace(/^/gm,' ')}\n}` 102 | 103 | } 104 | 105 | varsity.process = () => { 106 | 107 | let css_rules = '' 108 | 109 | // For each [data-varsity] element 110 | Array.from(document.querySelectorAll('[data-varsity]'), (tag, i) => { 111 | 112 | css_rules += varsity.transform(tag, i) 113 | 114 | }) 115 | 116 | if (css_rules.length > 0) { 117 | 118 | varsity.style += `\n${css_rules}$\n` 119 | 120 | } 121 | 122 | } 123 | 124 | varsity.transform = (tag, i) => { 125 | 126 | let newRule = '' 127 | 128 | // Set the name to the value of the data-varsity="" attribute, or the id="" if there is none 129 | let name = (tag.getAttribute('data-varsity') || tag.id || i) 130 | 131 | // List properties of element as variables 132 | newRule = `/* ${name} */` 133 | + `\n--${name}-offsetWidth: ${tag.offsetWidth};` 134 | + `\n--${name}-offsetHeight: ${tag.offsetHeight};` 135 | + `\n--${name}-offsetLeft: ${tag.offsetLeft};` 136 | + `\n--${name}-offsetTop: ${tag.offsetTop};` 137 | + `\n--${name}-aspect-ratio: ${tag.offsetWidth/tag.offsetHeight};` 138 | + `\n--${name}-characters: ${tag.textContent.length || (tag.value && tag.value.length) || 0};` 139 | + `\n--${name}-children: ${tag.getElementsByTagName('*').length};` 140 | + `\n--${name}-value: ${tag.value || ''};` 141 | + '\n' 142 | 143 | return newRule 144 | 145 | } 146 | 147 | // Update every `load`, `resize`, `input`, and `click` 148 | window.addEventListener('load', varsity.load) 149 | window.addEventListener('resize', varsity.load) 150 | window.addEventListener('input', varsity.load) 151 | window.addEventListener('click', varsity.load) 152 | 153 | return varsity 154 | 155 | })) 156 | -------------------------------------------------------------------------------- /xpathy.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | # XPathy 4 | ## version 0.1.1 5 | 6 | XPathy is a CSS reprocessor that resolves selectors using XPath. This plugin will read CSS selectors that end with a `[xpath]` attribute and use JavaScript and XPath to determine whether or not to apply that style to elements matching the other part of that selector. For example, the XPath selector `//div` will always resolve to `div`, so a selector written for `div [xpath="//div"] {}` will always apply to each `div div {}` element. 7 | 8 | By default, XPathy will reprocess selectors by watching the following events: 9 | 10 | - `load` 11 | - `resize` 12 | - `input` 13 | - `click` 14 | 15 | To run XPathy whenever you want, use the `xpathy.load()` function in JS. 16 | 17 | - https://github.com/tomhodgins/cssplus 18 | 19 | Author: Tommy Hodgins 20 | 21 | License: MIT 22 | 23 | */ 24 | 25 | // Uses Node, AMD or browser globals to create a module 26 | (function (root, factory) { 27 | 28 | if (typeof define === 'function' && define.amd) { 29 | 30 | // AMD: Register as an anonymous module 31 | define([], factory) 32 | 33 | } else if (typeof module === 'object' && module.exports) { 34 | 35 | // Node: Does not work with strict CommonJS, but 36 | // only CommonJS-like environments that support module.exports, 37 | // like Node 38 | module.exports = factory() 39 | 40 | } else { 41 | 42 | // Browser globals (root is window) 43 | root.xpathy = factory() 44 | 45 | } 46 | 47 | }(this, function() { 48 | 49 | const xpathy = {} 50 | 51 | xpathy.style = '' 52 | xpathy.count = 0 53 | 54 | xpathy.load = () => { 55 | 56 | // Find (or create) style tag to populate 57 | const style_tag = document.querySelector('[data-xpathy-style]') || (() => { 58 | 59 | const tag = document.createElement('style') 60 | tag.setAttribute('data-xpathy-style', '') 61 | document.head.appendChild(tag) 62 | return tag 63 | 64 | })() 65 | 66 | // Reset plugin styles and element count 67 | xpathy.style = '' 68 | xpathy.count = 0 69 | 70 | // Reset count on [data-xpathy] elements in DOM 71 | Array.from(document.querySelectorAll('[data-xpathy]'), tag => { 72 | 73 | tag.setAttribute('data-xpathy', '') 74 | 75 | }) 76 | 77 | xpathy.findRules() 78 | 79 | // Populate style tag with style 80 | style_tag.innerHTML = `\n${xpathy.style}\n` 81 | 82 | } 83 | 84 | xpathy.findRules = () => { 85 | 86 | // For each stylesheet 87 | Array.from(document.styleSheets, sheet => { 88 | 89 | // For each rule 90 | sheet.cssRules && Array.from(sheet.cssRules, rule => { 91 | 92 | xpathy.process(rule) 93 | 94 | }) 95 | 96 | }) 97 | 98 | } 99 | 100 | xpathy.process = rule => { 101 | 102 | // If rule is a qualified rule, process it 103 | if (rule.type === 1) { 104 | 105 | xpathy.style += xpathy.transform(rule) 106 | 107 | } 108 | 109 | // If rule is an at-rule, find all qualified rules inside 110 | if (rule.type === 4) { 111 | 112 | let css_rules = '' 113 | 114 | // Remember media query text 115 | let mediaText = rule.media.mediaText 116 | 117 | // If there are qualified rules, find all rules 118 | rule.cssRules && Array.from(rule.cssRules, mediaRule => { 119 | 120 | css_rules += xpathy.transform(mediaRule) 121 | 122 | }) 123 | 124 | // If there is at least one new rule, wrap in at-rule with media text 125 | if (css_rules.length > 0) { 126 | 127 | xpathy.style += `\n@media ${mediaText} {\n ${css_rules.replace(/^(.*)$/gmi,' $1')}\n}\n` 128 | 129 | } 130 | 131 | } 132 | 133 | } 134 | 135 | xpathy.transform = rule => { 136 | 137 | let newRule = '' 138 | 139 | let selector = rule.selectorText.replace(/\s*([^{]+)\s*{/gi, '$1') 140 | let ruleText = rule.cssText.replace(/[^{]+\{([^}]*)\}/gi, '$1') 141 | 142 | // Start a new list of matching rules 143 | let ruleList = [] 144 | 145 | // If `[xpath=` is present anywhere in the selector 146 | if (selector && selector.indexOf('[xpath=') !== -1) { 147 | 148 | // Extract the full selector name and test 149 | selector.replace(/^(.*)\[xpath=("(?:[^"]+)"|'(?:[^']+)')\].*/i, (string, selectorText, xpath) => { 150 | 151 | xpath = xpath.replace(/^'([^']*)'$/m, '$1') 152 | xpath = xpath.replace(/^"([^"]*)"$/m, '$1') 153 | 154 | // Create new array to hold nodes selected by XPath 155 | let list = new Array() 156 | 157 | // use document.evaluate() to query DOM with our XPath 158 | let nodes = document.evaluate( 159 | xpath, 160 | document, 161 | null, 162 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, 163 | null 164 | ) 165 | 166 | // If at least one node matches our XPath 167 | if (nodes) { 168 | 169 | for (let i=0; i 0) { 180 | 181 | // For each tag matching the selector 182 | list.forEach((tag, i) => { 183 | 184 | // Increment the plugin element count 185 | xpathy.count++ 186 | 187 | // Create a new selector for our new CSS rule 188 | let newSelector = selector.replace(/^(.*\[)(xpath=(?:"[^"]*"|'[^']*'))(\].*)$/im, (string, before, test, after) => { 189 | 190 | return `${before}data-xpathy~="${xpathy.count}"${after}` 191 | 192 | }) 193 | 194 | // Mark matching element with attribute and plugin element count 195 | let currentAttr = tag.getAttribute('data-xpathy') 196 | tag.setAttribute('data-xpathy', `${currentAttr} ${xpathy.count}`) 197 | 198 | // And add our new attribute to the selector list for that rule 199 | ruleList.push(newSelector) 200 | 201 | }) 202 | 203 | } 204 | 205 | }) 206 | 207 | // If at least one element passed the test 208 | if (ruleList.length > 0) { 209 | 210 | newRule = `\n/* ${selector} */\n${ruleList} {\n ${ruleText.replace(/(; )([^\{])/g,';\n $2')}\n}\n` 211 | 212 | } 213 | 214 | } 215 | 216 | return newRule 217 | 218 | } 219 | 220 | // Update every `load`, `resize`, `input`, and `click` 221 | window.addEventListener('load', xpathy.load) 222 | window.addEventListener('resize', xpathy.load) 223 | window.addEventListener('input', xpathy.load) 224 | window.addEventListener('click', xpathy.load) 225 | 226 | return xpathy 227 | 228 | })) 229 | --------------------------------------------------------------------------------