├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── License.md ├── Readme.md ├── Task.md ├── app ├── actions │ └── AppActions.js ├── alt.js ├── app.js ├── components │ ├── Application.js │ ├── EditableBox.js │ ├── FieldWithUnit.js │ ├── FontSelect.js │ ├── Footer.js │ ├── Header.js │ ├── PixelField.js │ ├── Select.js │ ├── TypeParamsForm.js │ ├── TypeStatsContainer.js │ ├── TypeStatsWidget.js │ └── __tests__ │ │ ├── Application-test.js │ │ ├── EditableBox-test.js │ │ ├── FieldWithUnit-test.js │ │ ├── FontSelect-test.js │ │ ├── PixelField-test.js │ │ └── Select-test.js ├── stores │ ├── AppStateStore.js │ └── __tests__ │ │ └── AppStateStore-test.js └── utils │ ├── __tests__ │ ├── newid-test.js │ ├── react-extras-test.js │ └── type-stats-test.js │ ├── newid.js │ ├── react-extras.js │ └── type-stats.js ├── build ├── bundle.js └── index.css ├── gulpfile.js ├── index.html ├── jestsetup.js ├── package.json ├── styles ├── blocks │ ├── footer.styl │ ├── header.styl │ ├── type-params-form.styl │ └── type-stats.styl ├── index.styl └── styles.styl ├── tamia ├── modules │ ├── checkbox │ │ ├── Readme.md │ │ ├── check.svg │ │ ├── example.html │ │ ├── index.styl │ │ └── radio.svg │ ├── code │ │ ├── Readme.md │ │ ├── example.html │ │ ├── index.styl │ │ └── tomorrow.styl │ ├── fade │ │ ├── Readme.md │ │ ├── example.html │ │ ├── index.styl │ │ ├── mask.svg │ │ └── mask_src.svg │ ├── flippable │ │ ├── Readme.md │ │ ├── example.html │ │ ├── index.styl │ │ └── script.js │ ├── form │ │ ├── Readme.md │ │ ├── example.html │ │ ├── index.styl │ │ └── script.js │ ├── list │ │ ├── Readme.md │ │ ├── example.html │ │ └── index.styl │ ├── media │ │ ├── Readme.md │ │ ├── example.html │ │ └── index.styl │ ├── modal │ │ ├── Readme.md │ │ ├── example.html │ │ ├── index.styl │ │ └── script.js │ ├── password │ │ ├── Readme.md │ │ ├── example.html │ │ ├── index.styl │ │ └── script.js │ ├── preload │ │ ├── Readme.md │ │ └── script.js │ ├── print │ │ ├── Readme.md │ │ └── index.styl │ ├── richtypo │ │ ├── Readme.md │ │ └── index.styl │ ├── rouble │ │ ├── Readme.md │ │ ├── example.html │ │ ├── fonts │ │ │ └── rouble-arial-regular.woff │ │ └── index.styl │ ├── select │ │ ├── Readme.md │ │ ├── arrow.svg │ │ ├── example.html │ │ ├── index.styl │ │ └── script.js │ ├── spinner │ │ ├── Readme.md │ │ ├── example.html │ │ ├── index.styl │ │ ├── script.js │ │ └── spinner.gif │ ├── switcher │ │ ├── Readme.md │ │ ├── example.html │ │ └── index.styl │ ├── table │ │ ├── Readme.md │ │ ├── example.html │ │ └── index.styl │ ├── text │ │ ├── Readme.md │ │ ├── example.html │ │ └── index.styl │ ├── toggle │ │ ├── Readme.md │ │ ├── example.html │ │ ├── index.styl │ │ └── script.js │ └── tooltip │ │ ├── Readme.md │ │ ├── example.html │ │ └── index.styl ├── tamia │ ├── bootstrap.styl │ ├── classes.styl │ ├── component.js │ ├── functions.styl │ ├── images.styl │ ├── index.styl │ ├── layout.styl │ ├── links.styl │ ├── mediaqueries.styl │ ├── misc.styl │ ├── opor.js │ └── tamia.js └── vendor │ └── transition-events.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "ecmaFeatures": { 7 | "modules": true, 8 | "jsx": true 9 | }, 10 | "rules": { 11 | "no-alert": 2, 12 | "no-array-constructor": 2, 13 | "no-caller": 2, 14 | "no-bitwise": 0, 15 | "no-catch-shadow": 2, 16 | "no-console": 1, 17 | "no-comma-dangle": 1, 18 | "no-control-regex": 2, 19 | "no-debugger": 1, 20 | "no-div-regex": 2, 21 | "no-dupe-keys": 2, 22 | "no-else-return": 0, 23 | "no-empty": 2, 24 | "no-empty-class": 2, 25 | "no-eq-null": 2, 26 | "no-eval": 2, 27 | "no-ex-assign": 2, 28 | "no-func-assign": 0, 29 | "no-floating-decimal": 2, 30 | "no-implied-eval": 2, 31 | "no-with": 2, 32 | "no-fallthrough": 2, 33 | "no-unreachable": 2, 34 | "no-undef": 2, 35 | "no-undef-init": 2, 36 | "no-unused-expressions": 2, 37 | "no-octal": 2, 38 | "no-octal-escape": 2, 39 | "no-obj-calls": 2, 40 | "no-multi-str": 2, 41 | "no-new-wrappers": 2, 42 | "no-new": 2, 43 | "no-new-func": 2, 44 | "no-native-reassign": 2, 45 | "no-plusplus": 0, 46 | "no-delete-var": 2, 47 | "no-return-assign": 2, 48 | "no-new-object": 2, 49 | "no-label-var": 2, 50 | "no-ternary": 0, 51 | "no-self-compare": 2, 52 | "no-sync": 2, 53 | "no-underscore-dangle": 0, 54 | "no-loop-func": 2, 55 | "no-empty-label": 2, 56 | "no-unused-vars": 1, 57 | "no-script-url": 2, 58 | "no-proto": 2, 59 | "no-iterator": 2, 60 | "no-mixed-requires": [0, false], 61 | "no-wrap-func": 2, 62 | "no-shadow": 2, 63 | "no-use-before-define": 0, 64 | "no-redeclare": 2, 65 | "no-regex-spaces": 2, 66 | "brace-style": 0, 67 | "block-scoped-var": 0, 68 | "camelcase": 2, 69 | "comma-dangle": 1, 70 | "complexity": [0, 11], 71 | "consistent-this": [0, "that"], 72 | "curly": 2, 73 | "dot-notation": [0, {"allowKeywords": false}], 74 | "eqeqeq": 2, 75 | "guard-for-in": 0, 76 | "max-depth": [0, 4], 77 | "max-len": [0, 80, 4], 78 | "max-params": [0, 3], 79 | "max-statements": [0, 10], 80 | "new-cap": 2, 81 | "new-parens": 2, 82 | "one-var": 0, 83 | "quotes": [2, "single"], 84 | "quote-props": 0, 85 | "radix": 0, 86 | "semi": 1, 87 | "strict": 2, 88 | "unnecessary-strict": 0, 89 | "global-strict": 0, 90 | "use-isnan": 2, 91 | "valid-typeof": 0, 92 | "wrap-iife": 2, 93 | "wrap-regex": 0, 94 | "no-irregular-whitespace": 0 95 | }, 96 | "globals": { 97 | "React": false 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | dist 4 | .publish 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright © 2015 Artem Sapegin (http://sapegin.me), contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # React Text Statistics 2 | 3 | [![Build Status](https://travis-ci.org/sapegin/react-text-stats.svg)](https://travis-ci.org/sapegin/react-text-stats) 4 | 5 | My [coding assignment](Task.md) for [Here](http://360.here.com/). 6 | 7 | Try the [deployed app](http://sapegin.github.io/react-text-stats/) or read below hot to run it locally. 8 | 9 | 10 | ## How to run locally 11 | 12 | First checkout the repository (`$ git clone https://github.com/sapegin/react-text-stats.git`) or download it as a [ZIP file](https://github.com/sapegin/react-text-stats/archive/master.zip). 13 | 14 | Then install dependencies: 15 | 16 | ``` 17 | $ npm install 18 | ``` 19 | 20 | And run development server: 21 | 22 | ``` 23 | $ npm run server 24 | ``` 25 | 26 | It’ll open the app in your browser. 27 | 28 | 29 | ## What’s inside 30 | 31 | ### App 32 | 33 | * [React](http://facebook.github.io/react/). 34 | * [alt](http://goatslacker.github.io/alt/) — a simple Flux implementation: stores, actions, etc. 35 | * [bem-cn](https://github.com/albburtsev/bem-cn) — BEM-style class name generator. 36 | * [lodash](https://lodash.com/). 37 | 38 | ### Styles 39 | 40 | * [Stylus](http://learnboost.github.io/stylus/). 41 | * [Autoprefixer](https://github.com/postcss/autoprefixer) (via [Stylobuild](https://github.com/kizu/stylobuild) Stylus plugin). 42 | * [Tâmia](http://tamiadev.github.io/tamia/) — my front-end framework. 43 | 44 | ### Build 45 | 46 | * [Gulp](http://gulpjs.com/). 47 | * [Webpack](http://webpack.github.io/). 48 | * [Babel](http://babeljs.io/) (as a [Webpack loader](https://github.com/babel/babel-loader)) — ES6/JSX trasformation. 49 | 50 | ### Testing 51 | 52 | * [Jest](http://facebook.github.io/jest/). 53 | 54 | ### Debugging 55 | 56 | * [BrowserSync](http://www.browsersync.io/). 57 | * [eslint](http://eslint.org/). 58 | 59 | 60 | ## Author 61 | 62 | * [Artem Sapegin](http://sapegin.me/) 63 | 64 | 65 | --- 66 | 67 | ## License 68 | 69 | The MIT License, see the included [License.md](License.md) file. 70 | -------------------------------------------------------------------------------- /Task.md: -------------------------------------------------------------------------------- 1 | # Text statistics 2 | 3 | Create JavaScript application that shows statistics for given text. 4 | 5 | View should contain: 6 | 7 | 1. Input for text 8 | 2. Input for font size 9 | 3. Select for fonts 10 | 4. Input for maximum width of text in pixels 11 | 5. Component displaying text statistics and text visualization 12 | 13 | Statistics should include: 14 | 15 | 1. Width of the longest line of the text in pixels 16 | 2. Number of lines 17 | 18 | Other: 19 | 20 | 1. Usage of external libraries is allowed 21 | 2. Application should calculate number of lines for wrapped text. If the text takes more space that is allowed for a single line, actual number of lines required to display that text should be calculated. 22 | 3. Multiple whitespaces between words and line breaks are allowed in the input text 23 | 4. Allow selecting from at least 3 different fonts (at least one bold) 24 | 5. All calculations and visualization should be made for selected font and font size 25 | 6. Should work in Google Chrome >= 39 26 | 7. Deliver your result in the best professional quality you can produce. Polish your solution. Make a master piece out of it. It is part of this task to compare what different people consider to be a professional quality solution. Remember about tests! 27 | 8. Store your solution on GitHub or BitBucket 28 | 29 | Example: 30 | 31 | “At w3schools.com you will learn how to make a website. We offer free tutorials in all web development technologies.” 32 | 33 | ![](http://wow.sapegin.me/image/022K1G3a1N1P/Image%202015-04-08%20at%203.47.17%20PM.png) 34 | 35 | Application should display values close to: 36 | 37 | ``` 38 | WIDTH 146px 39 | LINES 4 40 | ``` 41 | 42 | Visualization of the wrapped text: 43 | 44 | ![](http://wow.sapegin.me/image/25070t14282Y/Image%202015-04-08%20at%203.48.00%20PM.png) 45 | -------------------------------------------------------------------------------- /app/actions/AppActions.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin, http://sapegin.me, 2015 2 | 3 | import alt from '../alt'; 4 | 5 | class AppActions { 6 | constructor() { 7 | this.generateActions( 8 | 'updateAppState' 9 | ); 10 | } 11 | } 12 | 13 | export default alt.createActions(AppActions); 14 | -------------------------------------------------------------------------------- /app/alt.js: -------------------------------------------------------------------------------- 1 | import Alt from 'alt'; 2 | export default new Alt(); 3 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Application from './components/Application'; 4 | 5 | React.render(React.createElement(Application), document.getElementById('app')); 6 | -------------------------------------------------------------------------------- /app/components/Application.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin, http://sapegin.me, 2015 2 | 3 | import Header from './Header'; 4 | import Footer from './Footer'; 5 | import TypeStatsContainer from './TypeStatsContainer'; 6 | 7 | export default React.createClass({ 8 | displayName: 'Application', 9 | 10 | render() { 11 | return ( 12 |
13 |
14 | 15 |
17 | ); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /app/components/EditableBox.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin, http://sapegin.me, 2015 2 | 3 | import _ from 'lodash'; 4 | import { RestAttrsMixin } from '../utils/react-extras'; 5 | 6 | let PT = React.PropTypes; 7 | 8 | export default React.createClass({ 9 | displayName: 'EditableBox', 10 | propTypes: { 11 | value: PT.string.isRequired, 12 | onChange: PT.func 13 | }, 14 | mixins: [ 15 | RestAttrsMixin 16 | ], 17 | 18 | getElement() { 19 | return React.findDOMNode(this.refs.text); 20 | }, 21 | 22 | getValue() { 23 | return this.getElement().textContent; 24 | }, 25 | 26 | _handleKeydown(event) { 27 | // Disable formatting hotkeys 28 | if (event.ctrlKey || event.metaKey) { 29 | // B, I, U 30 | if (_.includes([66, 73, 85], event.keyCode)) { 31 | event.preventDefault(); 32 | } 33 | } 34 | }, 35 | 36 | _handleInput() { 37 | this._emitChange(this.getValue()); 38 | }, 39 | 40 | _handleBlur() { 41 | let text = this.getValue(); 42 | 43 | // Normalize spaces 44 | text = text 45 | .replace(/ /g, ' ') 46 | .replace(/\s+/g, ' ') 47 | ; 48 | 49 | this._emitChange(text); 50 | }, 51 | 52 | _emitChange(newValue) { 53 | if (this.props.onChange) { 54 | this.props.onChange(newValue); 55 | } 56 | }, 57 | 58 | render() { 59 | return ( 60 |
66 | {this.props.value} 67 |
68 | ); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /app/components/FieldWithUnit.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin, http://sapegin.me, 2015 2 | 3 | import Block from 'bem-cn'; 4 | import { RestAttrsMixin } from '../utils/react-extras'; 5 | 6 | let PT = React.PropTypes; 7 | let b = new Block('field-with-unit'); 8 | 9 | export default React.createClass({ 10 | displayName: 'FieldWithUnit', 11 | propTypes: { 12 | id: PT.string.isRequired, 13 | value: PT.any.isRequired, 14 | unit: PT.string.isRequired 15 | }, 16 | mixins: [ 17 | RestAttrsMixin 18 | ], 19 | 20 | getElement() { 21 | return this.refs.field.getDOMNode(); 22 | }, 23 | 24 | getValue() { 25 | return this.getElement().value; 26 | }, 27 | 28 | render() { 29 | let { id, value, unit } = this.props; 30 | return ( 31 |
32 | 33 | 34 |
35 | ); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /app/components/FontSelect.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin http://sapegin.me, 2015 2 | 3 | import Select from './Select'; 4 | 5 | export default React.createClass({ 6 | displayName: 'FontSelect', 7 | 8 | getDefaultProps() { 9 | return { 10 | items: [ 11 | 'Courier New Regular', 12 | 'Courier New Bold', 13 | 'Georgia Regular', 14 | 'Georgia Bold', 15 | 'Helvetica Regular', 16 | 'Helvetica Bold', 17 | 'Tahoma Regular', 18 | 'Tahoma Bold', 19 | 'Times New Roman Regular', 20 | 'Times New Roman Bold', 21 | 'Trebuchet MS Regular', 22 | 'Trebuchet MS Bold', 23 | 'Verdana Regular', 24 | 'Verdana Bold' 25 | ] 26 | }; 27 | }, 28 | 29 | getValue() { 30 | return this.refs.select.getValue(); 31 | }, 32 | 33 | render() { 34 | return ( 35 | 117 | {options} 118 | 119 | 120 | ); 121 | } 122 | }); 123 | -------------------------------------------------------------------------------- /app/components/TypeParamsForm.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin, http://sapegin.me, 2015 2 | 3 | import _ from 'lodash'; 4 | import Block from 'bem-cn'; 5 | import newId from '../utils/newid'; 6 | import FontSelect from './FontSelect'; 7 | import PixelField from './PixelField'; 8 | 9 | let b = new Block('form'); 10 | let PT = React.PropTypes; 11 | 12 | export default React.createClass({ 13 | displayName: 'TypeParamsForm', 14 | propTypes: { 15 | onChange: PT.func.isRequired 16 | }, 17 | throttleDelay: 50, 18 | 19 | componentWillMount() { 20 | this._handleChangeDelayed = _.throttle(this._handleChange, this.throttleDelay); 21 | }, 22 | 23 | _handleChange() { 24 | this.props.onChange({ 25 | font: this.refs.font.getValue(), 26 | fontSize: this.refs.fontSize.getValue(), 27 | maxWidth: this.refs.maxWidth.getValue() 28 | }); 29 | }, 30 | 31 | render() { 32 | let fontId = newId(); 33 | let fontSizeId = newId(); 34 | let maxWidthId = newId(); 35 | let { font, fontSize, maxWidth } = this.props.app; 36 | 37 | return ( 38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 | 55 |
56 |
57 |
58 | ); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /app/components/TypeStatsContainer.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin, http://sapegin.me, 2015 2 | 3 | import AltContainer from 'alt/components/AltContainer' 4 | import AppStateStore from '../stores/AppStateStore'; 5 | import AppActions from '../actions/AppActions'; 6 | import TypeParamsForm from './TypeParamsForm'; 7 | import TypeStatsWidget from './TypeStatsWidget'; 8 | 9 | export default React.createClass({ 10 | displayName: 'TypeStatsContainer', 11 | 12 | render() { 13 | return ( 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /app/components/TypeStatsWidget.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin, http://sapegin.me, 2015 2 | 3 | import Block from 'bem-cn'; 4 | import { calculate, parseFontName } from '../utils/type-stats'; 5 | import EditableBox from './EditableBox'; 6 | 7 | let b = new Block('type-stats'); 8 | let PT = React.PropTypes; 9 | 10 | export default React.createClass({ 11 | displayName: 'TypeStatsWidget', 12 | propTypes: { 13 | onChange: PT.func.isRequired 14 | }, 15 | 16 | _handleChange(text) { 17 | this.props.onChange({ 18 | text: text 19 | }); 20 | }, 21 | 22 | _plural(number, ...forms) { 23 | return number === 1 ? forms[0] : forms[1]; 24 | }, 25 | 26 | render() { 27 | let { font, fontSize, maxWidth } = this.props.app; 28 | let { fontFamily, fontWeight } = parseFontName(font); 29 | let { width, lines, overflow } = calculate(this.props.app); 30 | 31 | return ( 32 |
33 |
34 |
35 | 39 |
40 |
Required width: {width}px, {lines} {this._plural(lines, 'line', 'lines')}
41 |
42 | ); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /app/components/__tests__/Application-test.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin http://sapegin.me, 2015 2 | 3 | jest.dontMock('../Application'); 4 | let Application = require('../Application'); 5 | 6 | describe('components/Application', () => { 7 | 8 | let render = () => { 9 | return TestUtils.renderIntoDocument( 10 | 11 | ); 12 | }; 13 | 14 | it('should be component of type Application', () => { 15 | let element = render(); 16 | let isCompositeComponentOfType = TestUtils.isCompositeComponentWithType(element, Application); 17 | expect(isCompositeComponentOfType).toBe(true); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /app/components/__tests__/EditableBox-test.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin http://sapegin.me, 2015 2 | 3 | jest.dontMock('../EditableBox'); 4 | let EditableBox = require('../EditableBox'); 5 | 6 | describe('components/EditableBox', () => { 7 | 8 | const defaultProps = { 9 | value: 'The quick brown hamster' 10 | }; 11 | 12 | let render = (props=defaultProps) => { 13 | return TestUtils.renderIntoDocument( 14 | 15 | ); 16 | }; 17 | 18 | it('should be component of type EditableBox', () => { 19 | let element = render(); 20 | let isCompositeComponentOfType = TestUtils.isCompositeComponentWithType(element, EditableBox); 21 | expect(isCompositeComponentOfType).toBe(true); 22 | }); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /app/components/__tests__/FieldWithUnit-test.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin http://sapegin.me, 2015 2 | 3 | jest.dontMock('../FieldWithUnit'); 4 | let FieldWithUnit = require('../FieldWithUnit'); 5 | 6 | describe('components/FieldWithUnit', () => { 7 | 8 | const defaultProps = { 9 | id: 'pony', 10 | value: '20', 11 | unit: 'px' 12 | }; 13 | 14 | let render = (props=defaultProps) => { 15 | return TestUtils.renderIntoDocument( 16 | 17 | ); 18 | }; 19 | 20 | it('should be component of type FieldWithUnit', () => { 21 | let element = render(); 22 | let isCompositeComponentOfType = TestUtils.isCompositeComponentWithType(element, FieldWithUnit); 23 | expect(isCompositeComponentOfType).toBe(true); 24 | }); 25 | 26 | it('getValue() should return default value', () => { 27 | let element = render(); 28 | let value = element.getValue(); 29 | expect(value).toEqual(defaultProps.value); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /app/components/__tests__/FontSelect-test.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin http://sapegin.me, 2015 2 | 3 | jest.dontMock('../FontSelect'); 4 | jest.dontMock('../Select'); 5 | let FontSelect = require('../FontSelect'); 6 | 7 | describe('components/FontSelect', () => { 8 | 9 | let render = () => { 10 | return TestUtils.renderIntoDocument( 11 | 12 | ); 13 | }; 14 | 15 | it('should be component of type FontSelect', () => { 16 | let element = render(); 17 | let isCompositeComponentOfType = TestUtils.isCompositeComponentWithType(element, FontSelect); 18 | expect(isCompositeComponentOfType).toBe(true); 19 | }); 20 | 21 | it('getValue() should return default value', () => { 22 | let element = render(); 23 | let value = element.getValue(); 24 | expect(value).toEqual('Courier New Regular'); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /app/components/__tests__/PixelField-test.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin http://sapegin.me, 2015 2 | 3 | jest.dontMock('../PixelField'); 4 | jest.dontMock('../FieldWithUnit'); 5 | let PixelField = require('../PixelField'); 6 | 7 | describe('components/PixelField', () => { 8 | 9 | const defaultProps = { 10 | id: 'pony', 11 | value: '20' 12 | }; 13 | 14 | let render = (props=defaultProps) => { 15 | return TestUtils.renderIntoDocument( 16 | 17 | ); 18 | }; 19 | 20 | it('should be component of type PixelField', () => { 21 | let element = render(); 22 | let isCompositeComponentOfType = TestUtils.isCompositeComponentWithType(element, PixelField); 23 | expect(isCompositeComponentOfType).toBe(true); 24 | }); 25 | 26 | it('getValue() should return default value', () => { 27 | let element = render(); 28 | let value = element.getValue(); 29 | expect(value).toEqual(defaultProps.value); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /app/components/__tests__/Select-test.js: -------------------------------------------------------------------------------- 1 | // Author: Artem Sapegin http://sapegin.me, 2015 2 | 3 | jest.dontMock('../Select'); 4 | let Select = require('../Select'); 5 | 6 | describe('components/Select', () => { 7 | 8 | const defaultProps = { 9 | items: { 10 | 1: 'one', 11 | 2: 'two', 12 | 3: 'three' 13 | }, 14 | value: '1' 15 | }; 16 | 17 | let render = (props=defaultProps) => { 18 | return TestUtils.renderIntoDocument( 19 | 12 | 16 | 17 | 18 | Radio button: 19 | 20 |
21 | 22 | 26 |
27 | 28 | 29 | ## Skin 30 | 31 | Set `checkbox_default_skin` or `modules_default_skin` to `true` to enable default skin. 32 | -------------------------------------------------------------------------------- /tamia/modules/checkbox/check.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tamia/modules/checkbox/example.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 |
8 |
9 | 10 | 14 |
15 |
16 | 17 | 21 |
22 | 23 |
24 | 25 | 29 |
30 |
31 | 32 | 36 |
37 |
38 | 39 | 43 |
44 | -------------------------------------------------------------------------------- /tamia/modules/checkbox/index.styl: -------------------------------------------------------------------------------- 1 | // Tâmia © 2013 Artem Sapegin http://sapegin.me 2 | // Checkbox and radio button 3 | // Requires Modernizr.svg 4 | // Dependencies: form 5 | 6 | // Bones 7 | 8 | .checkbox, 9 | .radio 10 | white-space: nowrap 11 | 12 | &__button 13 | // For prehistoric browsers 14 | display: none 15 | 16 | &__text 17 | display: inline-block 18 | white-space: normal 19 | 20 | &__button, 21 | &__text 22 | vertical-align: middle 23 | 24 | 25 | &__input:checked, 26 | &__input:not(:checked) 27 | // Input should be hidden but focusable 28 | // display:none and width=height=0 don’t work here 29 | position: absolute 30 | opacity: 0 31 | overflow: hidden 32 | height: 1px 33 | width: 1px 34 | padding: 0 35 | border: 0 36 | 37 | &__input:checked + &__label &__button, 38 | &__input:not(:checked) + &__label &__button 39 | display: inline-block 40 | 41 | &__input:checked + &__label &__button 42 | position: relative 43 | &:before 44 | content: "" 45 | position: absolute 46 | line-height: 1 47 | 48 | &.is-disabled &__input + &__label 49 | opacity: .4 50 | 51 | 52 | // Default skin 53 | 54 | modules_default_skin ?= true 55 | checkbox_default_skin ?= false 56 | 57 | if modules_default_skin or checkbox_default_skin 58 | 59 | .checkbox, 60 | .radio 61 | &__button, 62 | &__text 63 | line-height: 1.8 64 | font-size: 1em 65 | 66 | &__input:checked + &__label &__button, 67 | &__input:not(:checked) + &__label &__button 68 | width: size = 1em 69 | height: size 70 | margin-top: -.15em 71 | margin-right: .15em 72 | border: 1px solid #bbb 73 | border-radius: form_border_radius 74 | box-shadow: inset 0 .1em .2em black(.1) 75 | 76 | &__input:checked + &__label &__button 77 | &:before 78 | background-size: 100% 100% 79 | background-repeat: no-repeat 80 | 81 | &__input:enabled:active + &__label &__button, 82 | &__input:focus + &__label &__button 83 | background: #f4f4f4 84 | 85 | .checkbox 86 | &__input:checked + &__label &__button 87 | &:before 88 | center(.8em, .65em) 89 | background-image: embedurl("check.svg") 90 | .no-svg &:before 91 | margin-top:-.5em; 92 | font-size: 0.8em 93 | font-weight:bold; 94 | color: #444 95 | content: "✓" 96 | 97 | .radio 98 | &__input:checked + &__label &__button, 99 | &__input:not(:checked) + &__label &__button 100 | border-radius: 50% 101 | 102 | &__input:checked + &__label &__button 103 | &:before 104 | center(.5em) 105 | background-image: embedurl("radio.svg") 106 | .no-svg &:before 107 | background: #444 108 | border-radius: 50% 109 | -------------------------------------------------------------------------------- /tamia/modules/checkbox/radio.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tamia/modules/code/Readme.md: -------------------------------------------------------------------------------- 1 | # Code blocks 2 | 3 | Nice code examples with highlighting. 4 | 5 | 6 | ## Markup 7 | 8 |
alert 'Hello world!'
9 | 10 |
11 |
alert 'Hello world!'
12 |
13 | 14 | 15 | ## Skin 16 | 17 | Set `code_default_skin` or `modules_default_skin` to `true` to enable default skin. 18 | 19 | 20 | ## Configuration 21 | 22 | ### code_class_prefix 23 | 24 | Type: String. Default: `hljs-`. 25 | 26 | Prefix for color scheme classes. 27 | 28 | 29 | ## Tools 30 | 31 | * [highlight.js](http://softwaremaniacs.org/soft/highlight/en/). 32 | * [docpad-plugin-highlightjs](https://github.com/docpad/docpad-plugin-highlightjs) for DocPad. 33 | -------------------------------------------------------------------------------- /tamia/modules/code/example.html: -------------------------------------------------------------------------------- 1 |
alert('Hello world!');
2 | -------------------------------------------------------------------------------- /tamia/modules/code/index.styl: -------------------------------------------------------------------------------- 1 | // Tâmia © 2015 Artem Sapegin http://sapegin.me 2 | // Code blocks 3 | 4 | modules_default_skin ?= true 5 | code_default_skin ?= false 6 | code_class_prefix ?= 'hljs-' 7 | 8 | .code, 9 | .text code, 10 | .text kbd, 11 | .text pre, 12 | .text pre * 13 | font-family: Consolas, "Lucida Console", Monaco, "DejaVu Sans Mono", monospace 14 | 15 | .code 16 | display: block 17 | white-space: pre-wrap 18 | -moz-tab-size: 4 19 | -o-tab-size: 4 20 | tab-size: 4 21 | -webkit-text-size-adjust: none 22 | 23 | code 24 | display: block 25 | font-size: 14px 26 | line-height: 1.3 27 | 28 | .indent 29 | display: inline-block 30 | width: 2.2em 31 | 32 | if modules_default_skin or code_default_skin 33 | 34 | @import 'tomorrow' 35 | 36 | code 37 | padding: spacer 38 | background: #fff 39 | background: white(.7) 40 | border-radius: 3px 41 | border: 1px solid #ccc 42 | border-color: black(.2) 43 | 44 | @media print 45 | border: 0 46 | 47 | // Enable code in .text blocks 48 | .text pre 49 | @extend .code 50 | -------------------------------------------------------------------------------- /tamia/modules/code/tomorrow.styl: -------------------------------------------------------------------------------- 1 | // Tâmia © 2015 Artem Sapegin http://sapegin.me 2 | // Tomorrow theme 3 | // Theme from: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow.css 4 | 5 | _prefix = code_class_prefix 6 | 7 | // Comment 8 | .{_prefix}comment 9 | color: #8e908c 10 | @media print 11 | font-style: italic 12 | 13 | // Red 14 | .{_prefix}variable, 15 | .{_prefix}attribute, 16 | .{_prefix}tag, 17 | .{_prefix}regexp, 18 | .ruby .{_prefix}constant, 19 | .xml .{_prefix}tag .{_prefix}title, 20 | .xml .{_prefix}pi, 21 | .xml .{_prefix}doctype, 22 | .html .{_prefix}doctype, 23 | .css .{_prefix}id, 24 | .css .{_prefix}class, 25 | .css .{_prefix}pseudo 26 | color: #c82829 27 | @media print 28 | font-weight: bold 29 | 30 | // Orange 31 | .{_prefix}number, 32 | .{_prefix}preprocessor, 33 | .{_prefix}pragma, 34 | .{_prefix}built_in, 35 | .{_prefix}literal, 36 | .{_prefix}params, 37 | .{_prefix}constant 38 | color: #f5871f 39 | 40 | // Yellow 41 | .ruby .{_prefix}class .{_prefix}title, 42 | .css .{_prefix}rules .{_prefix}attribute 43 | color: #eab700 44 | 45 | // Green 46 | .{_prefix}string, 47 | .{_prefix}value, 48 | .{_prefix}inheritance, 49 | .{_prefix}header, 50 | .ruby .{_prefix}symbol, 51 | .xml .{_prefix}cdata 52 | color: #718c00 53 | 54 | // Aqua 55 | .{_prefix}title, 56 | .css .{_prefix}hexcolor 57 | color: #3e999f 58 | 59 | // Blue 60 | .{_prefix}function, 61 | .python .{_prefix}decorator, 62 | .python .{_prefix}title, 63 | .ruby .{_prefix}function .{_prefix}title, 64 | .ruby .{_prefix}title .{_prefix}keyword, 65 | .perl .{_prefix}sub, 66 | .javascript .{_prefix}title, 67 | .coffeescript .{_prefix}title 68 | color: #4271ae 69 | 70 | // Purple 71 | .{_prefix}keyword, 72 | .javascript .{_prefix}function 73 | color: #8959a8 74 | 75 | .coffeescript .javascript, 76 | .javascript .xml, 77 | .tex .hljs-formula, 78 | .xml .javascript, 79 | .xml .vbscript, 80 | .xml .css, 81 | .xml .hljs-cdata 82 | opacity: 0.5 83 | -------------------------------------------------------------------------------- /tamia/modules/fade/Readme.md: -------------------------------------------------------------------------------- 1 | # Fade 2 | 3 | Fading text. 4 | 5 | 6 | ## Markup 7 | 8 |
Very long text
9 | 10 | 11 | ## Caveats 12 | 13 | Works best in Webkits (via `mask-image`). Works acceptable in Firefox (via SVG mask): fade width is fixed. Fades with ellipsis in any IE. 14 | 15 | -------------------------------------------------------------------------------- /tamia/modules/fade/example.html: -------------------------------------------------------------------------------- 1 |

