├── .eslintignore
├── src
├── demo
│ ├── lib
│ │ ├── bootstrap
│ │ │ ├── index.js
│ │ │ └── bootstrap-flex.min.css.map
│ │ └── prism
│ │ │ ├── index.js
│ │ │ ├── prism.css
│ │ │ └── prism.js
│ ├── assets
│ │ ├── demo.gif
│ │ └── images
│ │ │ ├── download-light.png
│ │ │ ├── github-mark-light.png
│ │ │ └── documentation-light.png
│ ├── styles
│ │ ├── colors.scss
│ │ └── demo.scss
│ ├── index.js
│ ├── components
│ │ ├── CopyrightFooter.vue
│ │ ├── FormInput.vue
│ │ ├── BadgeBar.vue
│ │ ├── FormCheck.vue
│ │ ├── LinkBar.vue
│ │ ├── FormRadio.vue
│ │ ├── CircleLink.vue
│ │ ├── CodeBlock.vue
│ │ ├── HeroHeader.vue
│ │ └── AppLayout.vue
│ └── Demo.vue
└── vue-typer
│ ├── index.js
│ ├── styles
│ ├── typer-colors.scss
│ └── caret-animations.scss
│ ├── utils
│ ├── shallow-equals.js
│ ├── shuffle.js
│ └── random-int.js
│ └── components
│ ├── Char.vue
│ ├── Caret.vue
│ └── VueTyper.vue
├── .gitignore
├── .babelrc
├── test
└── unit
│ ├── .eslintrc
│ ├── utils
│ ├── mount.js
│ └── wait.js
│ ├── index.js
│ ├── karma.conf.js
│ └── specs
│ └── VueTyper.spec.js
├── .eslintrc.js
├── index.html
├── LICENSE
├── RELEASE.md
├── dist
├── manifest.e0fcc538cee16be5b5d1.min.js
├── demo.1320e4785106378b9e121f0ec76a34aa.min.css
├── vue-typer.min.js
└── demo.ecd3fe850e3573d9d50a.min.js
├── package.json
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/demo/lib
2 |
--------------------------------------------------------------------------------
/src/demo/lib/bootstrap/index.js:
--------------------------------------------------------------------------------
1 | import './bootstrap-flex.min.css'
2 |
--------------------------------------------------------------------------------
/src/demo/assets/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cngu/vue-typer/HEAD/src/demo/assets/demo.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | node_modules
4 | *.log
5 | dist/*.map
6 | test/**/coverage
7 |
--------------------------------------------------------------------------------
/src/demo/styles/colors.scss:
--------------------------------------------------------------------------------
1 | $vue-blue: #35495E;
2 | $vue-green: #41B883;
3 | $body-background: #eee;
--------------------------------------------------------------------------------
/src/demo/lib/prism/index.js:
--------------------------------------------------------------------------------
1 | import './prism.css'
2 | import Prism from './prism'
3 |
4 | export default Prism
5 |
--------------------------------------------------------------------------------
/src/demo/assets/images/download-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cngu/vue-typer/HEAD/src/demo/assets/images/download-light.png
--------------------------------------------------------------------------------
/src/demo/assets/images/github-mark-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cngu/vue-typer/HEAD/src/demo/assets/images/github-mark-light.png
--------------------------------------------------------------------------------
/src/demo/assets/images/documentation-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cngu/vue-typer/HEAD/src/demo/assets/images/documentation-light.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": ["transform-runtime"],
4 | "comments": false,
5 | "env": {
6 | "test": {
7 | "plugins": ["istanbul"]
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "describe": true,
7 | "it": true,
8 | "expect": true,
9 | "sinon": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/test/unit/utils/mount.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | export default function mount(componentOptions, propsData) {
4 | const Ctor = Vue.extend(componentOptions)
5 | const vm = new Ctor({ propsData })
6 | return vm.$mount()
7 | }
8 |
--------------------------------------------------------------------------------
/test/unit/utils/wait.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | export function waitUntilRendered(next) {
4 | Vue.nextTick(next)
5 | }
6 |
7 | export function wait(clock, ms, next) {
8 | clock.tick(ms)
9 | waitUntilRendered(next)
10 | }
11 |
--------------------------------------------------------------------------------
/src/demo/styles/demo.scss:
--------------------------------------------------------------------------------
1 | @import 'colors';
2 |
3 | html {
4 | height: 100%;
5 | -webkit-font-smoothing: antialiased;
6 |
7 | body {
8 | height: 100%;
9 | margin: 0;
10 | background: $body-background;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/demo/index.js:
--------------------------------------------------------------------------------
1 | import './styles/demo.scss'
2 |
3 | import Vue from 'vue'
4 | import Demo from './Demo'
5 |
6 | // eslint-disable-next-line no-new
7 | new Vue({
8 | el: '#demo',
9 | render: createElement => createElement(Demo)
10 | })
11 |
--------------------------------------------------------------------------------
/src/vue-typer/index.js:
--------------------------------------------------------------------------------
1 | import VueTyperComponent from './components/VueTyper'
2 |
3 | export const VueTyper = VueTyperComponent
4 |
5 | export default {
6 | install(Vue) {
7 | Vue.component('vue-typer', VueTyperComponent)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/vue-typer/styles/typer-colors.scss:
--------------------------------------------------------------------------------
1 | $char-typed-color: black;
2 | $char-selected-color: black;
3 |
4 | $char-typed-background-color: transparent;
5 | $char-selected-background-color: #ACCEF7;
6 |
7 | $caret-idle-color: black;
8 | $caret-typing-color: black;
9 | $caret-selecting-color: black;
10 | $caret-erasing-color: black;
11 | $caret-complete-color: black;
12 |
--------------------------------------------------------------------------------
/src/vue-typer/utils/shallow-equals.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Performs a shallow comparison between 2 arrays.
3 | * @param {Array} a1
4 | * @param {Array} a2
5 | * @returns true if the array contents are strictly equal (===); false otherwise
6 | */
7 | export default (a1, a2) => {
8 | if (!Array.isArray(a1) || !Array.isArray(a2)) {
9 | return false
10 | }
11 |
12 | if (a1.length !== a2.length) {
13 | return false
14 | }
15 |
16 | for (let i = 0; i < a1.length; i++) {
17 | if (a1[i] !== a2[i]) {
18 | return false
19 | }
20 | }
21 |
22 | return true
23 | }
24 |
--------------------------------------------------------------------------------
/src/demo/components/CopyrightFooter.vue:
--------------------------------------------------------------------------------
1 |
2 | footer
3 | small
4 | | Released under the #[a(href='https://opensource.org/licenses/MIT', target='_blank') MIT License]
5 | br
6 | | Copyright © 2016-#{new Date().getFullYear()} Chris Nguyen
7 |
8 |
9 |
25 |
--------------------------------------------------------------------------------
/test/unit/index.js:
--------------------------------------------------------------------------------
1 | // Polyfill fn.bind() for PhantomJS
2 | /* eslint-disable no-extend-native */
3 | Function.prototype.bind = require('function-bind')
4 |
5 | // require all test files (files that ends with .spec.js)
6 | const testsContext = require.context('./specs', true, /\.spec$/)
7 | testsContext.keys().forEach(testsContext)
8 |
9 | // require all src files except index.js for coverage.
10 | // you can also change this to match only the subset of files that
11 | // you want coverage for.
12 | const srcContext = require.context('../../src/vue-typer', true, /^\.\/(?!index(\.js)?$)/)
13 | srcContext.keys().forEach(srcContext)
14 |
--------------------------------------------------------------------------------
/src/vue-typer/utils/shuffle.js:
--------------------------------------------------------------------------------
1 | import randomInt from './random-int'
2 |
3 | function swap(a, i, j) {
4 | if (i === j) {
5 | return
6 | }
7 | const temp = a[i]
8 | a[i] = a[j]
9 | a[j] = temp
10 | }
11 |
12 | /**
13 | * Performs an in-place shuffle.
14 | * Implemented using the Fisher-Yates/Knuth shuffle algorithm.
15 | * @param list - Array of items to shuffle in-place.
16 | */
17 | export default (list) => {
18 | if (!(list instanceof Array)) {
19 | return
20 | }
21 |
22 | for (let i = list.length - 1; i > 0; i--) {
23 | let randomIndex = randomInt(0, i)
24 | swap(list, i, randomIndex)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/demo/components/FormInput.vue:
--------------------------------------------------------------------------------
1 |
2 | .form-group.row.flex-items-xs-center
3 | label.col-form-label.col-xs-4.col-md-3.col-lg-6(:for='label') {{label}}
4 | .col-xs-4.col-md-3.col-lg-6
5 | input.form-control(
6 | :id='label',
7 | :type='type',
8 | :value='value',
9 | @input='$emit("input", $event.target.value)'
10 | )
11 |
12 |
13 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/src/demo/components/BadgeBar.vue:
--------------------------------------------------------------------------------
1 |
2 | span.badge-bar
3 | a(href='https://www.npmjs.com/package/vue-typer', target='_blank')
4 | img(src='https://img.shields.io/npm/dt/vue-typer.svg', alt='Downloads')
5 | a(href='https://www.npmjs.com/package/vue-typer', target='_blank')
6 | img(src='https://img.shields.io/npm/v/vue-typer.svg', alt='Version')
7 | a(href='https://www.npmjs.com/package/vue-typer', target='_blank')
8 | img(src='https://img.shields.io/npm/l/vue-typer.svg', alt='License')
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: 'babel-eslint',
4 | parserOptions: {
5 | sourceType: 'module'
6 | },
7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
8 | extends: 'standard',
9 | plugins: [
10 | 'html' // required to lint *.vue files
11 | ],
12 | // add your custom rules here
13 | 'rules': {
14 | // allow no-spaces between function name and argument parethesis list
15 | 'space-before-function-paren': 0,
16 | // allow paren-less arrow functions
17 | 'arrow-parens': 0,
18 | // allow debugger during development
19 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/demo/components/FormCheck.vue:
--------------------------------------------------------------------------------
1 |
2 | .form-group.row.flex-items-xs-center
3 | label.col-xs-4.col-md-3.col-lg-6.flex-xs-middle.shift-up(:for='label') {{label}}
4 | .col-xs-4.col-md-3.col-lg-6
5 | .form-check
6 | label.form-check-label
7 | input.form-check-input(
8 | :id='label',
9 | type='checkbox',
10 | :value='value',
11 | @change='$emit("input", $event.target.checked)'
12 | )
13 |
14 |
15 |
23 |
24 |
29 |
--------------------------------------------------------------------------------
/src/demo/components/LinkBar.vue:
--------------------------------------------------------------------------------
1 |
2 | .link-bar
3 | circle-link(href='https://github.com/cngu/vue-typer')
4 | img(src='../assets/images/github-mark-light.png')
5 | circle-link(href='https://github.com/cngu/vue-typer/blob/master/README.md#getting-started')
6 | img(src='../assets/images/documentation-light.png')
7 | circle-link(href='https://api.github.com/repos/cngu/vue-typer/zipball')
8 | img(src='../assets/images/download-light.png')
9 |
10 |
11 |
20 |
21 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | VueTyper Demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/demo/components/FormRadio.vue:
--------------------------------------------------------------------------------
1 |
2 | .form-group.row.flex-items-xs-center
3 | label.col-xs-4.col-md-3.col-lg-6 {{label}}
4 | .col-xs-4.col-md-3.col-lg-6
5 | .form-check(v-for='option of options')
6 | label.form-check-label
7 | input.form-check-input(
8 | type='radio',
9 | :value='option',
10 | @change='$emit("input", $event.target.value)',
11 | v-model='localModel'
12 | )
13 | | {{" " + option}}
14 |
15 |
16 |
31 |
32 |
37 |
--------------------------------------------------------------------------------
/src/vue-typer/utils/random-int.js:
--------------------------------------------------------------------------------
1 | function validNumber(val) {
2 | return typeof val === 'number' && !Number.isNaN(val) && Number.isFinite(val)
3 | }
4 |
5 | function validRange(lower, upper) {
6 | return validNumber(lower) && validNumber(upper) && lower <= upper
7 | }
8 |
9 | /**
10 | * @param min - Minimum random int
11 | * @param max - Maximum random int
12 | * @returns a random int in the range [min, max], or -1 if either of the following conditions are met:
13 | * - min and/or max are not of type 'number', NaN, or Infinity
14 | * - min > max
15 | */
16 | export default (min, max) => {
17 | if (!validRange(min, max)) {
18 | return -1
19 | }
20 |
21 | // Since we're generating random integers, rounded the arguments to the closest int within the range
22 | min = Math.ceil(min)
23 | max = Math.floor(max)
24 |
25 | return Math.floor(Math.random() * (max - min + 1)) + min
26 | }
27 |
--------------------------------------------------------------------------------
/src/demo/components/CircleLink.vue:
--------------------------------------------------------------------------------
1 |
2 | a(:href='href', target='_blank')
3 | slot
4 |
5 |
6 |
13 |
14 |
49 |
--------------------------------------------------------------------------------
/src/demo/components/CodeBlock.vue:
--------------------------------------------------------------------------------
1 |
2 | pre.code-block
3 | code(ref='codeBlock', :class='languageClass')
4 |
5 |
6 |
44 |
45 |
--------------------------------------------------------------------------------
/src/vue-typer/styles/caret-animations.scss:
--------------------------------------------------------------------------------
1 | @keyframes vue-typer-caret-blink {
2 | 50% {
3 | opacity: 0;
4 | }
5 | 100% {
6 | opacity: 1;
7 | }
8 | }
9 |
10 | @keyframes vue-typer-caret-smooth {
11 | 0%, 20% {
12 | opacity: 1;
13 | }
14 | 60%, 100% {
15 | opacity: 0;
16 | }
17 | }
18 |
19 | @keyframes vue-typer-caret-phase {
20 | 0%, 20% {
21 | opacity: 1;
22 | }
23 | 90%, 100% {
24 | opacity: 0;
25 | }
26 | }
27 |
28 | @keyframes vue-typer-caret-expand {
29 | 0%, 20% {
30 | transform: scaleY(1);
31 | }
32 | 80%, 100% {
33 | transform: scaleY(0);
34 | }
35 | }
36 |
37 | .vue-typer-caret-blink {
38 | animation: vue-typer-caret-blink 1s step-start 0s infinite;
39 | }
40 |
41 | .vue-typer-caret-smooth {
42 | animation: vue-typer-caret-smooth 0.5s ease-in-out 0s infinite alternate;
43 | }
44 |
45 | .vue-typer-caret-phase {
46 | animation: vue-typer-caret-phase 0.5s ease-in-out 0s infinite alternate;
47 | }
48 |
49 | .vue-typer-caret-expand {
50 | animation: vue-typer-caret-expand 0.5s ease-in-out 0s infinite alternate;
51 | }
52 |
--------------------------------------------------------------------------------
/test/unit/karma.conf.js:
--------------------------------------------------------------------------------
1 | // This is a karma config file. For more details see
2 | // http://karma-runner.github.io/0.13/config/configuration-file.html
3 | // we are also using it with karma-webpack
4 | // https://github.com/webpack/karma-webpack
5 |
6 | var webpackConfig = require('../../build/webpack.config.test')
7 |
8 | module.exports = function(config) {
9 | config.set({
10 | // to run in additional browsers:
11 | // 1. install corresponding karma launcher
12 | // http://karma-runner.github.io/0.13/config/browsers.html
13 | // 2. add it to the `browsers` array below.
14 | browsers: ['PhantomJS'],
15 | frameworks: ['mocha', 'sinon-chai'],
16 | reporters: ['spec', 'coverage'],
17 | files: ['./index.js'],
18 | preprocessors: {
19 | './index.js': ['webpack', 'sourcemap']
20 | },
21 | webpack: webpackConfig,
22 | webpackMiddleware: {
23 | noInfo: true
24 | },
25 | coverageReporter: {
26 | dir: './coverage',
27 | reporters: [
28 | { type: 'lcov', subdir: '.' },
29 | { type: 'text-summary' }
30 | ]
31 | }
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Chris Nguyen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/vue-typer/components/Char.vue:
--------------------------------------------------------------------------------
1 |
2 | span.char(:class="classes")
3 | | {{ val }}
4 |
5 |
6 |
23 |
24 |
53 |
--------------------------------------------------------------------------------
/src/demo/components/HeroHeader.vue:
--------------------------------------------------------------------------------
1 |
2 | header.jumbotron.jumbotron-fluid
3 | h1.display-4
4 | vue-typer.title-typer(text='VueTyper', :repeat='0', :pre-type-delay='1000', :type-delay='400', caret-animation='smooth')
5 | link-bar.link-bar
6 | badge-bar
7 |
8 |
9 |
20 |
21 |
43 |
44 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | 0. Ensure that `dist/` and `index.html` are not committed on `develop`.
2 | ```bash
3 | npm run clean
4 | ```
5 |
6 | 1. Update version in package.json on `develop`.
7 |
8 | 2. Create RC commit on `develop` with new version number:
9 | ```bash
10 | git add package.json
11 | git commit -m "RC MAJOR.MINOR.PATCH[ - optional message]"
12 | git push
13 | ```
14 |
15 | 3. Switch to `master`:
16 | ```bash
17 | git checkout master
18 | ```
19 |
20 | 4. Merge `develop` into `master`:
21 | ```bash
22 | git merge --no-commit develop
23 | ```
24 |
25 | 5. Test out the demo page locally:
26 | ```bash
27 | npm run dev
28 | ```
29 |
30 | 6. Build new package and demo page, and ensure all tests pass:
31 | ```bash
32 | npm run build
33 | ```
34 |
35 | 7. Ensure that the following pieces of information are still correct in the auto-generated comment at the top of vue-typer.js and vue-typer.min.js:
36 | - Package name
37 | - Version number
38 | - Copyright date
39 | - License
40 |
41 | 8. Ensure that the paths in `index.html` correctly point to the built files in `dist/`
42 |
43 | 9. Commit and tag with the same message:
44 | ```bash
45 | git add dist index.html
46 | git commit -m "[release] vMAJOR.MINOR.PATCH"
47 | git tag -a vMAJOR.MINOR.PATCH -m "[release] vMAJOR.MINOR.PATCH"
48 | ```
49 |
50 | 10. Publish to npm:
51 | ```bash
52 | npm publish
53 | ```
54 |
55 | 11. Push to github:
56 | ```bash
57 | git push --follow-tags
58 | ```
59 |
60 | 12. Add release notes on Github.
61 |
--------------------------------------------------------------------------------
/dist/manifest.e0fcc538cee16be5b5d1.min.js:
--------------------------------------------------------------------------------
1 | !function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,i){for(var u,a,f,l=0,s=[];l
2 | span.caret.custom(:class='animationClass')
3 |
4 |
5 |
26 |
27 |
65 |
--------------------------------------------------------------------------------
/src/demo/components/AppLayout.vue:
--------------------------------------------------------------------------------
1 |
2 | .app-layout
3 | slot(name='header')
4 |
5 | main.container
6 | section#playground.row
7 | h4.col-xs-12.text-xs-center VueTyper Playground
8 |
9 | #output-panel.card.col-xs-12.col-lg-6
10 | slot(name='main-playground-output')
11 |
12 | #text-panel.card.col-xs-12.col-lg-6
13 | slot(name='main-playground-text')
14 |
15 | #config-panel.card.col-xs-12.col-lg-6
16 | slot(name='main-playground-config')
17 |
18 | #code-panel.card.col-xs-12.col-lg-6
19 | slot(name='main-playground-code')
20 |
21 | section#style-showcase.row
22 | h4.col-xs-12.text-xs-center VueTyper is also stylable with CSS!
23 | .col-xs
24 | .row
25 | .card.col-xs-12.col-lg-4
26 | slot(name='style-showcase-panel-1')
27 | .card.col-xs-12.col-lg-4
28 | slot(name='style-showcase-panel-2')
29 | .card.col-xs-12.col-lg-4
30 | slot(name='style-showcase-panel-3')
31 |
32 | slot(name='footer')
33 |
34 |
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-typer",
3 | "version": "1.2.0",
4 | "description": "Vue component that simulates a user typing, selecting, and erasing text.",
5 | "main": "dist/vue-typer.min.js",
6 | "files": [
7 | "dist/vue-typer.js",
8 | "dist/vue-typer.min.js"
9 | ],
10 | "scripts": {
11 | "dev": "NODE_ENV='development' webpack-dev-server --config build/webpack.config.dev.js --hot --inline --open --compress",
12 | "dev:lan": "npm run dev -- --public $(ipconfig getifaddr en0):8080 --host 0.0.0.0",
13 | "demo": "webpack --config build/webpack.config.demo.js",
14 | "prod": "NODE_ENV='production' webpack --config build/webpack.config.prod.js",
15 | "build": "npm run lint && npm run test && npm run clean && npm run demo && npm run prod",
16 | "unit": "npm run unit:watch -- --single-run",
17 | "unit:watch": "NODE_ENV='test' karma start test/unit/karma.conf.js",
18 | "unit:debug": "npm run unit:watch -- --browsers Chrome",
19 | "test": "npm run unit",
20 | "lint": "NODE_ENV='production' eslint --format node_modules/eslint-friendly-formatter --ext .js,.vue src",
21 | "clean": "rimraf ./dist ./index.html"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/cngu/vue-typer.git"
26 | },
27 | "keywords": [
28 | "vue-typer",
29 | "vue",
30 | "vuejs",
31 | "vue.js",
32 | "component",
33 | "typer",
34 | "typewriter",
35 | "type",
36 | "auto"
37 | ],
38 | "author": "Chris Nguyen",
39 | "license": "MIT",
40 | "bugs": {
41 | "url": "https://github.com/cngu/vue-typer/issues"
42 | },
43 | "homepage": "https://github.com/cngu/vue-typer#readme",
44 | "dependencies": {
45 | "lodash.split": "^4.4.2"
46 | },
47 | "devDependencies": {
48 | "autoprefixer": "^6.5.4",
49 | "babel-core": "^6.21.0",
50 | "babel-eslint": "^7.1.1",
51 | "babel-loader": "^6.2.10",
52 | "babel-plugin-istanbul": "^4.1.1",
53 | "babel-plugin-transform-runtime": "^6.15.0",
54 | "babel-preset-es2015": "^6.18.0",
55 | "chai": "^3.5.0",
56 | "css-loader": "^0.26.1",
57 | "eslint": "^3.12.2",
58 | "eslint-config-standard": "^6.2.1",
59 | "eslint-friendly-formatter": "^2.0.6",
60 | "eslint-loader": "^1.6.1",
61 | "eslint-plugin-html": "^1.7.0",
62 | "eslint-plugin-promise": "^3.4.0",
63 | "eslint-plugin-standard": "^2.0.1",
64 | "extract-text-webpack-plugin": "^2.1.0",
65 | "file-loader": "^0.9.0",
66 | "html-webpack-plugin": "^2.24.1",
67 | "karma": "^1.3.0",
68 | "karma-chrome-launcher": "^2.0.0",
69 | "karma-coverage": "^1.1.1",
70 | "karma-mocha": "^1.3.0",
71 | "karma-phantomjs-launcher": "^1.0.2",
72 | "karma-sinon-chai": "^1.2.4",
73 | "karma-sourcemap-loader": "^0.3.7",
74 | "karma-spec-reporter": "0.0.26",
75 | "karma-webpack": "^2.0.3",
76 | "mocha": "^3.2.0",
77 | "node-sass": "^4.1.1",
78 | "phantomjs-prebuilt": "^2.1.14",
79 | "pug": "^2.0.0-beta6",
80 | "rimraf": "^2.5.4",
81 | "sass-loader": "^4.1.1",
82 | "sinon": "^2.1.0",
83 | "sinon-chai": "^2.8.0",
84 | "style-loader": "^0.13.1",
85 | "url-loader": "^0.5.7",
86 | "vue": "^2.1.8",
87 | "vue-loader": "^10.0.2",
88 | "vue-template-compiler": "^2.1.6",
89 | "webpack": "^2.4.1",
90 | "webpack-dev-server": "^2.4.4",
91 | "webpack-merge": "^1.1.2"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/dist/demo.1320e4785106378b9e121f0ec76a34aa.min.css:
--------------------------------------------------------------------------------
1 | html{height:100%;-webkit-font-smoothing:antialiased}html body{height:100%;margin:0;background:#eee}.demo #output-panel .demo-typer-container[data-v-6d53ee08]{height:100%}.demo #text-panel .form-group[data-v-6d53ee08]{margin-bottom:0}.demo #text-panel .form-group textarea[data-v-6d53ee08]{width:100%}.demo .shrink-text[data-v-6d53ee08]{font-size:.9rem}@keyframes rocking{0%,to{transform:rotate(-10deg)}50%{transform:rotate(10deg)}}main .state-typer{font-family:Arial,Helvetica Neue,Helvetica,sans-serif}main .state-typer .custom.char.typed{color:#009688}main .state-typer .custom.char.selected{color:#e91e63}main .state-typer .custom.caret{animation:rocking 1s ease-in-out 0s infinite}main .state-typer .custom.caret.typing{background-color:#009688}main .state-typer .custom.caret.selecting{display:inline-block;background-color:#e91e63}main .code-typer{font-family:monospace}main .code-typer .custom.char{color:#d4d4bd;background-color:#1e1e1e}main .code-typer .custom.char.selected{background-color:#264f78}main .code-typer .custom.caret{width:10px;background-color:#3f51b5}main .ghost-typer{font-family:Copperplate,Copperplate Gothic Light,fantasy}main .ghost-typer .custom.char.typed{color:#607d8b}main .ghost-typer .custom.char.selected{color:#607d8b;background-color:transparent;text-decoration:line-through}main .ghost-typer .custom.caret{display:none}span.vue-typer[data-v-c41bac74]{cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}span.vue-typer span.left[data-v-c41bac74],span.vue-typer span.right[data-v-c41bac74]{display:inline}@keyframes vue-typer-caret-blink{50%{opacity:0}to{opacity:1}}@keyframes vue-typer-caret-smooth{0%,20%{opacity:1}60%,to{opacity:0}}@keyframes vue-typer-caret-phase{0%,20%{opacity:1}90%,to{opacity:0}}@keyframes vue-typer-caret-expand{0%,20%{transform:scaleY(1)}80%,to{transform:scaleY(0)}}.vue-typer-caret-blink[data-v-a16e0f02]{animation:vue-typer-caret-blink 1s step-start 0s infinite}.vue-typer-caret-smooth[data-v-a16e0f02]{animation:vue-typer-caret-smooth .5s ease-in-out 0s infinite alternate}.vue-typer-caret-phase[data-v-a16e0f02]{animation:vue-typer-caret-phase .5s ease-in-out 0s infinite alternate}.vue-typer-caret-expand[data-v-a16e0f02]{animation:vue-typer-caret-expand .5s ease-in-out 0s infinite alternate}span.caret[data-v-a16e0f02]:empty:before{content:"\200B"}span[data-v-a16e0f02]{display:inline-block;width:1px}.idle[data-v-a16e0f02],.typing[data-v-a16e0f02]{background-color:#000}.selecting[data-v-a16e0f02]{display:none;background-color:#000}.erasing[data-v-a16e0f02]{background-color:#000}.complete[data-v-a16e0f02]{display:none;background-color:#000}.char[data-v-302772ec]{display:inline-block;white-space:pre-wrap}.newline[data-v-302772ec]{display:inline}.typed[data-v-302772ec]{color:#000;background-color:transparent}.selected[data-v-302772ec]{color:#000;background-color:#accef7}.erased[data-v-302772ec]{display:none}.app-layout main[data-v-322ce952]{margin-bottom:8rem}.app-layout main #playground[data-v-322ce952]{margin-bottom:4rem}.app-layout main h4[data-v-322ce952]{height:1.1em;margin-bottom:1em;color:#35495e}.app-layout main .card[data-v-322ce952]{padding:1em;margin-bottom:0}header[data-v-d376d9aa]{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;align-items:center;color:#fff;background:#35495e;padding:2rem 0}header h1[data-v-d376d9aa]{margin-bottom:2rem}header .link-bar[data-v-d376d9aa]{margin-bottom:1rem}header h1 .title-typer{font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-weight:300}header h1 .title-typer .custom.char{color:#fff}header h1 .title-typer .custom.caret{background-color:#fff}header h1 .title-typer .custom.caret.complete{display:inline-block}.link-bar img[data-v-9cbc8336]{width:50px;height:50px;padding:8px}a[data-v-4ed29124]{position:relative;display:inline-block;margin:15px 30px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border-radius:50%;transition:box-shadow .2s}a[data-v-4ed29124]:after{content:"";position:absolute;top:0;left:0;width:100%;height:100%;padding:0;border-radius:50%;box-shadow:0 0 0 2px #fff;transition:transform .2s,opacity .2s}a[data-v-4ed29124]:hover{box-shadow:0 0 0 4px #fff}a[data-v-4ed29124]:hover:after{transform:scale(.8);opacity:.5}.badge-bar a[data-v-6512f7f4]{margin:0 3px}footer[data-v-0582c86e]{padding:2rem;color:#fff;background:#41b883;text-align:center}footer a[data-v-0582c86e]{color:#fff;font-weight:700;text-decoration:none}.shift-up[data-v-8e4ab660]{margin-top:-2px}.col-form-label[data-v-06d6e692]{padding-top:.4rem}.col-lg-6[data-v-67442243]{padding-right:0}
2 | /*# sourceMappingURL=demo.1320e4785106378b9e121f0ec76a34aa.min.css.map*/
--------------------------------------------------------------------------------
/src/demo/lib/prism/prism.css:
--------------------------------------------------------------------------------
1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css&plugins=previewer-base+previewer-color+normalize-whitespace */
2 | /**
3 | * prism.js default theme for JavaScript, CSS and HTML
4 | * Based on dabblet (http://dabblet.com)
5 | * @author Lea Verou
6 | */
7 |
8 | code[class*="language-"],
9 | pre[class*="language-"] {
10 | color: black;
11 | background: none;
12 | text-shadow: 0 1px white;
13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
14 | text-align: left;
15 | white-space: pre;
16 | word-spacing: normal;
17 | word-break: normal;
18 | word-wrap: normal;
19 | line-height: 1.5;
20 |
21 | -moz-tab-size: 4;
22 | -o-tab-size: 4;
23 | tab-size: 4;
24 |
25 | -webkit-hyphens: none;
26 | -moz-hyphens: none;
27 | -ms-hyphens: none;
28 | hyphens: none;
29 | }
30 |
31 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
32 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
33 | text-shadow: none;
34 | background: #b3d4fc;
35 | }
36 |
37 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
38 | code[class*="language-"]::selection, code[class*="language-"] ::selection {
39 | text-shadow: none;
40 | background: #b3d4fc;
41 | }
42 |
43 | @media print {
44 | code[class*="language-"],
45 | pre[class*="language-"] {
46 | text-shadow: none;
47 | }
48 | }
49 |
50 | /* Code blocks */
51 | pre[class*="language-"] {
52 | padding: 1em;
53 | margin: .5em 0;
54 | overflow: auto;
55 | }
56 |
57 | :not(pre) > code[class*="language-"],
58 | pre[class*="language-"] {
59 | background: #f5f2f0;
60 | }
61 |
62 | /* Inline code */
63 | :not(pre) > code[class*="language-"] {
64 | padding: .1em;
65 | border-radius: .3em;
66 | white-space: normal;
67 | }
68 |
69 | .token.comment,
70 | .token.prolog,
71 | .token.doctype,
72 | .token.cdata {
73 | color: slategray;
74 | }
75 |
76 | .token.punctuation {
77 | color: #999;
78 | }
79 |
80 | .namespace {
81 | opacity: .7;
82 | }
83 |
84 | .token.property,
85 | .token.tag,
86 | .token.boolean,
87 | .token.number,
88 | .token.constant,
89 | .token.symbol,
90 | .token.deleted {
91 | color: #905;
92 | }
93 |
94 | .token.selector,
95 | .token.attr-name,
96 | .token.string,
97 | .token.char,
98 | .token.builtin,
99 | .token.inserted {
100 | color: #690;
101 | }
102 |
103 | .token.operator,
104 | .token.entity,
105 | .token.url,
106 | .language-css .token.string,
107 | .style .token.string {
108 | color: #a67f59;
109 | background: hsla(0, 0%, 100%, .5);
110 | }
111 |
112 | .token.atrule,
113 | .token.attr-value,
114 | .token.keyword {
115 | color: #07a;
116 | }
117 |
118 | .token.function {
119 | color: #DD4A68;
120 | }
121 |
122 | .token.regex,
123 | .token.important,
124 | .token.variable {
125 | color: #e90;
126 | }
127 |
128 | .token.important,
129 | .token.bold {
130 | font-weight: bold;
131 | }
132 | .token.italic {
133 | font-style: italic;
134 | }
135 |
136 | .token.entity {
137 | cursor: help;
138 | }
139 |
140 | .prism-previewer,
141 | .prism-previewer:before,
142 | .prism-previewer:after {
143 | position: absolute;
144 | pointer-events: none;
145 | }
146 | .prism-previewer,
147 | .prism-previewer:after {
148 | left: 50%;
149 | }
150 | .prism-previewer {
151 | margin-top: -48px;
152 | width: 32px;
153 | height: 32px;
154 | margin-left: -16px;
155 |
156 | opacity: 0;
157 | -webkit-transition: opacity .25s;
158 | -o-transition: opacity .25s;
159 | transition: opacity .25s;
160 | }
161 | .prism-previewer.flipped {
162 | margin-top: 0;
163 | margin-bottom: -48px;
164 | }
165 | .prism-previewer:before,
166 | .prism-previewer:after {
167 | content: '';
168 | position: absolute;
169 | pointer-events: none;
170 | }
171 | .prism-previewer:before {
172 | top: -5px;
173 | right: -5px;
174 | left: -5px;
175 | bottom: -5px;
176 | border-radius: 10px;
177 | border: 5px solid #fff;
178 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.5) inset, 0 0 10px rgba(0, 0, 0, 0.75);
179 | }
180 |
181 | .prism-previewer:after {
182 | top: 100%;
183 | width: 0;
184 | height: 0;
185 | margin: 5px 0 0 -7px;
186 | border: 7px solid transparent;
187 | border-color: rgba(255, 0, 0, 0);
188 | border-top-color: #fff;
189 | }
190 | .prism-previewer.flipped:after {
191 | top: auto;
192 | bottom: 100%;
193 | margin-top: 0;
194 | margin-bottom: 5px;
195 | border-top-color: rgba(255, 0, 0, 0);
196 | border-bottom-color: #fff;
197 | }
198 | .prism-previewer.active {
199 | opacity: 1;
200 | }
201 | .prism-previewer-color {
202 | background-image: linear-gradient(45deg, #bbb 25%, transparent 25%, transparent 75%, #bbb 75%, #bbb), linear-gradient(45deg, #bbb 25%, #eee 25%, #eee 75%, #bbb 75%, #bbb);
203 | background-size: 10px 10px;
204 | background-position: 0 0, 5px 5px;
205 | }
206 | .prism-previewer-color:before {
207 | background-color: inherit;
208 | background-clip: padding-box;
209 | }
210 |
211 |
--------------------------------------------------------------------------------
/src/demo/Demo.vue:
--------------------------------------------------------------------------------
1 |
2 | app-layout.demo
3 | hero-header(slot='header')
4 |
5 | template(slot='main-playground-output')
6 | h3.demo-typer-container.row.flex-items-xs-center.flex-items-xs-middle
7 | vue-typer.demo-typer(
8 | :text='text',
9 | :repeat='repeat',
10 | :shuffle='shuffle',
11 | :initial-action='initialAction',
12 | :pre-type-delay='preTypeDelay',
13 | :type-delay='typeDelay',
14 | :pre-erase-delay='preEraseDelay',
15 | :erase-delay='eraseDelay',
16 | :erase-style='eraseStyle',
17 | :erase-on-complete='eraseOnComplete',
18 | :caret-animation='caretAnimation')
19 |
20 | template(slot='main-playground-text')
21 | .form-group
22 | label(for='text') List of words to type:
23 | textarea(id='text', v-model='textModel', placeholder='text', :rows='3')
24 |
25 | template(slot='main-playground-config')
26 | .row
27 | #general-config.col-xs-12.col-lg-6
28 | form-input(v-model='repeatModel', label='repeat')
29 | form-check(v-model='shuffle', label='shuffle')
30 | form-check.shrink-text(v-model='eraseOnComplete', label='eraseOnComplete')
31 | form-radio(v-model='initialAction', :model='initialAction', label='initialAction',
32 | :options='["typing", "erasing"]')
33 |
34 | #delay-config.col-xs-12.col-lg-6
35 | form-input(v-model.number='preTypeDelay', label='preTypeDelay', type='number')
36 | form-input(v-model.number='typeDelay', label='typeDelay', type='number')
37 | form-input(v-model.number='preEraseDelay', label='preEraseDelay', type='number')
38 | form-input(v-model.number='eraseDelay', label='eraseDelay', type='number')
39 |
40 | #erase-style-config.col-xs-12.col-lg-6
41 | form-radio(v-model='eraseStyle', :model='eraseStyle', label='eraseStyle',
42 | :options='["backspace", "select-back", "select-all", "clear"]')
43 |
44 | #caret-config.col-xs-12.col-lg-6
45 | form-radio(v-model='caretAnimation', :model='caretAnimation', label='caretAnimation',
46 | :options='["solid", "blink", "smooth", "phase", "expand"]')
47 |
48 | template(slot='main-playground-code')
49 | code-block(:code='playgroundDemoCode', language='html')
50 |
51 | template(slot='style-showcase-panel-1')
52 | h4.text-xs-center
53 | vue-typer.state-typer(
54 | text='Katniss Everdeen',
55 | :pre-type-delay='1000',
56 | :type-delay='160',
57 | :pre-erase-delay='2000',
58 | :erase-delay='80',
59 | erase-style='select-back',
60 | caret-animation='solid'
61 | )
62 | code-block(:code='stateDemoStyleCode', language='css')
63 |
64 | template(slot='style-showcase-panel-2')
65 | h4.text-xs-center
66 | vue-typer.code-typer(
67 | text='Katniss Everdeen',
68 | :pre-type-delay='1000',
69 | :type-delay='160',
70 | :pre-erase-delay='2000',
71 | :erase-delay='1280',
72 | erase-style='select-all',
73 | caret-animation='blink'
74 | )
75 | code-block(:code='codeDemoStyleCode', language='css')
76 |
77 | template(slot='style-showcase-panel-3')
78 | h4.card-title.text-xs-center
79 | vue-typer.ghost-typer(
80 | text='Katniss Everdeen',
81 | :pre-type-delay='1000',
82 | :type-delay='160',
83 | :pre-erase-delay='2000',
84 | :erase-delay='80',
85 | erase-style='select-back'
86 | )
87 | code-block(:code='ghostDemoStyleCode', language='css')
88 |
89 | copyright-footer(slot='footer')
90 |
91 |
92 |
217 |
218 |
241 |
242 |
309 |
--------------------------------------------------------------------------------
/src/vue-typer/components/VueTyper.vue:
--------------------------------------------------------------------------------
1 |
2 | //-
3 | Ideally we'd just have span.left and span.right contain all the chars to the left and
4 | right of the caret, but line-wrapping becomes tricky on some browsers (FF/IE/Edge).
5 | Until we can find a solution for this, we just create one span per character.
6 | span.vue-typer
7 | span.left
8 | char.custom.typed(v-for='l in numLeftChars',
9 | :val="currentTextArray[l-1]")
10 | caret(:class='caretClasses', :animation='caretAnimation')
11 | span.right
12 | char.custom(v-for='r in numRightChars',
13 | :val="currentTextArray[numLeftChars + r-1]",
14 | :class='rightCharClasses')
15 |
16 |
17 |
404 |
405 |
415 |
--------------------------------------------------------------------------------
/test/unit/specs/VueTyper.spec.js:
--------------------------------------------------------------------------------
1 | import VueTyper from '../../../src/vue-typer/components/VueTyper'
2 | import _mount from '../utils/mount'
3 | import { waitUntilRendered, wait as _wait } from '../utils/wait'
4 |
5 | describe('VueTyper.vue', function() {
6 | let clock
7 |
8 | function mount(propsData) {
9 | return _mount(VueTyper, propsData)
10 | }
11 |
12 | function wait(ms, next) {
13 | _wait(clock, ms, next)
14 | }
15 |
16 | beforeEach(function() {
17 | clock = sinon.useFakeTimers()
18 | })
19 | afterEach(function() {
20 | clock.restore()
21 | })
22 |
23 | it('should have a name so it is identifiable in the Vue debugger', function() {
24 | expect(VueTyper.name).to.equal('VueTyper')
25 | })
26 |
27 | describe('Unicode support', function() {
28 | let vm
29 |
30 | function _mount(text) {
31 | vm = mount({ text })
32 | }
33 |
34 | function assertTextLength(length) {
35 | return expect(vm.currentTextLength).to.equal(length)
36 | }
37 |
38 | describe('Emojis', function() {
39 | const emojiTestData = {
40 | '1': ['💙', '⛳', '⛈'],
41 | '2': ['❤️', '💩'],
42 | '3': ['✍🏻', '🔥'],
43 | '4': ['👍🏻', '🤳🏻'],
44 | '5': ['💅🏻', '👨⚖️'],
45 | '7': ['👩🏻🎤', '👩🏻✈️'],
46 | '8': ['👩❤️👩', '👨👩👧'],
47 | '9': ['👩👩👦'],
48 | '11': ['👩❤️💋👩', '👨👩👧👦']
49 | }
50 |
51 | for (let emojiCodepoint in emojiTestData) {
52 | describe(`should properly count ${emojiCodepoint} codepoint emojis`, function() {
53 | let emojiList = emojiTestData[emojiCodepoint]
54 | for (let emoji of emojiList) {
55 | it(`${emoji} has length 1`, function() {
56 | _mount(emoji)
57 | assertTextLength(1)
58 | })
59 | }
60 | })
61 | }
62 | })
63 | })
64 |
65 | describe('Repeat and EraseOnComplete', function() {
66 | // eslint-disable-next-line one-var
67 | const preTypeDelay = 1, preEraseDelay = 1, typeDelay = 1, eraseDelay = 1
68 | let vm
69 | function createOptions(repeat, eraseOnComplete) {
70 | return { text: 'a', repeat, eraseOnComplete, preTypeDelay, typeDelay, preEraseDelay, eraseDelay }
71 | }
72 |
73 | it('should not repeat and should not erase final text', function(done) {
74 | vm = mount(createOptions(0, false))
75 | wait(preTypeDelay, () => {
76 | expect(vm.state).to.equal('complete')
77 | done()
78 | })
79 | })
80 |
81 | it('should not repeat and should erase final text', function(done) {
82 | vm = mount(createOptions(0, true))
83 | wait(preTypeDelay, () => {
84 | // assert that we're not done yet, we still have to erase the final text!
85 | expect(vm.state).not.to.equal('complete')
86 |
87 | // preEraseDelay = select-all, eraseDelay = actual erase
88 | wait(preEraseDelay + eraseDelay, () => {
89 | expect(vm.state).to.equal('complete')
90 | done()
91 | })
92 | })
93 | })
94 |
95 | it('should repeat as many times as specified and should not erase final text', function(done) {
96 | vm = mount(createOptions(1, false))
97 |
98 | // type first time
99 | wait(preTypeDelay, () => {
100 | // select-all and erase first time
101 | wait(preEraseDelay + eraseDelay, () => {
102 | // assert that we're not done yet, we still have to repeat one more time!
103 | expect(vm.state).not.to.equal('complete')
104 |
105 | // type second time
106 | wait(preTypeDelay, () => {
107 | // assert that we're not done yet, we still have to erase the final text!
108 | expect(vm.state).to.equal('complete')
109 | done()
110 | })
111 | })
112 | })
113 | })
114 |
115 | it('should repeat as many times as specified and erase final text', function(done) {
116 | vm = mount(createOptions(1, true))
117 |
118 | // type first time
119 | wait(preTypeDelay, () => {
120 | // select-all and erase first time
121 | wait(preEraseDelay + eraseDelay, () => {
122 | // assert that we're not done yet, we still have to repeat one more time!
123 | expect(vm.state).not.to.equal('complete')
124 |
125 | // type second time
126 | wait(preTypeDelay, () => {
127 | // assert that we're not done yet, we still have to erase the final text!
128 | expect(vm.state).not.to.equal('complete')
129 |
130 | // select-all and erase second time
131 | wait(preEraseDelay + eraseDelay, () => {
132 | expect(vm.state).to.equal('complete')
133 | done()
134 | })
135 | })
136 | })
137 | })
138 | })
139 | })
140 |
141 | describe('Caret', function() {
142 | it('should have the correct animation class', function() {
143 | let vm = mount({ text: 'abc', caretAnimation: 'solid' })
144 | let caret = vm.$el.querySelector('.caret')
145 | expect(caret.classList.contains('vue-typer-caret-solid')).to.be.true
146 |
147 | vm = mount({ text: 'abc', caretAnimation: 'blink' })
148 | caret = vm.$el.querySelector('.caret')
149 | expect(caret.classList.contains('vue-typer-caret-blink')).to.be.true
150 |
151 | vm = mount({ text: 'abc', caretAnimation: 'smooth' })
152 | caret = vm.$el.querySelector('.caret')
153 | expect(caret.classList.contains('vue-typer-caret-smooth')).to.be.true
154 |
155 | vm = mount({ text: 'abc', caretAnimation: 'phase' })
156 | caret = vm.$el.querySelector('.caret')
157 | expect(caret.classList.contains('vue-typer-caret-phase')).to.be.true
158 |
159 | vm = mount({ text: 'abc', caretAnimation: 'expand' })
160 | caret = vm.$el.querySelector('.caret')
161 | expect(caret.classList.contains('vue-typer-caret-expand')).to.be.true
162 | })
163 | it('should be positionable to the beginning', function() {
164 | const vm = mount({ text: 'abc' })
165 | vm.moveCaretToStart()
166 | expect(vm.currentTextIndex).to.equal(0)
167 | })
168 | it('should be positionable to the end', function() {
169 | const vm = mount({ text: 'abc' })
170 | vm.moveCaretToEnd()
171 | expect(vm.currentTextIndex).to.equal(3)
172 | })
173 | it('should be shiftable', function() {
174 | const vm = mount({ text: 'abc' })
175 | vm.moveCaretToStart()
176 | vm.shiftCaret(2)
177 | expect(vm.currentTextIndex).to.equal(2)
178 | })
179 | })
180 |
181 | describe('Events', function() {
182 | const text = 'abc'
183 | let vm
184 | beforeEach(function() {
185 | vm = mount({ text })
186 | })
187 |
188 | it('should emit \'typed-char\' event for each char in a typed word', function(done) {
189 | let numTyped = 0
190 | vm.$on('typed-char', (char, index) => {
191 | expect(text.charAt(numTyped)).to.equal(char)
192 | expect(numTyped).to.equal(index)
193 |
194 | numTyped++
195 | if (numTyped === text.length) {
196 | done()
197 | }
198 | })
199 |
200 | let numChars = text.length
201 | while (numChars--) {
202 | vm.typeStep()
203 | }
204 | })
205 |
206 | it('should emit \'typed\' event after a word is typed', function(done) {
207 | vm.$on('typed', (word) => {
208 | expect(word).to.equal(text)
209 | done()
210 | })
211 | vm.onTyped()
212 | })
213 | it('should emit \'erased\' event after a word is erased', function(done) {
214 | vm.$on('erased', (word) => {
215 | expect(word).to.equal(text)
216 | done()
217 | })
218 | vm.onErased()
219 | })
220 | it('should emit \'completed\' event after all words are typed/erased', function(done) {
221 | vm.$on('completed', (word) => {
222 | done()
223 | })
224 | vm.onComplete()
225 | })
226 | })
227 |
228 | describe('Typing and Erasing', function() {
229 | function expectText(vm, side, expectedText, expectedCharClass) {
230 | const container = vm.$el.querySelector('.' + side)
231 | expect(container.classList.contains(side)).to.be.true
232 | expect(container.childElementCount).to.equal(expectedText.length)
233 |
234 | let child
235 | for (let i = 0; i < expectedText.length; i++) {
236 | child = container.children[i]
237 | if (expectedCharClass) {
238 | expect(child.classList.contains(expectedCharClass))
239 | }
240 | expect(child.textContent).to.equal(expectedText.charAt(i))
241 | }
242 | }
243 | function expectLeftText(vm, expectedText) {
244 | expectText(vm, 'left', expectedText, 'typed')
245 | }
246 | function expectRightText(vm, expectedText, expectedCharClass) {
247 | expectText(vm, 'right', expectedText, expectedCharClass)
248 | }
249 |
250 | describe('Initial Action', function() {
251 | it('should initialize to typing state', function(done) {
252 | const vm = mount({
253 | text: 'abc',
254 | initialAction: 'typing'
255 | })
256 |
257 | waitUntilRendered(() => {
258 | expectLeftText(vm, '')
259 | expectRightText(vm, 'abc')
260 | done()
261 | })
262 | })
263 |
264 | it('should initialize to erasing state', function(done) {
265 | const vm = mount({
266 | text: 'abc',
267 | initialAction: 'erasing'
268 | })
269 |
270 | waitUntilRendered(() => {
271 | expectLeftText(vm, 'abc')
272 | expectRightText(vm, '')
273 | done()
274 | })
275 | })
276 | })
277 |
278 | describe('Typing Delay', function() {
279 | const preTypeDelay = 100
280 | const typeDelay = 50
281 | let vm
282 | beforeEach(function() {
283 | vm = mount({
284 | text: 'abc',
285 | typeDelay,
286 | preTypeDelay
287 | })
288 | })
289 |
290 | it('should wait \'preTypeDelay\' before typing the first character', function(done) {
291 | wait(preTypeDelay, () => {
292 | expectLeftText(vm, 'a')
293 | expectRightText(vm, 'bc')
294 | done()
295 | })
296 | })
297 |
298 | it('should wait \'typeDelay\' before typing the second character', function(done) {
299 | wait(preTypeDelay + typeDelay, () => {
300 | expectLeftText(vm, 'ab')
301 | expectRightText(vm, 'c')
302 | done()
303 | })
304 | })
305 | })
306 |
307 | describe('Erasing Delay', function() {
308 | const preEraseDelay = 100
309 | const eraseDelay = 50
310 | let vm
311 |
312 | function createBeforeEach(eraseStyle) {
313 | return function() {
314 | vm = mount({
315 | text: 'abc',
316 | initialAction: 'erasing',
317 | eraseStyle,
318 | eraseDelay,
319 | preEraseDelay
320 | })
321 | }
322 | }
323 |
324 | describe('backspace', function() {
325 | beforeEach(createBeforeEach('backspace'))
326 | it('should wait \'preEraseDelay\' before erasing the first character', function(done) {
327 | wait(preEraseDelay, () => {
328 | expectLeftText(vm, 'ab')
329 | expectRightText(vm, 'c', 'erased')
330 | done()
331 | })
332 | })
333 | it('should wait \'eraseDelay\' before erasing the second character', function(done) {
334 | wait(preEraseDelay + eraseDelay, () => {
335 | expectLeftText(vm, 'a')
336 | expectRightText(vm, 'bc', 'erased')
337 | done()
338 | })
339 | })
340 | })
341 |
342 | describe('select-back', function() {
343 | beforeEach(createBeforeEach('select-back'))
344 | it('should wait \'preEraseDelay\' before selecting the first character', function(done) {
345 | wait(preEraseDelay, () => {
346 | expectLeftText(vm, 'ab')
347 | expectRightText(vm, 'c', 'erased')
348 | done()
349 | })
350 | })
351 | it('should wait \'eraseDelay\' before selecting the second character', function(done) {
352 | wait(preEraseDelay + eraseDelay, () => {
353 | expectLeftText(vm, 'a')
354 | expectRightText(vm, 'bc', 'selected')
355 | done()
356 | })
357 | })
358 | })
359 |
360 | describe('select-all', function() {
361 | beforeEach(createBeforeEach('select-all'))
362 | it('should wait \'preEraseDelay\' before selecting all characters', function(done) {
363 | wait(preEraseDelay, () => {
364 | expectLeftText(vm, '')
365 | expectRightText(vm, 'abc', 'selected')
366 | done()
367 | })
368 | })
369 | it('should wait \'eraseDelay\' before erasing the entire selection', function(done) {
370 | wait(preEraseDelay + eraseDelay, () => {
371 | expectLeftText(vm, '')
372 | expectRightText(vm, 'abc', 'erased')
373 | done()
374 | })
375 | })
376 | })
377 |
378 | describe('clear', function() {
379 | beforeEach(createBeforeEach('clear'))
380 | it('should wait \'preEraseDelay\' before clearing all characters', function(done) {
381 | wait(preEraseDelay, () => {
382 | expectLeftText(vm, '')
383 | expectRightText(vm, 'abc', 'erased')
384 | done()
385 | })
386 | })
387 | })
388 | })
389 | })
390 | })
391 |
--------------------------------------------------------------------------------
/src/demo/lib/prism/prism.js:
--------------------------------------------------------------------------------
1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css&plugins=previewer-base+previewer-color+normalize-whitespace */
2 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(\w+)\b/i,t=0,n=_self.Prism={util:{encode:function(e){return e instanceof a?new a(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(v instanceof a)){u.lastIndex=0;var b=u.exec(v),k=1;if(!b&&h&&m!=r.length-1){if(u.lastIndex=y,b=u.exec(e),!b)break;for(var w=b.index+(c?b[1].length:0),_=b.index+b[0].length,A=m,P=y,j=r.length;j>A&&_>P;++A)P+=r[A].length,w>=P&&(++m,y=P);if(r[m]instanceof a||r[A-1].greedy)continue;k=A-m,v=e.slice(y,P),b.index-=y}if(b){c&&(f=b[1].length);var w=b.index+f,b=b[0].slice(f),_=w+b.length,x=v.slice(0,w),O=v.slice(_),S=[m,k];x&&S.push(x);var N=new a(l,g?n.tokenize(b,g):b,d,b,h);S.push(N),O&&S.push(O),Array.prototype.splice.apply(r,S)}}}}}return r},hooks:{all:{},add:function(e,t){var a=n.hooks.all;a[e]=a[e]||[],a[e].push(t)},run:function(e,t){var a=n.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(t)}}},a=n.Token=function(e,t,n,a,r){this.type=e,this.content=t,this.alias=n,this.length=0|(a||"").length,this.greedy=!!r};if(a.stringify=function(e,t,r){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return a.stringify(n,t,e)}).join("");var i={type:e.type,content:a.stringify(e.content,t,r),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}n.hooks.run("wrap",i);var o=Object.keys(i.attributes).map(function(e){return e+'="'+(i.attributes[e]||"").replace(/"/g,""")+'"'}).join(" ");return"<"+i.tag+' class="'+i.classes.join(" ")+'"'+(o?" "+o:"")+">"+i.content+""+i.tag+">"},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var t=JSON.parse(e.data),a=t.language,r=t.code,i=t.immediateClose;_self.postMessage(n.highlight(r,n.languages[a],a)),i&&_self.close()},!1),_self.Prism):_self.Prism;var r=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return r&&(n.filename=r.src,document.addEventListener&&!r.hasAttribute("data-manual")&&("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener("DOMContentLoaded",n.highlightAll))),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
3 | Prism.languages.markup={comment://,prolog:/<\?[\w\W]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/?[\da-z]{1,8};/i},Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup;
4 | Prism.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:{pattern:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,greedy:!0},property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.util.clone(Prism.languages.css),Prism.languages.markup&&(Prism.languages.insertBefore("markup","tag",{style:{pattern:/(