├── .gitignore ├── CHANGELOG.md ├── README.md ├── example ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── build │ ├── .gitignore │ ├── build │ │ └── images │ │ │ └── .keep │ ├── images │ │ ├── .keep │ │ └── GitHub-Mark-64px.png │ ├── index.html │ ├── javascripts │ │ └── vendor │ │ │ └── floatl.umd.js │ ├── source │ │ └── images │ │ │ └── .keep │ └── stylesheets │ │ ├── floatl.css │ │ ├── site.css │ │ └── vendor │ │ └── normalize.css ├── config.rb └── source │ ├── images │ ├── .keep │ └── GitHub-Mark-64px.png │ ├── index.html.erb │ ├── javascripts │ └── vendor │ │ └── floatl.umd.js │ ├── layouts │ └── layout.erb │ └── stylesheets │ ├── floatl.css.scss │ ├── site.css.scss │ └── vendor │ └── normalize.css ├── karma.conf.js ├── package.json ├── rollup.config.js ├── spec ├── main.spec.ts ├── specHelpers.ts └── utils.spec.ts ├── src ├── main.ts └── utils.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sass-cache 3 | .rpt2_cache 4 | 5 | /node_modules 6 | /built 7 | /coverage 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file going forward. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [2.0.1] - 2018-02-14 11 | ### Fixed 12 | - Add `npm prepare` command to build the files 13 | 14 | ## [2.0.0] - 2018-02-14 15 | ### Added 16 | - `Floatl#destroy` to remove event listeners 17 | - SauceLabs setup 18 | - Example site 19 | 20 | ### Changed 21 | - Change changelog format to [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 22 | - Rewritten in TypeScript 23 | - Properly setup CodeClimate 24 | - Use Semaphore for CI 25 | - Refactor tests to remove jquery and jasmine-jquery devdependencies 26 | - Moved some documentation from README to GitHub wiki 27 | 28 | ### Removed 29 | - Bower support 30 | - Support for passing in jQuery elements 31 | - Included CSS styling 32 | 33 | ## [1.0.5] - 2017-02-27 34 | ### Fixed 35 | - Forgot to release built files (again!) 36 | 37 | ## [1.0.4] - 2017-02-27 38 | ### Changed 39 | - Handle state change on input event (thanks @zlatevbg!) 40 | 41 | ## [1.0.3] - 2016-10-19 42 | ### Changed 43 | - Stop initialisation if required either input or label is not found (thanks @leptest!) 44 | 45 | ## [1.0.2] - 2016-10-05 46 | ### Fixed 47 | - Actually release built files, whoops! 48 | 49 | ## [1.0.1] - 2016-10-05 50 | ### Changed 51 | - Properly assign regex variable in `removeClass` function, fix #16 (thanks @martinothamar!) 52 | 53 | ## [1.0.0] - 2016-07-13 54 | ### Added 55 | - Start using changelog 56 | - Include globally built floatl.global.js 57 | 58 | ### Changed 59 | - Change default Webpack output to commonjs2 module 60 | - Remove Gulp in favour of NPM scripts 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ☁️ Floatl 2 | 3 | A pragmatic implementation of the [Float Label Pattern](https://dribbble.com/shots/1254439--GIF-Mobile-Form-Interaction) for the web. 4 | 5 | [![NPM](https://img.shields.io/npm/v/floatl.svg)](https://www.npmjs.com/package/floatl) 6 | [![Build Status](https://semaphoreci.com/api/v1/richardvenneman/floatl/branches/master/badge.svg)](https://semaphoreci.com/richardvenneman/floatl) 7 | [![Maintainability](https://api.codeclimate.com/v1/badges/c68d489f62e4a80f0ae2/maintainability)](https://codeclimate.com/github/richardvenneman/floatl/maintainability) 8 | [![Test Coverage](https://api.codeclimate.com/v1/badges/c68d489f62e4a80f0ae2/test_coverage)](https://codeclimate.com/github/richardvenneman/floatl/test_coverage) 9 | 10 | ![Floatl](https://i.imgur.com/fjDfAcE.gif) 11 | 12 | Try it for yourself: https://richardvenneman.github.io/floatl/ 13 | 14 | ## Features 15 | 16 | 🤙 Supports textfields and textareas 17 | 18 | 🏝 Dependency-free and maintained 19 | 20 | ⚡️ Small size, < 1Kb gzipped 21 | 22 |
23 | 🤣 Oldie browser support 24 | 25 | Sauce Test Status 26 | 27 | 28 | _* Supports IE8 & IE9 as well, but I couldn't get them to run on SauceLabs_ 😰 29 |
30 | 31 | ## Installation 32 | 33 | Floatl is built primarily for module bundlers such as [Browserify](http://browserify.org) and [webpack](http://webpack.github.io). 34 | As such it is distributed via [NPM](https://www.npmjs.com/package/floatl). 35 | 36 | ```bash 37 | yarn add floatl # or npm i -S floatl 38 | ``` 39 | 40 | _An UMD build is available from the [GitHub releases page](https://github.com/richardvenneman/floatl/releases) if you're not using a module bundler. This version adds `Floatl` to the global namespace._ 41 | 42 | ### Styling 43 | 44 | Add the required styling to your app. Please refer to the [GitHub wiki page on styling](https://github.com/richardvenneman/floatl/wiki/Styling) for more information. 45 | 46 | ## Usage 47 | 48 | *NOTE: Check the [GitHub wiki page](https://github.com/richardvenneman/floatl/wiki) for instructions on how to use with React, AngularJS and Stimulus.* 49 | 50 | Markup your `label` and `input` (or `textarea`) with the floatl classes and wrap them in an element with the `floatl` class: 51 | 52 | ```html 53 |
54 | 55 | 56 |
57 | ``` 58 | 59 | Instantiate Floatl by passing in the wrapping DOM element: 60 | 61 | ```javascript 62 | import Floatl from "floatl"; 63 | 64 | var element = document.getElementById("my-floatl-element"); 65 | new Floatl(element); 66 | ``` 67 | 68 | ### Destroying instances 69 | 70 | If you keep a reference to your Floatl instance, it's easy to remove all of the event handlers by calling the `destroy` instance method: 71 | 72 | ```javascript 73 | var myFloatl = new Floatl(element); 74 | myFloatl.destroy() 75 | ``` 76 | 77 | ## Placeholder polyfilling 78 | 79 | While the JavaScript supports IE8+, Floatl aims to be good at Floating Labels and only that. The Floating Labels Pattern works best with placeholders and it is therefor recommended to install legacy browser placeholder support should you need it, for example [Placekeeper](https://github.com/kristerkari/placekeeper) or [Placeholders.js](https://github.com/jamesallardice/Placeholders.js). 80 | 81 | ## Motivations 82 | 83 | There are several libraries available that implement the Float Label Pattern, most notably [floatlabels.js](https://github.com/clubdesign/floatlabels.js) and [FloatLabel.js](https://github.com/m10l/FloatLabel.js). However, these libraries did not quite fulfill the requisites I had in mind (see features above) and I did not find any Bower compatible libraries when I started this project. Furthermore I like to use a well-maintained library. Since we're using this library in production at [CitySpotters](https://www.cityspotters.com) I'm keeping this library up to date. 84 | 85 | ## Contributing 86 | 87 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 88 | 89 | * Report bugs 90 | * Fix bugs and submit pull requests 91 | * Write, clarify, or fix documentation 92 | * Suggest or add new features 93 | * Write missing tests 94 | * Improve the TypeScript implementation 95 | 96 | ## Development & testing 97 | 98 | This project uses [Jasmine](http://jasmine.github.io) with the [Karma Test Runner](http://karma-runner.github.io/). 99 | 100 | * Install dependencies with `yarn install` 101 | * Run the test suite with: `yarn test` (or `yarn run tdd` for Test Driven Development) 102 | 103 | ## License 104 | 105 | This library is released under the [MIT License](http://www.opensource.org/licenses/MIT). 106 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'middleman', '~> 4.2' 4 | gem 'middleman-autoprefixer', '~> 2.7' 5 | gem 'middleman-livereload', '~> 3.4' 6 | gem 'middleman-gh-pages', '~> 0.3' 7 | 8 | gem 'tzinfo-data', platforms: [:mswin, :mingw, :jruby] 9 | gem 'wdm', '~> 0.1', platforms: [:mswin, :mingw] 10 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (5.0.6) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | tzinfo (~> 1.1) 9 | addressable (2.5.2) 10 | public_suffix (>= 2.0.2, < 4.0) 11 | autoprefixer-rails (7.2.6) 12 | execjs 13 | backports (3.11.1) 14 | coffee-script (2.4.1) 15 | coffee-script-source 16 | execjs 17 | coffee-script-source (1.12.2) 18 | compass-import-once (1.0.5) 19 | sass (>= 3.2, < 3.5) 20 | concurrent-ruby (1.0.5) 21 | contracts (0.13.0) 22 | dotenv (2.2.1) 23 | em-websocket (0.5.1) 24 | eventmachine (>= 0.12.9) 25 | http_parser.rb (~> 0.6.0) 26 | erubis (2.7.0) 27 | eventmachine (1.2.5) 28 | execjs (2.7.0) 29 | fast_blank (1.0.0) 30 | fastimage (2.1.1) 31 | ffi (1.9.21) 32 | haml (5.0.4) 33 | temple (>= 0.8.0) 34 | tilt 35 | hamster (3.0.0) 36 | concurrent-ruby (~> 1.0) 37 | hashie (3.5.7) 38 | http_parser.rb (0.6.0) 39 | i18n (0.7.0) 40 | kramdown (1.16.2) 41 | listen (3.0.8) 42 | rb-fsevent (~> 0.9, >= 0.9.4) 43 | rb-inotify (~> 0.9, >= 0.9.7) 44 | memoist (0.16.0) 45 | middleman (4.2.1) 46 | coffee-script (~> 2.2) 47 | compass-import-once (= 1.0.5) 48 | haml (>= 4.0.5) 49 | kramdown (~> 1.2) 50 | middleman-cli (= 4.2.1) 51 | middleman-core (= 4.2.1) 52 | sass (>= 3.4.0, < 4.0) 53 | middleman-autoprefixer (2.8.0) 54 | autoprefixer-rails (>= 7.0.1, < 8.0.0) 55 | middleman-core (>= 3.3.3) 56 | middleman-cli (4.2.1) 57 | thor (>= 0.17.0, < 2.0) 58 | middleman-core (4.2.1) 59 | activesupport (>= 4.2, < 5.1) 60 | addressable (~> 2.3) 61 | backports (~> 3.6) 62 | bundler (~> 1.1) 63 | contracts (~> 0.13.0) 64 | dotenv 65 | erubis 66 | execjs (~> 2.0) 67 | fast_blank 68 | fastimage (~> 2.0) 69 | hamster (~> 3.0) 70 | hashie (~> 3.4) 71 | i18n (~> 0.7.0) 72 | listen (~> 3.0.0) 73 | memoist (~> 0.14) 74 | padrino-helpers (~> 0.13.0) 75 | parallel 76 | rack (>= 1.4.5, < 3) 77 | sass (>= 3.4) 78 | servolux 79 | tilt (~> 2.0) 80 | uglifier (~> 3.0) 81 | middleman-gh-pages (0.3.1) 82 | rake (> 0.9.3) 83 | middleman-livereload (3.4.6) 84 | em-websocket (~> 0.5.1) 85 | middleman-core (>= 3.3) 86 | rack-livereload (~> 0.3.15) 87 | minitest (5.11.3) 88 | padrino-helpers (0.13.3.4) 89 | i18n (~> 0.6, >= 0.6.7) 90 | padrino-support (= 0.13.3.4) 91 | tilt (>= 1.4.1, < 3) 92 | padrino-support (0.13.3.4) 93 | activesupport (>= 3.1) 94 | parallel (1.12.1) 95 | public_suffix (3.0.2) 96 | rack (2.0.4) 97 | rack-livereload (0.3.16) 98 | rack 99 | rake (12.3.0) 100 | rb-fsevent (0.10.2) 101 | rb-inotify (0.9.10) 102 | ffi (>= 0.5.0, < 2) 103 | sass (3.4.25) 104 | servolux (0.13.0) 105 | temple (0.8.0) 106 | thor (0.20.0) 107 | thread_safe (0.3.6) 108 | tilt (2.0.8) 109 | tzinfo (1.2.5) 110 | thread_safe (~> 0.1) 111 | uglifier (3.2.0) 112 | execjs (>= 0.3.0, < 3) 113 | 114 | PLATFORMS 115 | ruby 116 | 117 | DEPENDENCIES 118 | middleman (~> 4.2) 119 | middleman-autoprefixer (~> 2.7) 120 | middleman-gh-pages (~> 0.3) 121 | middleman-livereload (~> 3.4) 122 | tzinfo-data 123 | wdm (~> 0.1) 124 | 125 | BUNDLED WITH 126 | 1.16.0 127 | -------------------------------------------------------------------------------- /example/Rakefile: -------------------------------------------------------------------------------- 1 | require 'middleman-gh-pages' 2 | 3 | ENV['PROJECT_ROOT'] = '/Users/richard/Projects/floatl/example' 4 | ENV['COMMIT_MESSAGE_SUFFIX'] = '[skip ci]' 5 | -------------------------------------------------------------------------------- /example/build/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .sass-cache/ 4 | 5 | -------------------------------------------------------------------------------- /example/build/build/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardvenneman/floatl/0e3159ddafb23f698f9ad7b720e77871f2d88c6a/example/build/build/images/.keep -------------------------------------------------------------------------------- /example/build/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardvenneman/floatl/0e3159ddafb23f698f9ad7b720e77871f2d88c6a/example/build/images/.keep -------------------------------------------------------------------------------- /example/build/images/GitHub-Mark-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardvenneman/floatl/0e3159ddafb23f698f9ad7b720e77871f2d88c6a/example/build/images/GitHub-Mark-64px.png -------------------------------------------------------------------------------- /example/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | Floatl 10 | 11 | 12 | 13 | 14 |

Floatl

15 | 16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 | 37 |
38 | 39 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /example/build/javascripts/vendor/floatl.umd.js: -------------------------------------------------------------------------------- 1 | /* floatl version 1.0.5 */ 2 | (function (global, factory) { 3 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 4 | typeof define === 'function' && define.amd ? define(factory) : 5 | (global.Floatl = factory()); 6 | }(this, (function () { 'use strict'; 7 | 8 | function addClass(element, className) { 9 | if (element.classList) { 10 | element.classList.add(className); 11 | } 12 | else { 13 | element.className += " " + className; 14 | } 15 | } 16 | function removeClass(element, className) { 17 | if (element.classList) { 18 | element.classList.remove(className); 19 | } 20 | else { 21 | var re = new RegExp("(^|\\b)" + className.split(" ").join("|") + "(\\b|$)", "gi"); 22 | element.className = element.className.replace(re, " "); 23 | } 24 | } 25 | function addEventListener(element, event, cb) { 26 | if (element.addEventListener) { 27 | element.addEventListener(event, cb); 28 | } 29 | else { 30 | element.attachEvent("on" + event, function () { 31 | cb.call(element); 32 | }); 33 | } 34 | } 35 | function removeEventListener(element, event, cb) { 36 | if (element.removeEventListener) { 37 | element.removeEventListener(event, cb); 38 | } 39 | else { 40 | element.detachEvent("on" + event, cb); 41 | } 42 | } 43 | 44 | var Floatl = /** @class */ (function () { 45 | function Floatl(element) { 46 | var _this = this; 47 | this.handleChange = function () { 48 | if (_this.input.value === "") { 49 | removeClass(_this.element, Floatl.ACTIVE_CLASS); 50 | } 51 | else { 52 | addClass(_this.element, Floatl.ACTIVE_CLASS); 53 | } 54 | }; 55 | this.addFocusedClass = function () { 56 | addClass(_this.element, Floatl.FOCUSED_CLASS); 57 | }; 58 | this.removeFocusedClass = function () { 59 | removeClass(_this.element, Floatl.FOCUSED_CLASS); 60 | }; 61 | this.element = element; 62 | this.label = element.querySelectorAll(".floatl__label")[0]; 63 | this.input = element.querySelectorAll(".floatl__input")[0]; 64 | // Return early if not both the label and input are present 65 | if (!this.label || !this.input) { 66 | return; 67 | } 68 | if (this.input.nodeName === "TEXTAREA") { 69 | addClass(this.element, Floatl.MULTILINE_CLASS); 70 | } 71 | // Handle initial value 72 | this.handleChange(); 73 | // Bind event listeners 74 | addEventListener(this.input, "focus", this.addFocusedClass); 75 | addEventListener(this.input, "blur", this.removeFocusedClass); 76 | for (var _i = 0, _a = ["keyup", "blur", "change", "input"]; _i < _a.length; _i++) { 77 | var event_1 = _a[_i]; 78 | addEventListener(this.input, event_1, this.handleChange); 79 | } 80 | } 81 | Floatl.prototype.destroy = function () { 82 | removeEventListener(this.input, "focus", this.addFocusedClass); 83 | removeEventListener(this.input, "blur", this.removeFocusedClass); 84 | for (var _i = 0, _a = ["keyup", "blur", "change", "input"]; _i < _a.length; _i++) { 85 | var event_2 = _a[_i]; 86 | removeEventListener(this.input, event_2, this.handleChange); 87 | } 88 | }; 89 | Floatl.FOCUSED_CLASS = "floatl--focused"; 90 | Floatl.ACTIVE_CLASS = "floatl--active"; 91 | Floatl.MULTILINE_CLASS = "floatl--multiline"; 92 | return Floatl; 93 | }()); 94 | 95 | return Floatl; 96 | 97 | }))); 98 | -------------------------------------------------------------------------------- /example/build/source/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardvenneman/floatl/0e3159ddafb23f698f9ad7b720e77871f2d88c6a/example/build/source/images/.keep -------------------------------------------------------------------------------- /example/build/stylesheets/floatl.css: -------------------------------------------------------------------------------- 1 | .floatl { 2 | position: relative; } 3 | 4 | .floatl--focused .floatl__label { 5 | color: #2a8dea; } 6 | 7 | .floatl--active .floatl__label { 8 | visibility: visible; 9 | opacity: 1; 10 | top: 1px; } 11 | .floatl--active .floatl__input { 12 | padding: 21px 11px 7px; } 13 | .floatl--active.floatl--multiline .floatl__label { 14 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.95)), color-stop(80%, rgba(255, 255, 255, 0.95)), to(rgba(255, 255, 255, 0))); 15 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.95) 80%, rgba(255, 255, 255, 0) 100%); 16 | background-color: white; } 17 | 18 | .floatl__label { 19 | -webkit-transition: all 200ms ease; 20 | transition: all 200ms ease; 21 | position: absolute; 22 | visibility: hidden; 23 | opacity: 0; 24 | top: 3px; 25 | left: 9px; 26 | display: inline-block; 27 | padding: 6px 3px 3px; 28 | font-weight: bold; 29 | font-size: 11px; 30 | line-height: 1em; 31 | color: #666666; } 32 | 33 | .floatl__input { 34 | -webkit-transition: all 200ms ease; 35 | transition: all 200ms ease; 36 | padding: 14px 11px; 37 | background-color: #fafafa; 38 | font-size: 16px; 39 | border: 1px solid #e6e6e6; 40 | border-radius: 5px; 41 | -webkit-appearance: none; 42 | -moz-appearance: none; 43 | appearance: none; 44 | outline: none; } 45 | .floatl__input::-moz-selection { 46 | background-color: #2a8dea; 47 | color: white; } 48 | .floatl__input::selection { 49 | background-color: #2a8dea; 50 | color: white; } 51 | .floatl__input:focus { 52 | border-color: #2a8dea; } 53 | 54 | input.floatl__input { 55 | height: 47px; } 56 | -------------------------------------------------------------------------------- /example/build/stylesheets/site.css: -------------------------------------------------------------------------------- 1 | @import url(./vendor/normalize.css); 2 | .floatl { 3 | position: relative; } 4 | 5 | .floatl--focused .floatl__label { 6 | color: #2a8dea; } 7 | 8 | .floatl--active .floatl__label { 9 | visibility: visible; 10 | opacity: 1; 11 | top: 1px; } 12 | .floatl--active .floatl__input { 13 | padding: 21px 11px 7px; } 14 | .floatl--active.floatl--multiline .floatl__label { 15 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.95)), color-stop(80%, rgba(255, 255, 255, 0.95)), to(rgba(255, 255, 255, 0))); 16 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.95) 80%, rgba(255, 255, 255, 0) 100%); 17 | background-color: white; } 18 | 19 | .floatl__label { 20 | -webkit-transition: all 200ms ease; 21 | transition: all 200ms ease; 22 | position: absolute; 23 | visibility: hidden; 24 | opacity: 0; 25 | top: 3px; 26 | left: 9px; 27 | display: inline-block; 28 | padding: 6px 3px 3px; 29 | font-weight: bold; 30 | font-size: 11px; 31 | line-height: 1em; 32 | color: #666666; } 33 | 34 | .floatl__input { 35 | -webkit-transition: all 200ms ease; 36 | transition: all 200ms ease; 37 | padding: 14px 11px; 38 | background-color: #fafafa; 39 | font-size: 16px; 40 | border: 1px solid #e6e6e6; 41 | border-radius: 5px; 42 | -webkit-appearance: none; 43 | -moz-appearance: none; 44 | appearance: none; 45 | outline: none; } 46 | .floatl__input::-moz-selection { 47 | background-color: #2a8dea; 48 | color: white; } 49 | .floatl__input::selection { 50 | background-color: #2a8dea; 51 | color: white; } 52 | .floatl__input:focus { 53 | border-color: #2a8dea; } 54 | 55 | input.floatl__input { 56 | height: 47px; } 57 | 58 | html { 59 | -webkit-box-sizing: border-box; 60 | box-sizing: border-box; 61 | text-rendering: optimizeLegibility; 62 | -webkit-font-smoothing: antialiased; 63 | -moz-osx-font-smoothing: grayscale; } 64 | 65 | *, 66 | *::before, 67 | *::after { 68 | -webkit-box-sizing: inherit; 69 | box-sizing: inherit; } 70 | 71 | body { 72 | -webkit-animation: gradient 20s ease infinite; 73 | animation: gradient 20s ease infinite; 74 | color: #333; 75 | background: linear-gradient(76deg, #2c3e50, #4ca1af); 76 | background-size: 400% 400%; 77 | font-family: -apple-system, BlinkMacSystemFont, "Avenir Next", "Avenir", "Segoe UI", "Lucida Grande", "Helvetica Neue", "Helvetica", "Fira Sans", "Roboto", "Noto", "Droid Sans", "Cantarell", "Oxygen", "Ubuntu", "Franklin Gothic Medium", "Century Gothic", "Liberation Sans", sans-serif; 78 | padding: 18vh 1rem; } 79 | 80 | @-webkit-keyframes gradient { 81 | 0% { 82 | background-position: 0% 50%; } 83 | 50% { 84 | background-position: 100% 50%; } 85 | 100% { 86 | background-position: 0% 50%; } } 87 | 88 | @keyframes gradient { 89 | 0% { 90 | background-position: 0% 50%; } 91 | 50% { 92 | background-position: 100% 50%; } 93 | 100% { 94 | background-position: 0% 50%; } } 95 | .header { 96 | color: white; 97 | text-align: center; 98 | font-weight: 300; } 99 | 100 | .container { 101 | will-change: transform; 102 | margin: 0 auto; 103 | padding: 3em; 104 | background-color: white; 105 | max-width: 360px; 106 | border-radius: 5px; } 107 | 108 | input, textarea { 109 | width: 100%; } 110 | 111 | textarea { 112 | height: 100px; } 113 | 114 | .floatl + .floatl { 115 | margin-top: 1em; } 116 | 117 | footer { 118 | padding-top: 2em; 119 | margin: 2em auto 0; 120 | width: 128px; 121 | text-align: center; 122 | border-top: 1px dashed #e6e6e6; } 123 | 124 | .github-link { 125 | -webkit-transition: opacity 300ms ease; 126 | transition: opacity 300ms ease; 127 | opacity: 0.9; 128 | display: inline-block; 129 | text-decoration: none; } 130 | .github-link:hover { 131 | opacity: 0.6; } 132 | -------------------------------------------------------------------------------- /example/build/stylesheets/vendor/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Correct the font size and margin on `h1` elements within `section` and 29 | * `article` contexts in Chrome, Firefox, and Safari. 30 | */ 31 | 32 | h1 { 33 | font-size: 2em; 34 | margin: 0.67em 0; 35 | } 36 | 37 | /* Grouping content 38 | ========================================================================== */ 39 | 40 | /** 41 | * 1. Add the correct box sizing in Firefox. 42 | * 2. Show the overflow in Edge and IE. 43 | */ 44 | 45 | hr { 46 | -webkit-box-sizing: content-box; 47 | box-sizing: content-box; /* 1 */ 48 | height: 0; /* 1 */ 49 | overflow: visible; /* 2 */ 50 | } 51 | 52 | /** 53 | * 1. Correct the inheritance and scaling of font size in all browsers. 54 | * 2. Correct the odd `em` font sizing in all browsers. 55 | */ 56 | 57 | pre { 58 | font-family: monospace, monospace; /* 1 */ 59 | font-size: 1em; /* 2 */ 60 | } 61 | 62 | /* Text-level semantics 63 | ========================================================================== */ 64 | 65 | /** 66 | * Remove the gray background on active links in IE 10. 67 | */ 68 | 69 | a { 70 | background-color: transparent; 71 | } 72 | 73 | /** 74 | * 1. Remove the bottom border in Chrome 57- 75 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 76 | */ 77 | 78 | abbr[title] { 79 | border-bottom: none; /* 1 */ 80 | text-decoration: underline; /* 2 */ 81 | -webkit-text-decoration: underline dotted; 82 | text-decoration: underline dotted; /* 2 */ 83 | } 84 | 85 | /** 86 | * Add the correct font weight in Chrome, Edge, and Safari. 87 | */ 88 | 89 | b, 90 | strong { 91 | font-weight: bolder; 92 | } 93 | 94 | /** 95 | * 1. Correct the inheritance and scaling of font size in all browsers. 96 | * 2. Correct the odd `em` font sizing in all browsers. 97 | */ 98 | 99 | code, 100 | kbd, 101 | samp { 102 | font-family: monospace, monospace; /* 1 */ 103 | font-size: 1em; /* 2 */ 104 | } 105 | 106 | /** 107 | * Add the correct font size in all browsers. 108 | */ 109 | 110 | small { 111 | font-size: 80%; 112 | } 113 | 114 | /** 115 | * Prevent `sub` and `sup` elements from affecting the line height in 116 | * all browsers. 117 | */ 118 | 119 | sub, 120 | sup { 121 | font-size: 75%; 122 | line-height: 0; 123 | position: relative; 124 | vertical-align: baseline; 125 | } 126 | 127 | sub { 128 | bottom: -0.25em; 129 | } 130 | 131 | sup { 132 | top: -0.5em; 133 | } 134 | 135 | /* Embedded content 136 | ========================================================================== */ 137 | 138 | /** 139 | * Remove the border on images inside links in IE 10. 140 | */ 141 | 142 | img { 143 | border-style: none; 144 | } 145 | 146 | /* Forms 147 | ========================================================================== */ 148 | 149 | /** 150 | * 1. Change the font styles in all browsers. 151 | * 2. Remove the margin in Firefox and Safari. 152 | */ 153 | 154 | button, 155 | input, 156 | optgroup, 157 | select, 158 | textarea { 159 | font-family: inherit; /* 1 */ 160 | font-size: 100%; /* 1 */ 161 | line-height: 1.15; /* 1 */ 162 | margin: 0; /* 2 */ 163 | } 164 | 165 | /** 166 | * Show the overflow in IE. 167 | * 1. Show the overflow in Edge. 168 | */ 169 | 170 | button, 171 | input { /* 1 */ 172 | overflow: visible; 173 | } 174 | 175 | /** 176 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 177 | * 1. Remove the inheritance of text transform in Firefox. 178 | */ 179 | 180 | button, 181 | select { /* 1 */ 182 | text-transform: none; 183 | } 184 | 185 | /** 186 | * Correct the inability to style clickable types in iOS and Safari. 187 | */ 188 | 189 | button, 190 | [type="button"], 191 | [type="reset"], 192 | [type="submit"] { 193 | -webkit-appearance: button; 194 | } 195 | 196 | /** 197 | * Remove the inner border and padding in Firefox. 198 | */ 199 | 200 | button::-moz-focus-inner, 201 | [type="button"]::-moz-focus-inner, 202 | [type="reset"]::-moz-focus-inner, 203 | [type="submit"]::-moz-focus-inner { 204 | border-style: none; 205 | padding: 0; 206 | } 207 | 208 | /** 209 | * Restore the focus styles unset by the previous rule. 210 | */ 211 | 212 | button:-moz-focusring, 213 | [type="button"]:-moz-focusring, 214 | [type="reset"]:-moz-focusring, 215 | [type="submit"]:-moz-focusring { 216 | outline: 1px dotted ButtonText; 217 | } 218 | 219 | /** 220 | * Correct the padding in Firefox. 221 | */ 222 | 223 | fieldset { 224 | padding: 0.35em 0.75em 0.625em; 225 | } 226 | 227 | /** 228 | * 1. Correct the text wrapping in Edge and IE. 229 | * 2. Correct the color inheritance from `fieldset` elements in IE. 230 | * 3. Remove the padding so developers are not caught out when they zero out 231 | * `fieldset` elements in all browsers. 232 | */ 233 | 234 | legend { 235 | -webkit-box-sizing: border-box; 236 | box-sizing: border-box; /* 1 */ 237 | color: inherit; /* 2 */ 238 | display: table; /* 1 */ 239 | max-width: 100%; /* 1 */ 240 | padding: 0; /* 3 */ 241 | white-space: normal; /* 1 */ 242 | } 243 | 244 | /** 245 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 246 | */ 247 | 248 | progress { 249 | vertical-align: baseline; 250 | } 251 | 252 | /** 253 | * Remove the default vertical scrollbar in IE 10+. 254 | */ 255 | 256 | textarea { 257 | overflow: auto; 258 | } 259 | 260 | /** 261 | * 1. Add the correct box sizing in IE 10. 262 | * 2. Remove the padding in IE 10. 263 | */ 264 | 265 | [type="checkbox"], 266 | [type="radio"] { 267 | -webkit-box-sizing: border-box; 268 | box-sizing: border-box; /* 1 */ 269 | padding: 0; /* 2 */ 270 | } 271 | 272 | /** 273 | * Correct the cursor style of increment and decrement buttons in Chrome. 274 | */ 275 | 276 | [type="number"]::-webkit-inner-spin-button, 277 | [type="number"]::-webkit-outer-spin-button { 278 | height: auto; 279 | } 280 | 281 | /** 282 | * 1. Correct the odd appearance in Chrome and Safari. 283 | * 2. Correct the outline style in Safari. 284 | */ 285 | 286 | [type="search"] { 287 | -webkit-appearance: textfield; /* 1 */ 288 | outline-offset: -2px; /* 2 */ 289 | } 290 | 291 | /** 292 | * Remove the inner padding in Chrome and Safari on macOS. 293 | */ 294 | 295 | [type="search"]::-webkit-search-decoration { 296 | -webkit-appearance: none; 297 | } 298 | 299 | /** 300 | * 1. Correct the inability to style clickable types in iOS and Safari. 301 | * 2. Change font properties to `inherit` in Safari. 302 | */ 303 | 304 | ::-webkit-file-upload-button { 305 | -webkit-appearance: button; /* 1 */ 306 | font: inherit; /* 2 */ 307 | } 308 | 309 | /* Interactive 310 | ========================================================================== */ 311 | 312 | /* 313 | * Add the correct display in Edge, IE 10+, and Firefox. 314 | */ 315 | 316 | details { 317 | display: block; 318 | } 319 | 320 | /* 321 | * Add the correct display in all browsers. 322 | */ 323 | 324 | summary { 325 | display: list-item; 326 | } 327 | 328 | /* Misc 329 | ========================================================================== */ 330 | 331 | /** 332 | * Add the correct display in IE 10+. 333 | */ 334 | 335 | template { 336 | display: none; 337 | } 338 | 339 | /** 340 | * Add the correct display in IE 10. 341 | */ 342 | 343 | [hidden] { 344 | display: none; 345 | } 346 | -------------------------------------------------------------------------------- /example/config.rb: -------------------------------------------------------------------------------- 1 | # Activate and configure extensions 2 | # https://middlemanapp.com/advanced/configuration/#configuring-extensions 3 | 4 | activate :autoprefixer do |prefix| 5 | prefix.browsers = "last 2 versions" 6 | end 7 | 8 | activate :livereload 9 | 10 | activate :relative_assets 11 | set :relative_links, true 12 | 13 | set :sass, output_style: :expanded 14 | 15 | # Layouts 16 | # https://middlemanapp.com/basics/layouts/ 17 | 18 | # Per-page layout changes 19 | page '/*.xml', layout: false 20 | page '/*.json', layout: false 21 | page '/*.txt', layout: false 22 | 23 | # With alternative layout 24 | # page '/path/to/file.html', layout: 'other_layout' 25 | 26 | # Proxy pages 27 | # https://middlemanapp.com/advanced/dynamic-pages/ 28 | 29 | # proxy( 30 | # '/this-page-has-no-template.html', 31 | # '/template-file.html', 32 | # locals: { 33 | # which_fake_page: 'Rendering a fake page with a local variable' 34 | # }, 35 | # ) 36 | 37 | # Helpers 38 | # Methods defined in the helpers block are available in templates 39 | # https://middlemanapp.com/basics/helper-methods/ 40 | 41 | # helpers do 42 | # def some_helper 43 | # 'Helping' 44 | # end 45 | # end 46 | 47 | # Build-specific configuration 48 | # https://middlemanapp.com/advanced/configuration/#environment-specific-settings 49 | 50 | # configure :build do 51 | # activate :minify_css 52 | # activate :minify_javascript 53 | # end 54 | -------------------------------------------------------------------------------- /example/source/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardvenneman/floatl/0e3159ddafb23f698f9ad7b720e77871f2d88c6a/example/source/images/.keep -------------------------------------------------------------------------------- /example/source/images/GitHub-Mark-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardvenneman/floatl/0e3159ddafb23f698f9ad7b720e77871f2d88c6a/example/source/images/GitHub-Mark-64px.png -------------------------------------------------------------------------------- /example/source/index.html.erb: -------------------------------------------------------------------------------- 1 | --- 2 | title: Floatl 3 | --- 4 | 5 |

Floatl

6 | 7 |
8 |
9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 | 28 |
29 | 30 | 37 | -------------------------------------------------------------------------------- /example/source/javascripts/vendor/floatl.umd.js: -------------------------------------------------------------------------------- 1 | /* floatl version 1.0.5 */ 2 | (function (global, factory) { 3 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 4 | typeof define === 'function' && define.amd ? define(factory) : 5 | (global.Floatl = factory()); 6 | }(this, (function () { 'use strict'; 7 | 8 | function addClass(element, className) { 9 | if (element.classList) { 10 | element.classList.add(className); 11 | } 12 | else { 13 | element.className += " " + className; 14 | } 15 | } 16 | function removeClass(element, className) { 17 | if (element.classList) { 18 | element.classList.remove(className); 19 | } 20 | else { 21 | var re = new RegExp("(^|\\b)" + className.split(" ").join("|") + "(\\b|$)", "gi"); 22 | element.className = element.className.replace(re, " "); 23 | } 24 | } 25 | function addEventListener(element, event, cb) { 26 | if (element.addEventListener) { 27 | element.addEventListener(event, cb); 28 | } 29 | else { 30 | element.attachEvent("on" + event, function () { 31 | cb.call(element); 32 | }); 33 | } 34 | } 35 | function removeEventListener(element, event, cb) { 36 | if (element.removeEventListener) { 37 | element.removeEventListener(event, cb); 38 | } 39 | else { 40 | element.detachEvent("on" + event, cb); 41 | } 42 | } 43 | 44 | var Floatl = /** @class */ (function () { 45 | function Floatl(element) { 46 | var _this = this; 47 | this.handleChange = function () { 48 | if (_this.input.value === "") { 49 | removeClass(_this.element, Floatl.ACTIVE_CLASS); 50 | } 51 | else { 52 | addClass(_this.element, Floatl.ACTIVE_CLASS); 53 | } 54 | }; 55 | this.addFocusedClass = function () { 56 | addClass(_this.element, Floatl.FOCUSED_CLASS); 57 | }; 58 | this.removeFocusedClass = function () { 59 | removeClass(_this.element, Floatl.FOCUSED_CLASS); 60 | }; 61 | this.element = element; 62 | this.label = element.querySelectorAll(".floatl__label")[0]; 63 | this.input = element.querySelectorAll(".floatl__input")[0]; 64 | // Return early if not both the label and input are present 65 | if (!this.label || !this.input) { 66 | return; 67 | } 68 | if (this.input.nodeName === "TEXTAREA") { 69 | addClass(this.element, Floatl.MULTILINE_CLASS); 70 | } 71 | // Handle initial value 72 | this.handleChange(); 73 | // Bind event listeners 74 | addEventListener(this.input, "focus", this.addFocusedClass); 75 | addEventListener(this.input, "blur", this.removeFocusedClass); 76 | for (var _i = 0, _a = ["keyup", "blur", "change", "input"]; _i < _a.length; _i++) { 77 | var event_1 = _a[_i]; 78 | addEventListener(this.input, event_1, this.handleChange); 79 | } 80 | } 81 | Floatl.prototype.destroy = function () { 82 | removeEventListener(this.input, "focus", this.addFocusedClass); 83 | removeEventListener(this.input, "blur", this.removeFocusedClass); 84 | for (var _i = 0, _a = ["keyup", "blur", "change", "input"]; _i < _a.length; _i++) { 85 | var event_2 = _a[_i]; 86 | removeEventListener(this.input, event_2, this.handleChange); 87 | } 88 | }; 89 | Floatl.FOCUSED_CLASS = "floatl--focused"; 90 | Floatl.ACTIVE_CLASS = "floatl--active"; 91 | Floatl.MULTILINE_CLASS = "floatl--multiline"; 92 | return Floatl; 93 | }()); 94 | 95 | return Floatl; 96 | 97 | }))); 98 | -------------------------------------------------------------------------------- /example/source/layouts/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | <%= current_page.data.title || "Middleman" %> 10 | <%= stylesheet_link_tag "site" %> 11 | <%= javascript_include_tag "vendor/floatl.umd.js" %> 12 | 13 | 14 | <%= yield %> 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/source/stylesheets/floatl.css.scss: -------------------------------------------------------------------------------- 1 | $font-size: 16px; 2 | $vpadding: 7px; 3 | $hpadding: 11px; 4 | 5 | $label-font-size: 11px; 6 | 7 | $duration: 200ms; 8 | $border-color: hsl(0, 0%, 90%); 9 | $text-color: hsl(0, 0%, 40%); 10 | $background-color: white; 11 | $active-color: hsl(209, 82%, 54%); 12 | 13 | .floatl { 14 | position: relative; 15 | } 16 | 17 | .floatl--focused .floatl__label { 18 | color: $active-color; 19 | } 20 | 21 | .floatl--active { 22 | .floatl__label { 23 | visibility: visible; 24 | opacity: 1; 25 | top: 1px; 26 | } 27 | 28 | .floatl__input { 29 | padding: ($vpadding * 3) $hpadding $vpadding; 30 | } 31 | 32 | &.floatl--multiline .floatl__label { 33 | background: linear-gradient( 34 | to bottom, 35 | rgba(white, 0.95) 0%, 36 | rgba(white, 0.95) 80%, 37 | rgba(white, 0) 100% 38 | ); 39 | background-color: $background-color; 40 | } 41 | } 42 | 43 | .floatl__label { 44 | transition: all $duration ease; 45 | position: absolute; 46 | visibility: hidden; 47 | opacity: 0; 48 | top: 3px; 49 | left: $hpadding - 3px + 1px; 50 | display: inline-block; 51 | padding: 6px 3px 3px; 52 | font-weight: bold; 53 | font-size: $label-font-size; 54 | line-height: 1em; 55 | color: $text-color; 56 | } 57 | 58 | .floatl__input { 59 | transition: all $duration ease; 60 | padding: ($vpadding * 2) $hpadding; 61 | background-color: hsl(0, 0%, 98%); 62 | font-size: $font-size; 63 | border: 1px solid $border-color; 64 | border-radius: 5px; 65 | appearance: none; 66 | outline: none; 67 | 68 | &::selection { 69 | background-color: $active-color; 70 | color: white; 71 | } 72 | 73 | &:focus { 74 | border-color: $active-color; 75 | } 76 | } 77 | 78 | input.floatl__input { 79 | height: 47px; 80 | } 81 | -------------------------------------------------------------------------------- /example/source/stylesheets/site.css.scss: -------------------------------------------------------------------------------- 1 | @import './vendor/normalize.css'; 2 | @import 'floatl.css.scss'; 3 | 4 | html { 5 | box-sizing: border-box; 6 | text-rendering: optimizeLegibility; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | *, 12 | *::before, 13 | *::after { 14 | box-sizing: inherit; 15 | } 16 | 17 | body { 18 | animation: gradient 20s ease infinite; 19 | color: #333; 20 | background: linear-gradient(76deg, #2c3e50, #4ca1af); 21 | background-size: 400% 400%; 22 | font-family: -apple-system, BlinkMacSystemFont, "Avenir Next", "Avenir", 23 | "Segoe UI", "Lucida Grande", "Helvetica Neue", "Helvetica", "Fira Sans", 24 | "Roboto", "Noto", "Droid Sans", "Cantarell", "Oxygen", "Ubuntu", 25 | "Franklin Gothic Medium", "Century Gothic", "Liberation Sans", sans-serif; 26 | padding: 18vh 1rem; 27 | } 28 | 29 | @keyframes gradient { 30 | 0% { 31 | background-position: 0% 50% 32 | } 33 | 50% { 34 | background-position: 100% 50% 35 | } 36 | 100% { 37 | background-position: 0% 50% 38 | } 39 | } 40 | 41 | .header { 42 | color: white; 43 | text-align: center; 44 | font-weight: 300; 45 | } 46 | 47 | .container { 48 | will-change: transform; 49 | margin: 0 auto; 50 | padding: 3em; 51 | background-color: white; 52 | max-width: 360px; 53 | border-radius: 5px; 54 | } 55 | 56 | input, textarea { 57 | width: 100%; 58 | } 59 | 60 | textarea { 61 | height: 100px; 62 | } 63 | 64 | .floatl + .floatl { 65 | margin-top: 1em; 66 | } 67 | 68 | footer { 69 | padding-top: 2em; 70 | margin: 2em auto 0; 71 | width: 128px; 72 | text-align: center; 73 | border-top: 1px dashed hsl(0, 0%, 90%); 74 | } 75 | 76 | .github-link { 77 | transition: opacity 300ms ease; 78 | opacity: 0.9; 79 | display: inline-block; 80 | text-decoration: none; 81 | 82 | &:hover { 83 | opacity: 0.6; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /example/source/stylesheets/vendor/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Correct the font size and margin on `h1` elements within `section` and 29 | * `article` contexts in Chrome, Firefox, and Safari. 30 | */ 31 | 32 | h1 { 33 | font-size: 2em; 34 | margin: 0.67em 0; 35 | } 36 | 37 | /* Grouping content 38 | ========================================================================== */ 39 | 40 | /** 41 | * 1. Add the correct box sizing in Firefox. 42 | * 2. Show the overflow in Edge and IE. 43 | */ 44 | 45 | hr { 46 | box-sizing: content-box; /* 1 */ 47 | height: 0; /* 1 */ 48 | overflow: visible; /* 2 */ 49 | } 50 | 51 | /** 52 | * 1. Correct the inheritance and scaling of font size in all browsers. 53 | * 2. Correct the odd `em` font sizing in all browsers. 54 | */ 55 | 56 | pre { 57 | font-family: monospace, monospace; /* 1 */ 58 | font-size: 1em; /* 2 */ 59 | } 60 | 61 | /* Text-level semantics 62 | ========================================================================== */ 63 | 64 | /** 65 | * Remove the gray background on active links in IE 10. 66 | */ 67 | 68 | a { 69 | background-color: transparent; 70 | } 71 | 72 | /** 73 | * 1. Remove the bottom border in Chrome 57- 74 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 75 | */ 76 | 77 | abbr[title] { 78 | border-bottom: none; /* 1 */ 79 | text-decoration: underline; /* 2 */ 80 | text-decoration: underline dotted; /* 2 */ 81 | } 82 | 83 | /** 84 | * Add the correct font weight in Chrome, Edge, and Safari. 85 | */ 86 | 87 | b, 88 | strong { 89 | font-weight: bolder; 90 | } 91 | 92 | /** 93 | * 1. Correct the inheritance and scaling of font size in all browsers. 94 | * 2. Correct the odd `em` font sizing in all browsers. 95 | */ 96 | 97 | code, 98 | kbd, 99 | samp { 100 | font-family: monospace, monospace; /* 1 */ 101 | font-size: 1em; /* 2 */ 102 | } 103 | 104 | /** 105 | * Add the correct font size in all browsers. 106 | */ 107 | 108 | small { 109 | font-size: 80%; 110 | } 111 | 112 | /** 113 | * Prevent `sub` and `sup` elements from affecting the line height in 114 | * all browsers. 115 | */ 116 | 117 | sub, 118 | sup { 119 | font-size: 75%; 120 | line-height: 0; 121 | position: relative; 122 | vertical-align: baseline; 123 | } 124 | 125 | sub { 126 | bottom: -0.25em; 127 | } 128 | 129 | sup { 130 | top: -0.5em; 131 | } 132 | 133 | /* Embedded content 134 | ========================================================================== */ 135 | 136 | /** 137 | * Remove the border on images inside links in IE 10. 138 | */ 139 | 140 | img { 141 | border-style: none; 142 | } 143 | 144 | /* Forms 145 | ========================================================================== */ 146 | 147 | /** 148 | * 1. Change the font styles in all browsers. 149 | * 2. Remove the margin in Firefox and Safari. 150 | */ 151 | 152 | button, 153 | input, 154 | optgroup, 155 | select, 156 | textarea { 157 | font-family: inherit; /* 1 */ 158 | font-size: 100%; /* 1 */ 159 | line-height: 1.15; /* 1 */ 160 | margin: 0; /* 2 */ 161 | } 162 | 163 | /** 164 | * Show the overflow in IE. 165 | * 1. Show the overflow in Edge. 166 | */ 167 | 168 | button, 169 | input { /* 1 */ 170 | overflow: visible; 171 | } 172 | 173 | /** 174 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 175 | * 1. Remove the inheritance of text transform in Firefox. 176 | */ 177 | 178 | button, 179 | select { /* 1 */ 180 | text-transform: none; 181 | } 182 | 183 | /** 184 | * Correct the inability to style clickable types in iOS and Safari. 185 | */ 186 | 187 | button, 188 | [type="button"], 189 | [type="reset"], 190 | [type="submit"] { 191 | -webkit-appearance: button; 192 | } 193 | 194 | /** 195 | * Remove the inner border and padding in Firefox. 196 | */ 197 | 198 | button::-moz-focus-inner, 199 | [type="button"]::-moz-focus-inner, 200 | [type="reset"]::-moz-focus-inner, 201 | [type="submit"]::-moz-focus-inner { 202 | border-style: none; 203 | padding: 0; 204 | } 205 | 206 | /** 207 | * Restore the focus styles unset by the previous rule. 208 | */ 209 | 210 | button:-moz-focusring, 211 | [type="button"]:-moz-focusring, 212 | [type="reset"]:-moz-focusring, 213 | [type="submit"]:-moz-focusring { 214 | outline: 1px dotted ButtonText; 215 | } 216 | 217 | /** 218 | * Correct the padding in Firefox. 219 | */ 220 | 221 | fieldset { 222 | padding: 0.35em 0.75em 0.625em; 223 | } 224 | 225 | /** 226 | * 1. Correct the text wrapping in Edge and IE. 227 | * 2. Correct the color inheritance from `fieldset` elements in IE. 228 | * 3. Remove the padding so developers are not caught out when they zero out 229 | * `fieldset` elements in all browsers. 230 | */ 231 | 232 | legend { 233 | box-sizing: border-box; /* 1 */ 234 | color: inherit; /* 2 */ 235 | display: table; /* 1 */ 236 | max-width: 100%; /* 1 */ 237 | padding: 0; /* 3 */ 238 | white-space: normal; /* 1 */ 239 | } 240 | 241 | /** 242 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 243 | */ 244 | 245 | progress { 246 | vertical-align: baseline; 247 | } 248 | 249 | /** 250 | * Remove the default vertical scrollbar in IE 10+. 251 | */ 252 | 253 | textarea { 254 | overflow: auto; 255 | } 256 | 257 | /** 258 | * 1. Add the correct box sizing in IE 10. 259 | * 2. Remove the padding in IE 10. 260 | */ 261 | 262 | [type="checkbox"], 263 | [type="radio"] { 264 | box-sizing: border-box; /* 1 */ 265 | padding: 0; /* 2 */ 266 | } 267 | 268 | /** 269 | * Correct the cursor style of increment and decrement buttons in Chrome. 270 | */ 271 | 272 | [type="number"]::-webkit-inner-spin-button, 273 | [type="number"]::-webkit-outer-spin-button { 274 | height: auto; 275 | } 276 | 277 | /** 278 | * 1. Correct the odd appearance in Chrome and Safari. 279 | * 2. Correct the outline style in Safari. 280 | */ 281 | 282 | [type="search"] { 283 | -webkit-appearance: textfield; /* 1 */ 284 | outline-offset: -2px; /* 2 */ 285 | } 286 | 287 | /** 288 | * Remove the inner padding in Chrome and Safari on macOS. 289 | */ 290 | 291 | [type="search"]::-webkit-search-decoration { 292 | -webkit-appearance: none; 293 | } 294 | 295 | /** 296 | * 1. Correct the inability to style clickable types in iOS and Safari. 297 | * 2. Change font properties to `inherit` in Safari. 298 | */ 299 | 300 | ::-webkit-file-upload-button { 301 | -webkit-appearance: button; /* 1 */ 302 | font: inherit; /* 2 */ 303 | } 304 | 305 | /* Interactive 306 | ========================================================================== */ 307 | 308 | /* 309 | * Add the correct display in Edge, IE 10+, and Firefox. 310 | */ 311 | 312 | details { 313 | display: block; 314 | } 315 | 316 | /* 317 | * Add the correct display in all browsers. 318 | */ 319 | 320 | summary { 321 | display: list-item; 322 | } 323 | 324 | /* Misc 325 | ========================================================================== */ 326 | 327 | /** 328 | * Add the correct display in IE 10+. 329 | */ 330 | 331 | template { 332 | display: none; 333 | } 334 | 335 | /** 336 | * Add the correct display in IE 10. 337 | */ 338 | 339 | [hidden] { 340 | display: none; 341 | } 342 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | process.env.CHROME_BIN = require("puppeteer").executablePath(); 2 | 3 | const sauceLabs = process.env.CI ? true : false; 4 | const sauceLabsLaunchers = { 5 | sl_win_chrome: { 6 | base: "SauceLabs", 7 | browserName: "chrome", 8 | platform: "Windows 10" 9 | }, 10 | sl_mac_chrome: { 11 | base: "SauceLabs", 12 | browserName: "chrome", 13 | platform: "macOS 10.12" 14 | }, 15 | sl_firefox: { 16 | base: "SauceLabs", 17 | browserName: "firefox", 18 | platform: "Windows 10" 19 | }, 20 | sl_mac_firfox: { 21 | base: "SauceLabs", 22 | browserName: "firefox", 23 | platform: "macOS 10.12" 24 | }, 25 | sl_safari: { 26 | base: "SauceLabs", 27 | browserName: "safari", 28 | platform: "macOS 10.12" 29 | }, 30 | sl_edge: { 31 | base: "SauceLabs", 32 | browserName: "MicrosoftEdge", 33 | platform: "Windows 10" 34 | }, 35 | sl_ie_11: { 36 | base: "SauceLabs", 37 | browserName: "internet explorer", 38 | version: "11.103", 39 | platform: "Windows 10" 40 | }, 41 | sl_ie_10: { 42 | base: "SauceLabs", 43 | browserName: "internet explorer", 44 | version: "10.0", 45 | platform: "Windows 7" 46 | }, 47 | sl_ios_safari_9: { 48 | base: "SauceLabs", 49 | browserName: "iphone", 50 | version: "10.3" 51 | }, 52 | "SL_ANDROID4.4": { 53 | base: "SauceLabs", 54 | browserName: "android", 55 | platform: "Linux", 56 | version: "4.4" 57 | }, 58 | SL_ANDROID5: { 59 | base: "SauceLabs", 60 | browserName: "android", 61 | platform: "Linux", 62 | version: "5.1" 63 | }, 64 | SL_ANDROID6: { 65 | base: "SauceLabs", 66 | browserName: "Chrome", 67 | platform: "Android", 68 | version: "6.0", 69 | device: "Android Emulator" 70 | } 71 | }; 72 | 73 | module.exports = function(config) { 74 | config.set({ 75 | concurrency: 5, 76 | 77 | frameworks: ["jasmine", "karma-typescript"], 78 | 79 | files: ["src/**/*.ts", "spec/**/*.ts"], 80 | 81 | preprocessors: { 82 | "src/**/*.ts": ["karma-typescript", "coverage"], 83 | "spec/**/*.ts": "karma-typescript" 84 | }, 85 | 86 | reporters: ["progress", "coverage", "karma-typescript", "notify"].concat( 87 | sauceLabs ? "saucelabs" : [] 88 | ), 89 | 90 | customLaunchers: sauceLabs ? sauceLabsLaunchers : {}, 91 | 92 | browsers: sauceLabs ? Object.keys(sauceLabsLaunchers) : ["ChromeHeadless"], 93 | 94 | coverageReporter: { 95 | type: "lcov", 96 | subdir: ".", 97 | file: "lcov.info" 98 | }, 99 | 100 | sauceLabs: { 101 | build: process.env.SEMAPHORE_BUILD_NUMBER, 102 | testName: "Karma tests" 103 | } 104 | }); 105 | }; 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "floatl", 3 | "version": "2.0.1", 4 | "description": "A pragmatic implementation of the Float Label Pattern", 5 | "homepage": "https://github.com/richardvenneman/floatl", 6 | "bugs": { 7 | "url": "https://github.com/richardvenneman/floatl/issues" 8 | }, 9 | "license": "MIT", 10 | "author": "Richard Venneman (richardvenneman@me.com)", 11 | "files": [ 12 | "built" 13 | ], 14 | "main": "built/floatl.cjs.js", 15 | "module": "built/floatl.esm.js", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/richardvenneman/floatl.git" 19 | }, 20 | "scripts": { 21 | "build": "rollup -c", 22 | "dev": "rollup -c -w", 23 | "test": "karma start --single-run", 24 | "tdd": "karma start", 25 | "prepare": "npm run build" 26 | }, 27 | "browser": "built/floatl.umd.js", 28 | "dependencies": {}, 29 | "devDependencies": { 30 | "@types/jasmine": "^2.8.6", 31 | "jasmine": "^3.0.0", 32 | "jasmine-core": "^2.99.1", 33 | "karma": "^3.0.0", 34 | "karma-chrome-launcher": "^2.2.0", 35 | "karma-cli": "^1.0.1", 36 | "karma-jasmine": "^1.1.1", 37 | "karma-notify-reporter": "^1.0.1", 38 | "karma-sauce-launcher": "^1.2.0", 39 | "karma-typescript": "^3.0.12", 40 | "prettier": "1.17.1", 41 | "puppeteer": "^1.0.0", 42 | "rollup": "^0.68.2", 43 | "rollup-plugin-typescript2": "^0.15.1", 44 | "tslint-config-prettier": "^1.8.0", 45 | "typescript": "^2.7.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import pkg from "./package.json"; 3 | 4 | const banner = `/* ${pkg.name} version ${pkg.version} */` 5 | const exports = "default" 6 | 7 | export default [ 8 | { 9 | input: "src/main.ts", 10 | output: { banner, exports, file: pkg.browser, format: "umd", name: "Floatl" }, 11 | plugins: [typescript()] 12 | }, 13 | { 14 | input: "src/main.ts", 15 | output: [ 16 | { banner, exports, file: pkg.main, format: "cjs" }, 17 | { banner, exports, file: pkg.module, format: "es" } 18 | ], 19 | plugins: [typescript()] 20 | } 21 | ]; 22 | -------------------------------------------------------------------------------- /spec/main.spec.ts: -------------------------------------------------------------------------------- 1 | import Floatl from "../src/main" 2 | import { trigger } from "./specHelpers" 3 | 4 | describe("Floatl", () => { 5 | let fixture 6 | 7 | beforeEach(() => { 8 | document.body.insertAdjacentHTML( 9 | "afterbegin", 10 | `
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
` 20 | ) 21 | fixture = document.getElementById("fixture") 22 | }) 23 | 24 | afterEach(() => { 25 | document.body.removeChild(fixture) 26 | }) 27 | 28 | describe("Initialization", () => { 29 | it("Inits with a HTML element", () => { 30 | const action = () => { 31 | return new Floatl(document.getElementById("floatlOne")) 32 | } 33 | 34 | expect(action).not.toThrow() 35 | }) 36 | 37 | it(`adds ${Floatl.ACTIVE_CLASS} class if input has a value`, () => { 38 | const element = document.getElementById("floatlOne") 39 | const input = document.getElementById("floatlOneInput"); 40 | (input as HTMLInputElement).value = "Initial value" 41 | const floatl = new Floatl(element) 42 | 43 | expect(element.classList.contains(Floatl.ACTIVE_CLASS)).toBeTruthy() 44 | }) 45 | 46 | it(`does not add ${Floatl.MULTILINE_CLASS} class if not a textarea`, () => { 47 | const element = document.getElementById("floatlOne") 48 | const floatl = new Floatl(element) 49 | 50 | expect(element.classList.contains(Floatl.MULTILINE_CLASS)).toBeFalsy() 51 | }) 52 | 53 | it(`adds ${Floatl.MULTILINE_CLASS} class if applied to a textarea`, () => { 54 | const element = document.getElementById("floatlTwo") 55 | const floatl = new Floatl(element) 56 | 57 | expect(element.classList.contains(Floatl.MULTILINE_CLASS)).toBeTruthy() 58 | }) 59 | }) 60 | 61 | describe("Focused state", () => { 62 | let element 63 | let input 64 | 65 | beforeEach(() => { 66 | element = document.getElementById("floatlOne") 67 | input = document.getElementById("floatlOneInput") 68 | const floatl = new Floatl(element) 69 | }) 70 | 71 | it(`adds ${Floatl.FOCUSED_CLASS} class on focus`, () => { 72 | expect(element.classList.contains(Floatl.FOCUSED_CLASS)).toBeFalsy() 73 | 74 | trigger(input, "focus") 75 | 76 | expect(element.classList.contains(Floatl.FOCUSED_CLASS)).toBeTruthy() 77 | }) 78 | 79 | it(`removes ${Floatl.FOCUSED_CLASS} class on blur`, () => { 80 | trigger(input, "focus") 81 | 82 | expect(element.classList.contains(Floatl.FOCUSED_CLASS)).toBeTruthy() 83 | 84 | trigger(input, "blur") 85 | 86 | expect(element.classList.contains(Floatl.FOCUSED_CLASS)).toBeFalsy() 87 | }) 88 | }) 89 | 90 | describe("Active state", () => { 91 | let element 92 | let input 93 | 94 | beforeEach(() => { 95 | element = document.getElementById("floatlOne") 96 | input = document.getElementById("floatlOneInput") 97 | const floatl = new Floatl(element) 98 | }) 99 | 100 | it(`adds ${Floatl.ACTIVE_CLASS} class when entering characters`, () => { 101 | expect(element.classList.contains(Floatl.ACTIVE_CLASS)).toBeFalsy() 102 | 103 | input.value = "test" 104 | trigger(input, "keyup") 105 | 106 | expect(element.classList.contains(Floatl.ACTIVE_CLASS)).toBeTruthy() 107 | }) 108 | 109 | it(`removes ${Floatl.ACTIVE_CLASS} removing all characters`, () => { 110 | input.value = "test" 111 | trigger(input, "keyup") 112 | input.value = "" 113 | trigger(input, "keyup") 114 | 115 | expect(element.classList.contains(Floatl.ACTIVE_CLASS)).toBeFalsy() 116 | }) 117 | }) 118 | 119 | describe("Destroy", () => { 120 | it("removes event listeners when destroying", () => { 121 | const element = document.getElementById("floatlOne") 122 | const input = document.getElementById( 123 | "floatlOneInput" 124 | ) as HTMLInputElement 125 | const floatl = new Floatl(element) 126 | 127 | floatl.destroy() 128 | 129 | trigger(input, "focus") 130 | 131 | expect(element.classList.contains(Floatl.FOCUSED_CLASS)).toBeFalsy() 132 | 133 | input.value = "test" 134 | trigger(input, "keyup") 135 | 136 | expect(element.classList.contains(Floatl.ACTIVE_CLASS)).toBeFalsy() 137 | }) 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /spec/specHelpers.ts: -------------------------------------------------------------------------------- 1 | export function trigger(element: any, eventName: string) { 2 | if (document.createEvent) { 3 | const event = document.createEvent("HTMLEvents") 4 | event.initEvent(eventName, true, false) 5 | element.dispatchEvent(event) 6 | } else { 7 | element.fireEvent(eventName) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addClass, 3 | addEventListener, 4 | removeClass, 5 | removeEventListener 6 | } from "../src/utils" 7 | 8 | describe("Utils", () => { 9 | let fixture 10 | 11 | beforeEach(() => { 12 | document.body.insertAdjacentHTML("afterbegin", `
`) 13 | fixture = document.getElementById("fixture") 14 | }) 15 | 16 | afterEach(() => { 17 | document.body.removeChild(fixture) 18 | }) 19 | 20 | describe("addClass", () => { 21 | it("adds a className to a DOM element", () => { 22 | addClass(fixture, "test") 23 | 24 | expect(fixture.className).toEqual("test") 25 | }) 26 | 27 | it("adds a className to a DOM element with existing classes", () => { 28 | fixture.insertAdjacentHTML( 29 | "afterbegin", 30 | `
` 31 | ) 32 | const element = document.getElementById("element") 33 | 34 | addClass(element, "test") 35 | 36 | expect(element.className).toEqual("one test") 37 | }) 38 | }) 39 | 40 | describe("removeClass", () => { 41 | it("removes a className from a DOM element", () => { 42 | addClass(fixture, "test") 43 | 44 | expect(fixture.className).toEqual("test") 45 | 46 | removeClass(fixture, "test") 47 | 48 | expect(fixture.className).toEqual("") 49 | }) 50 | }) 51 | 52 | describe("addEventListener", () => { 53 | it("executes callback on attached DOM events", () => { 54 | const event = document.createEvent("HTMLEvents") 55 | event.initEvent("click", true, true) 56 | 57 | const callback = () => { 58 | fixture.innerHTML = "callback changed value" 59 | } 60 | 61 | addEventListener(fixture, "click", callback) 62 | fixture.dispatchEvent(event) 63 | 64 | expect(fixture.innerHTML).toEqual("callback changed value") 65 | }) 66 | }) 67 | 68 | describe("removeEventListener", () => { 69 | it("works", () => { 70 | const cb = () => (fixture.innerHTML = "test") 71 | const event = document.createEvent("HTMLEvents") 72 | event.initEvent("click", true, true) 73 | 74 | addEventListener(fixture, "click", cb) 75 | fixture.dispatchEvent(event) 76 | 77 | expect(fixture.innerHTML).toEqual("test") 78 | 79 | fixture.innerHTML = "" 80 | 81 | removeEventListener(fixture, "click", cb) 82 | fixture.dispatchEvent(event) 83 | 84 | expect(fixture.innerHTML).toEqual("") 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addClass, 3 | addEventListener, 4 | removeClass, 5 | removeEventListener 6 | } from "./utils" 7 | 8 | export default class Floatl { 9 | public static readonly FOCUSED_CLASS = "floatl--focused" 10 | public static readonly ACTIVE_CLASS = "floatl--active" 11 | public static readonly MULTILINE_CLASS = "floatl--multiline" 12 | 13 | private element: HTMLElement 14 | private label: Element 15 | private input: Element 16 | 17 | constructor(element: HTMLElement) { 18 | this.element = element 19 | this.label = element.querySelectorAll(".floatl__label")[0] 20 | this.input = element.querySelectorAll(".floatl__input")[0] 21 | 22 | // Return early if not both the label and input are present 23 | if (!this.label || !this.input) { 24 | return 25 | } 26 | 27 | if (this.input.nodeName === "TEXTAREA") { 28 | addClass(this.element, Floatl.MULTILINE_CLASS) 29 | } 30 | 31 | // Handle initial value 32 | this.handleChange() 33 | 34 | // Bind event listeners 35 | addEventListener(this.input, "focus", this.addFocusedClass) 36 | addEventListener(this.input, "blur", this.removeFocusedClass) 37 | 38 | for (const event of ["keyup", "blur", "change", "input"]) { 39 | addEventListener(this.input, event, this.handleChange) 40 | } 41 | } 42 | 43 | public destroy() { 44 | removeEventListener(this.input, "focus", this.addFocusedClass) 45 | removeEventListener(this.input, "blur", this.removeFocusedClass) 46 | 47 | for (const event of ["keyup", "blur", "change", "input"]) { 48 | removeEventListener(this.input, event, this.handleChange) 49 | } 50 | } 51 | 52 | private handleChange = () => { 53 | if ((this.input as HTMLInputElement | HTMLTextAreaElement).value === "") { 54 | removeClass(this.element, Floatl.ACTIVE_CLASS) 55 | } else { 56 | addClass(this.element, Floatl.ACTIVE_CLASS) 57 | } 58 | } 59 | 60 | private addFocusedClass = () => { 61 | addClass(this.element, Floatl.FOCUSED_CLASS) 62 | } 63 | 64 | private removeFocusedClass = () => { 65 | removeClass(this.element, Floatl.FOCUSED_CLASS) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function addClass(element: Element, className: string) { 2 | if (element.classList) { 3 | element.classList.add(className) 4 | } else { 5 | element.className += ` ${className}` 6 | } 7 | } 8 | 9 | export function removeClass(element: Element, className: string) { 10 | if (element.classList) { 11 | element.classList.remove(className) 12 | } else { 13 | const re = new RegExp( 14 | "(^|\\b)" + className.split(" ").join("|") + "(\\b|$)", 15 | "gi" 16 | ) 17 | element.className = element.className.replace(re, " ") 18 | } 19 | } 20 | 21 | export function addEventListener(element: Element, event: string, cb: any) { 22 | if (element.addEventListener) { 23 | element.addEventListener(event, cb) 24 | } else { 25 | (element as any).attachEvent(`on${event}`, () => { 26 | cb.call(element) 27 | }) 28 | } 29 | } 30 | 31 | export function removeEventListener(element: Element, event: string, cb: any) { 32 | if (element.removeEventListener) { 33 | element.removeEventListener(event, cb) 34 | } else { 35 | (element as any).detachEvent(`on${event}`, cb) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "./built", 5 | "target": "ES3", 6 | "sourceMap": true 7 | }, 8 | "include": ["./src/**/*"], 9 | "exclude": ["**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "semicolon": [true, "never"] 5 | } 6 | } 7 | --------------------------------------------------------------------------------