2 | -------------------------------------------------------------------------------- /tamia/modules/fade/index.styl: -------------------------------------------------------------------------------- 1 | // Tâmia © 2014 Artem Sapegin http://sapegin.me 2 | // Fading text 3 | // Based on http://mir.aculo.us/2012/09/16/masking-html-elements-with-gradient-based-fadeouts/ + thanks to @kizu 4 | 5 | .fade 6 | display: block 7 | overflow: hidden 8 | white-space: nowrap 9 | mask: embedurl("mask.svg#fade-mask") // Firefox 10 | -webkit-mask: -webkit-linear-gradient(right, transparent 0, #000 3em) // Webkit 11 | -ms-text-overflow: ellipsis // IE 12 | -------------------------------------------------------------------------------- /tamia/modules/fade/mask.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tamia/modules/fade/mask_src.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tamia/modules/flippable/Readme.md: -------------------------------------------------------------------------------- 1 | # Flippable pane 2 | 3 | Vertical or horizontal flippable pane. With 3D animation. 4 | 5 | 6 | # Markup 7 | 8 |
9 |
Front
10 |
Back
11 |
12 | 13 | 14 | ## Modifiers 15 | 16 | ### .flippable.flippable_vertical 17 | 18 | Vertical rotation (horizontal by default). 19 | 20 | 21 | ## States 22 | 23 | ### .flippable.is-flipped 24 | 25 | Back side is visible. 26 | 27 | 28 | ## Events 29 | 30 | ### flip.tamia 31 | 32 | Flips pane. 33 | 34 | ### flipped.tamia 35 | 36 | Fires on every flip. Argument will be `true` if back side is visible. 37 | 38 | 39 | ## JS Hooks 40 | 41 | ### .js-flip 42 | 43 | Element that flips pane when clicked. 44 | -------------------------------------------------------------------------------- /tamia/modules/flippable/example.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | -------------------------------------------------------------------------------- /tamia/modules/flippable/index.styl: -------------------------------------------------------------------------------- 1 | // Tâmia © 2013 Artem Sapegin http://sapegin.me 2 | // Flippable pane 3 | 4 | .flippable 5 | no-select() 6 | position: relative 7 | perspective: 600px 8 | cursor: default 9 | 10 | &__front, 11 | &__back 12 | position: absolute 13 | top: 0 14 | left: 0 15 | width: inherit 16 | height: inherit 17 | transform-style: preserve-3d 18 | backface-visibility: hidden 19 | transition: all .4s ease-out-back 20 | &__front 21 | z-index: 900 22 | transform: rotateY(0deg) rotateX(0deg) 23 | &__back 24 | z-index:800; 25 | transform: rotateY(-180deg) 26 | &_vertical &__back 27 | transform: rotateX(-180deg) 28 | 29 | &.is-flipped &__front 30 | transform: rotateY(180deg) 31 | &_vertical.is-flipped &__front 32 | transform: rotateX(180deg) 33 | 34 | .is-flipped &__back 35 | transform: rotateY(0deg) rotateX(0deg) 36 | 37 | -------------------------------------------------------------------------------- /tamia/modules/flippable/script.js: -------------------------------------------------------------------------------- 1 | // Tâmia © 2014 Artem Sapegin http://sapegin.me 2 | // Flippable pane 3 | 4 | /*global tamia:false*/ 5 | ;(function(window, $, undefined) { 6 | 'use strict'; 7 | 8 | tamia.Flippable = tamia.extend(tamia.Component, { 9 | displayName: 'tamia.Flippable', 10 | binded: 'toggle', 11 | 12 | init: function() { 13 | if (this.elem.hasClass('js-flip')) { 14 | this.elem.on('click', this.toggle_); 15 | } 16 | else { 17 | this.elem.on('click', '.js-flip', this.toggle_); 18 | } 19 | 20 | this.elem.on('flip.tamia', this.toggle_); 21 | }, 22 | 23 | toggle: function() { 24 | this.elem.toggleState('flipped'); 25 | this.elem.trigger('flipped.tamia', this.elem.hasState('flipped')); 26 | } 27 | }); 28 | 29 | tamia.initComponents({flippable: tamia.Flippable}); 30 | 31 | }(window, jQuery)); 32 | -------------------------------------------------------------------------------- /tamia/modules/form/Readme.md: -------------------------------------------------------------------------------- 1 | # Form 2 | 3 | Basic form controls: inputs, textareas, buttons, form layouts and helpers. 4 | 5 | 6 | ## Markup 7 | 8 | Controls: 9 | 10 | 11 | 12 | 13 | 14 | Block controls: 15 | 16 | 17 | 18 | 19 | Field with unit: 20 | 21 |
22 | 23 | 24 |
25 | 26 | Layout: 27 | 28 |
29 |
30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 | 44 | Messages: 45 | 46 |
Thank you!
47 |
Are you sure?
48 |
Not enough cheese
49 | 50 | 51 | ## States 52 | 53 | ### .is-disabled 54 | 55 | Disabled state of a control. Should be combined with `disabled` attribute where appropriate. 56 | 57 | ### .is-success / .is-error 58 | 59 | Shows/hides success/error messages: 60 | 61 |
62 |
Thank you!
63 |
Go away!
64 |
65 | 66 | 67 | ## Events 68 | 69 | ### enable.form.tamia / disable.form.tamia 70 | 71 | Enables / disables all descendant form elements (they should have classes `.field`, `.button` or `.disablable`). 72 | 73 | ### lock.form.tamia / unlock.form.tamia 74 | 75 | Disables / enables submit button of a form. 76 | 77 | 78 | ## Ajax forms 79 | 80 | You can convert any form to Ajax form: 81 | 82 |
83 | ... 84 |
Thank you!
85 |
Go away!
86 |
87 | 88 | You can subscribe to events `send.form.tamia`, `success.form.tamia` and `success.form.tamia`: 89 | 90 | form.on({ 91 | 'send.form.tamia': function(event, fields) { 92 | event.preventDefault(); // Cancel form sending 93 | }, 94 | 'success.form.tamia': function(event, data) { 95 | // Server should return data.result = 'success' 96 | return 'Error message.'; 97 | }, 98 | 'error.form.tamia': function(event, data) { 99 | return 'Success message.'; 100 | } 101 | }); 102 | 103 | Or use states `.is-success`, `.is-error` and `.is-sending` to check form status. 104 | 105 | 106 | ## Auto disable submit button 107 | 108 | Disable submit button on form submit: 109 | 110 |
111 | 112 | 113 | ## Skin 114 | 115 | Set `form_default_skin` or `modules_default_skin` to `true` to enable default skin. 116 | 117 | 118 | ## Configuration 119 | 120 | ### form_focus_color 121 | 122 | Type: CSS color value. 123 | 124 | Color of focus outline. 125 | -------------------------------------------------------------------------------- /tamia/modules/form/example.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |
9 | 10 |
11 | 12 | 13 |
14 | 15 |
Thank you!
16 |
Are you sure?
17 |
Not enough cheese
18 | 19 |
20 | 21 | 22 |
23 | -------------------------------------------------------------------------------- /tamia/modules/form/index.styl: -------------------------------------------------------------------------------- 1 | // Tâmia © 2013 Artem Sapegin http://sapegin.me 2 | // Basic form controls: inputs, textareas, buttons 3 | 4 | // Disabled form element 5 | .is-disabled 6 | cursor: default 7 | pointer-events: none 8 | text-shadow: none 9 | 10 | 11 | // Bones 12 | 13 | .field, 14 | .button 15 | display: inline-block 16 | vertical-align: middle 17 | font-size: 1em 18 | line-height: 1 19 | outline: 0 20 | transition: opacity .25 ease-out 21 | 22 | &_block 23 | display: block 24 | width: 100% 25 | 26 | &.is-disabled 27 | opacity: .4 28 | 29 | .field 30 | // Hide IE10 clear button 31 | &::-ms-clear 32 | size: 0 // Not display:none because: http://bit.ly/1h3UlAH 33 | 34 | .field_area 35 | resize: vertical // Vertical resizing for textareas 36 | 37 | .button 38 | no-select() 39 | position: relative // Fixes strange bugs in webkit 40 | text-decoration: none 41 | white-space: nowrap 42 | cursor: pointer 43 | 44 | // Fixing Mozilla's inner paddings 45 | // https://github.com/nanoblocks/nanoblocks/blob/gh-pages/blocks/button/button.styl 46 | &::-moz-focus-inner 47 | padding: 0 48 | border: none 49 | 50 | .button + .button, 51 | .field + .field, 52 | .field + .button, 53 | .button + .field 54 | margin-left: spacer 55 | 56 | .form 57 | &_block .field, 58 | &_block .button, 59 | &_block .select, // TODO: Move to select 60 | &_block .password // TODO: Move to password 61 | display: block 62 | width: 100% 63 | 64 | &__group 65 | space(2) 66 | 67 | &__row, 68 | &__row.is-transit 69 | display: flex 70 | space() 71 | &:last-child 72 | space(0) 73 | 74 | &__label 75 | width: 20% 76 | min-width: 6em 77 | max-width: 20em 78 | padding-right: spacer 79 | padding-top: .2em 80 | text-align: right 81 | 82 | &__widget 83 | flex: 1 84 | padding-left: spacer 85 | text-align: left 86 | 87 | .form__success, 88 | .form__error 89 | display: none 90 | &.is-success .form__success 91 | display: block 92 | &.is-error .form__error 93 | display: block 94 | 95 | .field-with-unit 96 | position: relative 97 | font-size: 1em 98 | line-height: 1.4 99 | 100 | &__field 101 | width: 100% 102 | 103 | &__unit 104 | position: absolute 105 | top: 0 106 | right: 0 107 | 108 | &_left &__unit 109 | right: auto 110 | left: 0 111 | 112 | 113 | // Close button 114 | .close 115 | line-height: 1 116 | outline: 0 117 | 118 | // Fix 12 | 13 | 16 |
17 |
Cancel
18 |
Save
19 |
20 | 21 | 22 | To disable modal closing by click on a shade use `data-modal-close-on-shade="no"` attribute: 23 | 24 |