├── .npmignore
├── resources
├── vue
│ ├── mixins
│ │ ├── store-utils.js
│ │ ├── i18n-utils.js
│ │ ├── dom-event-utils.js
│ │ └── router-utils.js
│ └── components
│ │ ├── __tests__
│ │ ├── calendar.spec.js
│ │ └── __snapshots__
│ │ │ └── calendar.spec.js.snap
│ │ ├── calendar-years.vue
│ │ ├── calendar-months.vue
│ │ ├── calendar-header.vue
│ │ └── calendar.vue
├── css
│ ├── variables.styl
│ ├── main.styl
│ └── components
│ │ └── calendar.styl
├── lang
│ └── calendar.js
└── js
│ ├── main.js
│ └── utils
│ ├── date-time.js
│ ├── __tests__
│ └── object.spec.js
│ ├── object.js
│ └── url.js
├── jest.init.js
├── README.md
├── .editorconfig
├── postcss.config.js
├── .babelrc.js
├── .gitignore
├── examples
└── index.html
├── jest.config.js
├── rollup.config.js
├── package.json
└── gulpfile.js
/.npmignore:
--------------------------------------------------------------------------------
1 | __tests__
--------------------------------------------------------------------------------
/resources/vue/mixins/store-utils.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/jest.init.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Test globals
3 | */
--------------------------------------------------------------------------------
/resources/css/variables.styl:
--------------------------------------------------------------------------------
1 | color_accent = blue
--------------------------------------------------------------------------------
/resources/css/main.styl:
--------------------------------------------------------------------------------
1 | @import './variables.styl'
2 |
3 | @import './components/**/*.styl'
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Frontend utilities package
2 |
3 | A set of commonly used functions and Vue-mixins
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | # Unix-style newlines with a newline ending every file
4 | [*]
5 | end_of_line = lf
6 | insert_final_newline = true
7 |
8 | # 4 space indentation
9 | indent_style = space
10 | indent_size = 4
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const isDev = process.env.NODE_ENV !== 'production';
2 |
3 | module.exports = {
4 | plugins: [
5 | require('postcss-import'),
6 | require('autoprefixer'),
7 | isDev ? false : require('cssnano')({ preset:'default' })
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.babelrc.js:
--------------------------------------------------------------------------------
1 | const isModern = true //process.env.BROWSERS_ENV === 'modern';
2 |
3 | module.exports = {
4 | presets: [
5 | ['@babel/preset-env', {
6 | useBuiltIns: 'usage',
7 | corejs: '2',
8 | targets: isModern ? { esmodules: true } : undefined,
9 | }]
10 | ]
11 | };
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 |
3 |
4 | ## test
5 | /tests/report
6 | /coverage
7 |
8 |
9 | ## IDEs
10 | /.idea
11 | /.vscode
12 | .phpstorm.meta.php
13 | _ide_helper.php
14 | _ide_helper_models.php
15 |
16 |
17 | ## package managers
18 | /node_modules
19 | npm-debug.log
20 | yarn-error.log
21 | package-lock.json
22 | yarn.lock
23 | /vendor
24 | composer.lock
--------------------------------------------------------------------------------
/resources/vue/mixins/i18n-utils.js:
--------------------------------------------------------------------------------
1 | import { get } from '../../js/utils/object'
2 |
3 | export default {
4 |
5 | props: {
6 |
7 | lang: Object
8 | },
9 |
10 | computed: {
11 | // TODO: fix objects merging (replace ???)
12 | // TODO: write test for external lang prop
13 | '$lang': function() {
14 | return { ...get(this.$options, '_config.lang', {}), ...this.lang }
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AWES Utilities
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/resources/lang/calendar.js:
--------------------------------------------------------------------------------
1 | export default {
2 | weekdays: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
3 | weekdaysFull: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thirsday', 'Friday', 'Saturday'],
4 | months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'],
5 | monthsFull: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
6 | prevMonth: 'Previous month',
7 | nextMonth: 'Next month'
8 | }
--------------------------------------------------------------------------------
/resources/css/components/calendar.styl:
--------------------------------------------------------------------------------
1 | calendar_width = 300px
2 |
3 | .ui-calendar
4 | width calendar_width
5 | max-width 100%
6 |
7 | &__days,
8 | &__weekdays
9 | display flex
10 | flex-wrap wrap
11 |
12 | &__weekday
13 | text-align center
14 |
15 | &__day,
16 | &__weekday
17 | width (100% / 7.0001)
18 |
19 | &__day
20 |
21 | &.is-selected
22 | box-shadow 0 0 5px color_accent
23 |
24 | &.is-edge
25 | pointer-events none
26 | opacity .5
--------------------------------------------------------------------------------
/resources/js/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/vue.js'
2 | import Calendar from '../vue/components/calendar.vue'
3 | import CalendarHeader from '../vue/components/calendar-header.vue'
4 | import CalendarMonths from '../vue/components/calendar-months.vue'
5 | import CalendarYears from '../vue/components/calendar-years.vue'
6 |
7 | Vue.component('ui-calendar', Calendar)
8 | Vue.component('ui-calendar-header', CalendarHeader)
9 | Vue.component('ui-calendar-months', CalendarMonths)
10 | Vue.component('ui-calendar-years', CalendarYears)
11 |
12 | const app = new Vue({
13 | el: '#app'
14 | });
15 |
--------------------------------------------------------------------------------
/resources/vue/mixins/dom-event-utils.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | methods: {
4 |
5 | _dataAttributeEmitter($event) {
6 |
7 | let btn = $event.target
8 |
9 | if ( ! btn.hasAttribute('data-emit') ) {
10 | btn = btn.closest('[data-emit]')
11 | }
12 |
13 | if ( ! btn ) return
14 |
15 | let event = btn.getAttribute('data-emit')
16 | let args = btn.hasAttribute('data-args') ? JSON.parse(btn.getAttribute('data-args')) : true
17 |
18 | this.$emit(event, args)
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: [
3 | 'js',
4 | 'vue'
5 | ],
6 | moduleDirectories: [
7 | 'resources/js/utils',
8 | 'node_modules'
9 | ],
10 | transform: {
11 | '^.*\\.(vue)$': 'vue-jest',
12 | '^.+\\.js$': 'babel-jest'
13 | },
14 | setupFiles: [
15 | '/jest.init.js'
16 | ],
17 | collectCoverage: true,
18 | collectCoverageFrom: [
19 | '/resources/vue/components/*.vue',
20 | '/resources/js/utils/*.js'
21 | ],
22 | coverageDirectory: '/coverage',
23 | coverageReporters: ['html', 'text-summary']
24 | }
--------------------------------------------------------------------------------
/resources/vue/components/__tests__/calendar.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import uiCalendar from '../calendar.vue'
3 |
4 | describe('Calendar', () => {
5 |
6 | it('matches overall snapshot', () => {
7 |
8 | const wrapper = mount(uiCalendar, {
9 | propsData: {
10 | month: 6,
11 | year: 2019
12 | }
13 | })
14 |
15 | expect(wrapper.html()).toMatchSnapshot()
16 | })
17 |
18 | it('passes timestamp to "day" scoped slot', () => {
19 |
20 | const wrapper = mount(uiCalendar, {
21 | propsData: {
22 | month: 6,
23 | year: 2019,
24 | },
25 | scopedSlots: {
26 | day: `{{ props.timestamp }}
`
27 | }
28 | })
29 |
30 | expect(wrapper.find('.day').text()).toEqual('1561928400000')
31 | })
32 | })
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | const isDev = process.env.NODE_ENV === 'development'
2 |
3 | const vue = require('rollup-plugin-vue')
4 | const uglify = require('rollup-plugin-terser').terser
5 | const nodeResolve = require('rollup-plugin-node-resolve')
6 | const json = require('rollup-plugin-json')
7 | const commonJs = require('rollup-plugin-commonjs')
8 |
9 | module.exports = {
10 | input: './resources/js/main.js',
11 | output: {
12 | file: './dist/js/main.js',
13 | format: 'iife'
14 | },
15 | plugins: [
16 | vue({
17 | css: false,
18 | template: {
19 | compilerOptions: {
20 | whitespace: 'condense',
21 | preserveWhitespace: false
22 | }
23 | }
24 | }),
25 | nodeResolve({
26 | mainFields: ['module', 'main']
27 | }),
28 | commonJs({
29 | include: 'node_modules/**',
30 | sourceMap: false
31 | }),
32 | json()
33 | ]
34 | }
35 |
36 | if ( ! isDev ) {
37 | module.exports.plugins.push( uglify() )
38 | }
39 |
--------------------------------------------------------------------------------
/resources/js/utils/date-time.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates an array of 42 days to display a calendar
3 | *
4 | * @param {number} month from `0` to `11` - month to render, required
5 | * @param {number} year in `XXXX` format - year to render, required
6 | * @param {number} [firstDay = 0] from 0 to 6, e.g. `0 === Sunday`, `1 === Monday`, ...
7 | * default `0`
8 | *
9 | * @return {Array} Array of Date objects of given month and year
10 | * with edge days to fullfill 6 x 7 square table
11 | */
12 | export function getCalendarDays(month, year, firstDay = 0) {
13 |
14 | const result = [];
15 |
16 | let day = new Date(year, month);
17 |
18 | // Modify first day if not correct
19 | if (day.getDay() !== firstDay) {
20 | day.setDate(firstDay - day.getDay() + 1);
21 | }
22 |
23 | // 6 weeks always displayed to keep size
24 | for (let i = 0; i < (6 * 7); ++i) {
25 | result.push(new Date(day));
26 | day.setDate(day.getDate() + 1);
27 | }
28 |
29 | return result;
30 | }
--------------------------------------------------------------------------------
/resources/vue/components/calendar-years.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/resources/vue/mixins/router-utils.js:
--------------------------------------------------------------------------------
1 | import { forEach, isEmpty } from '../js/object'
2 |
3 | /**
4 | * Modifies VueRouter current GET-params and pushes next route
5 | * applied to VueRouter.prototype
6 | *
7 | * @param {Object} queryObj - params object. If param is set to `null`,
8 | * `undefined`, or empty `String`,
9 | * it will be deleted from query.
10 | * To set param=null, pass a string `'null'`
11 | * @param {Boolean} push - true to use history.pushState,
12 | * false to use history.replaceState
13 | *
14 | * @return {Object} - AWES._vueRouter - global Vue router instance
15 | */
16 | export function setParam(queryObj, push = true) {
17 |
18 | // do nothing if nothing passed
19 | if ( isEmpty(queryObj) ) return
20 |
21 | // shallow copy next route is enough for reactivity
22 | let next = Object.assign({}, this.currentRoute)
23 |
24 | // shallow copy route query
25 | let query = Object.assign({}, this.currentRoute.query)
26 |
27 | // merge queries
28 | Object.assign(query, queryObj)
29 |
30 | // remove null values
31 | query = forEach(query, function(val, key, obj) {
32 | if ( typeof val === 'undefined' || val === '' || val === null ) {
33 | delete obj[key]
34 | }
35 | })
36 |
37 | // set query and push route
38 | next.query = query
39 | this[push ? 'push' : 'replace'](next)
40 |
41 | return this
42 | }
43 |
44 | /**
45 | * Component mixin - extends default $router functional
46 | */
47 |
48 | export const routerMixin = {
49 |
50 | beforeCreate() {
51 | this.$router.$setParam = setParam
52 | }
53 | }
54 |
55 | export default routerMixin
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@awes-io/utilities",
3 | "version": "2.1.0",
4 | "description": "Utilities package for frontend",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest --no-cache",
8 | "watch": "cross-env NODE_ENV=development gulp",
9 | "build": "npm run test && cross-env NODE_ENV=production gulp"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git@github.com:awes-io/utilities.git"
14 | },
15 | "keywords": [
16 | "utilities",
17 | "vue",
18 | "helpers"
19 | ],
20 | "author": "AwesCode (https://www.awescode.de/)",
21 | "license": "ISC",
22 | "dependencies": {
23 | "vue": "^2.6.10"
24 | },
25 | "devDependencies": {
26 | "@babel/core": "^7.4.5",
27 | "@babel/preset-env": "^7.4.5",
28 | "@vue/test-utils": "^1.0.0-beta.29",
29 | "autoprefixer": "^9.6.0",
30 | "babel-core": "^7.0.0",
31 | "babel-jest": "^24.8.0",
32 | "browser-sync": "^2.26.7",
33 | "cross-env": "^5.2.0",
34 | "cssnano": "^4.1.10",
35 | "gulp": "^4.0.2",
36 | "gulp-clean": "^0.4.0",
37 | "gulp-noop": "^1.0.0",
38 | "gulp-plumber": "^1.2.1",
39 | "gulp-postcss": "^8.0.0",
40 | "gulp-rollup": "^2.16.2",
41 | "gulp-sourcemaps": "^2.6.5",
42 | "gulp-stylus": "^2.7.0",
43 | "jest": "^24.8.0",
44 | "postcss": "^7.0.17",
45 | "postcss-import": "^12.0.1",
46 | "rollup": "^0.68.2",
47 | "rollup-plugin-commonjs": "^9.2.0",
48 | "rollup-plugin-json": "^3.1.0",
49 | "rollup-plugin-node-resolve": "^4.0.0",
50 | "rollup-plugin-terser": "^4.0.2",
51 | "rollup-plugin-vue": "^4.6.1",
52 | "stylus": "^0.54.5",
53 | "vue-jest": "^3.0.4",
54 | "vue-template-compiler": "^2.6.10",
55 | "vue-test-utils": "^1.0.0-beta.11"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/resources/vue/components/calendar-months.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
16 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp')
2 | const clean = require('gulp-clean')
3 | const plumber = require('gulp-plumber')
4 | const noop = require('gulp-noop')
5 | const stylus = require('gulp-stylus')
6 | const postcss = require('gulp-postcss')
7 | const sourcemaps = require('gulp-sourcemaps')
8 | const browserSync = require('browser-sync').create()
9 | const rollup = require('gulp-rollup')
10 |
11 | const isDev = process.env.NODE_ENV === 'development'
12 |
13 |
14 | /*
15 | * Server
16 | */
17 |
18 | if ( isDev ) {
19 | gulp.task('serve', function(){
20 |
21 | browserSync.init({
22 | ui: false,
23 | open: false,
24 | notify: false,
25 | serveStatic: ['./examples', './dist'],
26 | proxy: "localhost:3030"
27 | })
28 |
29 | gulp.watch('./resources/css/**/*.styl', gulp.series('build:styles'))
30 | gulp.watch(['./resources/js/**/*.js', './resources/vue/**/*.vue', 'src/vue/**/*.js'], gulp.series('build:js', 'reload'))
31 | gulp.watch('./examples/**/*.html', gulp.series('reload'))
32 | })
33 |
34 | gulp.task('reload', function(done) { browserSync.reload(); done() })
35 | }
36 |
37 |
38 | /*
39 | * JS
40 | */
41 |
42 | const rollupConfig = require('./rollup.config.js')
43 | rollupConfig.allowRealFiles = true // solves gulp-rollup hipotetical file system problem
44 | rollupConfig.rollup = require('rollup')
45 |
46 | gulp.task('build:js', function(){
47 | return gulp.src('./resources/js/*.js')
48 | .pipe( plumber() )
49 | .pipe( isDev ? sourcemaps.init() : noop() )
50 | .pipe( rollup(rollupConfig) )
51 | .pipe( isDev ? sourcemaps.write() : noop() )
52 | .pipe( gulp.dest('./dist/js') )
53 | })
54 |
55 |
56 | /*
57 | * Styles
58 | */
59 |
60 | gulp.task('build:styles', function(){
61 | return gulp.src('./resources/css/main.styl')
62 | .pipe( plumber() )
63 | .pipe( isDev ? sourcemaps.init() : noop() )
64 | .pipe( stylus() )
65 | .pipe( postcss() )
66 | .pipe( isDev ? sourcemaps.write() : noop() )
67 | .pipe( gulp.dest('./dist/css') )
68 | .pipe( isDev ? browserSync.stream() : noop() )
69 | })
70 |
71 |
72 | /*
73 | * Gloabl tasks
74 | */
75 |
76 | gulp.task('clean', function(){
77 | return gulp.src('./dist', { read: false, allowEmpty: true })
78 | .pipe( clean() )
79 | })
80 |
81 | gulp.task('build', gulp.series('build:js', 'build:styles') )
82 |
83 | // start
84 | defaultTask = ['clean', 'build']
85 | if ( isDev ) defaultTask.push('serve')
86 | gulp.task('default', gulp.series(defaultTask) )
87 |
--------------------------------------------------------------------------------
/resources/vue/components/calendar-header.vue:
--------------------------------------------------------------------------------
1 |
2 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/resources/js/utils/__tests__/object.spec.js:
--------------------------------------------------------------------------------
1 | import {
2 | isObject,
3 | isEmpty,
4 | pathToArr,
5 | get,
6 | set
7 | } from '../object'
8 |
9 |
10 | describe('The isObject method', () => {
11 |
12 | it('returns {Boolean}false on undefined, null, Number, Boolean, NaN, String, Function', () => {
13 |
14 | // undefined
15 | expect( isObject() ).toBe(false)
16 | expect( isObject(undefined) ).toBe(false)
17 |
18 | // null
19 | expect( isObject(null) ).toBe(false)
20 |
21 | // number
22 | expect( isObject(0) ).toBe(false)
23 | expect( isObject(Infinity) ).toBe(false)
24 | expect( isObject(NaN) ).toBe(false)
25 |
26 | // string
27 | expect( isObject('') ).toBe(false)
28 | expect( isObject('test') ).toBe(false)
29 |
30 | // function
31 | expect( isObject(function(){}) ).toBe(false)
32 |
33 | })
34 |
35 | it('returns {Boolean}true on Array and Objects', () => {
36 |
37 | // array
38 | expect( isObject([]) ).toBe(true)
39 |
40 | // object
41 | expect( isObject({}) ).toBe(true)
42 | expect( isObject(new Object()) ).toBe(true)
43 | })
44 | })
45 |
46 |
47 | describe('The isEmpty method', () => {
48 |
49 | it('returns {Boolean}false on undefined; null; false; 0; empty String, Aarray and Object', () => {
50 |
51 | expect( isEmpty() ).toBe(true)
52 | expect( isEmpty(undefined) ).toBe(true)
53 | expect( isEmpty(null) ).toBe(true)
54 | expect( isEmpty(false) ).toBe(true)
55 | expect( isEmpty('') ).toBe(true)
56 | expect( isEmpty(0) ).toBe(true)
57 | expect( isEmpty([]) ).toBe(true)
58 | expect( isEmpty({}) ).toBe(true)
59 | })
60 |
61 | it('returns {Boolean}true on true; function; String; Array and Object with values', () => {
62 |
63 | expect( isEmpty(true) ).toBe(false)
64 | expect( isEmpty('test') ).toBe(false)
65 | expect( isEmpty(function(){}) ).toBe(false)
66 | expect( isEmpty(['']) ).toBe(false)
67 | expect( isEmpty({test: ''}) ).toBe(false)
68 | })
69 | })
70 |
71 |
72 | describe('The pathToArr method', () => {
73 |
74 | const result = ['some', 'nested', '0', 'value']
75 |
76 | it('supports both dot and bracket notation', () => {
77 |
78 | expect( pathToArr('some.nested[0].value') ).toEqual(result)
79 | expect( pathToArr('some.nested.0.value') ).toEqual(result)
80 | })
81 |
82 | it('supports single and double quots', () => {
83 |
84 | expect( pathToArr('some["nested"].0.value') ).toEqual(result)
85 | expect( pathToArr('some["nested"][0][\'value\']') ).toEqual(result)
86 | })
87 |
88 | it('supports spaces', () => {
89 |
90 | expect( pathToArr('[\'with\']["spaced value"]') ).toEqual(['with', 'spaced value'])
91 | })
92 | })
93 |
94 |
95 | describe('The get method', () => {
96 |
97 | const mock = {
98 | number: 1,
99 | boolean: false,
100 | array: [
101 | [1,2,3],
102 | 'two',
103 | {
104 | prop: 'test'
105 | }
106 | ],
107 | object: {
108 | 'empty one': {},
109 | property: ['a', 'b', 'c']
110 | }
111 | }
112 |
113 |
114 | it('returns values', () => {
115 |
116 | expect( get(mock, 'number') ).toBe(1)
117 | expect( get(mock, 'boolean') ).toBe(false)
118 | })
119 |
120 | it('returns exact objects', () => {
121 |
122 | expect( get(mock, 'object') ).toBe(mock.object)
123 | })
124 |
125 | it('returns nested values', () => {
126 |
127 | expect( get(mock, 'object.property[2]') ).toBe('c')
128 | })
129 |
130 | it('returns default value if seeked not found', () => {
131 |
132 | expect( get(mock, 'proper.ty', 'default') ).toBe('default')
133 | })
134 |
135 | it('returns default value on non-objects', () => {
136 |
137 | expect( get(null, 'proper.ty', 'default') ).toBe('default')
138 | expect( get(42, 'proper.ty') ).toBe(undefined)
139 | })
140 | })
141 |
142 |
143 | // describe('The set method', () => {
144 |
145 | // it('sets given values by paths', () => {
146 |
147 |
148 | // })
149 | // })
--------------------------------------------------------------------------------
/resources/js/utils/object.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Detects if the given value is an object
3 | *
4 | * @param {*} val - a variable to check
5 | *
6 | */
7 |
8 | export function isObject(val) {
9 | return val != null && typeof val === 'object'
10 | }
11 |
12 |
13 | /**
14 | * Detects if the value is empty
15 | * returns true if the value is `undefined`, `null`, `false`, `''`, `0`, `[]` or `{}`
16 | *
17 | * @param {Any} val - value to check
18 | *
19 | * @returns {Boolean} - is the value empty
20 | */
21 | export function isEmpty(val) {
22 | if ( ! val ) {
23 | return true
24 | } else if ( typeof val !== 'function' && val.hasOwnProperty('length') && typeof val.length === 'number' ) {
25 | return !val.length
26 | } else if ( typeof val === 'object' ) {
27 | return !Object.keys(val).length
28 | }
29 | return false
30 | }
31 |
32 |
33 | /**
34 | * Creates an array by splitting given path to object's value
35 | *
36 | * @param {String} path - Path to value in object
37 | *
38 | * @returns {Array} Array of levels to object
39 | *
40 | * @example
41 | * // returns ['some', 'nested', '0', 'value']
42 | * pathToArr('some.nested[0].value')
43 | * pathToArr('some.nested.0.value')
44 | *
45 | */
46 |
47 | export function pathToArr(path) {
48 | return path.split(/(?:\]?\.|\[['"]?|['"]?\])/g).filter(part => part !== '')
49 | }
50 |
51 |
52 | /**
53 | * Get a value by given path
54 | *
55 | * @param {Object} obj - object to search
56 | * @param {String} path - path to level
57 | * @param {*} defaultValue - default value if nothig found
58 | *
59 | * @returns {*} value of given path in object
60 | */
61 |
62 | export function get(obj, path, defaultValue) {
63 |
64 | if ( ! isObject(obj) ) {
65 | console.warn('get supports only objects, ', obj, ' given')
66 | return defaultValue
67 | }
68 |
69 | // create a path array of levels from a key
70 | path = pathToArr(path)
71 |
72 | let current = obj, value
73 |
74 | while (path.length && current) {
75 | let key = path.shift()
76 | if (path.length) {
77 | current = current[key]
78 | } else {
79 | value = current[key]
80 | }
81 | }
82 |
83 | return typeof value !== 'undefined' ? value : defaultValue
84 | }
85 |
86 |
87 | /**
88 | * Sets value in object by given path array
89 | * > mutates original objects!
90 | *
91 | * @param {Object} obj - flattened object
92 | * @param {Array} path - path levels
93 | * @param {*} value - value to set
94 | *
95 | * @returns {Object} - objects with setted values
96 | *
97 | */
98 |
99 | export function set(obj, path, value) {
100 |
101 | // create a path array of levels from a key
102 | let _path = pathToArr(path)
103 |
104 | // set current object level
105 | let current = obj
106 |
107 |
108 | do {
109 |
110 | // get next key
111 | let _key = _path.shift()
112 |
113 | // check if its a middle or last key
114 | if (_path.length) {
115 |
116 | // skip if a structure with such key exists
117 | if (!current[_key]) {
118 |
119 | // creaate an array if next key is numeric or an object otherwise
120 | let nextStructure = isNaN(_path[0]) ? {} : []
121 | current[_key] = nextStructure
122 | }
123 |
124 | // go a level deeper for next iteration
125 | current = current[_key]
126 |
127 | } else {
128 |
129 | // if this is a last key, set it`s value
130 | current[_key] = value
131 | }
132 | } while (_path.length)
133 |
134 | return obj
135 | }
136 |
137 |
138 | /**
139 | * Applies a function to every nested object in given object
140 | * and passes value, key and object itself
141 | *
142 | * @param {Array, Object} obj - given object
143 | * @param {Function} fn - function to apply
144 | *
145 | * @return {Array, Object} mutated object
146 | */
147 |
148 | export function forEach(obj, fn) {
149 |
150 | if ( ! isObject(obj) ) return
151 |
152 | let keys = Object.keys(obj)
153 |
154 | for (let i = 0; i < keys.length; i++ ) {
155 | let key = keys[i]
156 | let val = obj[key]
157 | if ( isObject(val) ) {
158 | forEach(val, fn)
159 | } else {
160 | fn.call(null, obj[key], key, obj)
161 | }
162 | }
163 |
164 | return obj
165 | }
--------------------------------------------------------------------------------
/resources/vue/components/calendar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
17 | {{ weekday }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
29 |
30 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/resources/js/utils/url.js:
--------------------------------------------------------------------------------
1 | import { isObject, get } from './object'
2 |
3 |
4 | /**
5 | * Replaces params in url template with given data
6 | *
7 | * @param {String} url - url template
8 | * @param {Object} data - data object with params
9 | *
10 | * @return {String} - replaced url
11 | *
12 | * @throws {'url must be a string'} If argument url is not of type String
13 | *
14 | * @example
15 | * // returns 'http://site.com/view/42'
16 | * urlFromTemplate('http://site.com/{method}/{page.id}', {method: 'view', page: {id: 42}})
17 | *
18 | * // return 'http://site.com/42'
19 | * urlFromTemplate('http://site.com/{method}/{page.id}', {no_method: 'view', page: {id: 42}})
20 | */
21 | export function urlFromTemplate(url, data) {
22 |
23 | if ( typeof url !== 'string' ) {
24 | throw new Error('`url` must be a string, ' + typeof url + ' given')
25 | }
26 |
27 | const tokenRe = /([&?]?\{[\w\s\[\].]+})/g
28 | const isParamRe = /^([?&])+/
29 | const paramNameRe = /(\w+)/
30 |
31 | let isFirstParam = true
32 |
33 | // collect all tokens in template
34 | let tokens = url.match(tokenRe)
35 |
36 | if ( tokens && tokens.length ) {
37 |
38 | // replace tokens
39 | tokens.forEach( token => {
40 |
41 | let isParam = isParamRe.test(token)
42 | let prop = paramNameRe.exec(token)[0]
43 | let replacer = ''
44 |
45 | if ( isParam ) {
46 | let _mock = {}
47 | _mock[prop] = get(data, prop, '')
48 | replacer = (isFirstParam ? '?' : '&') + stringifyQuery(_mock)
49 | isFirstParam = false
50 | } else {
51 | replacer = get(data, prop, '')
52 | }
53 |
54 | url = url.replace(token, replacer)
55 | })
56 |
57 | // noramlize:
58 | // replace double slashes
59 | url = url.replace(/([^:]\/)\/+/g, '$1')
60 |
61 | }
62 |
63 | return url
64 | }
65 |
66 |
67 | /**
68 | * Creates a query string from given params
69 | * @param {Object} queryObj query params to stringify
70 | * @param {String} prefix parent name (for recursive calls)
71 | * @param {Array} str already built uri components (for recursive calls)
72 | * @param {Boolean} isArray add name in brackets or not (for recursive calls)
73 | *
74 | * @return {String} stringified query
75 | *
76 | * @example
77 | * // returns space=with%20space&obj%5Bnumber%5D=123&obj%5Barray%5D%5B%5D=1&obj%5Barray%5D%5B%5D=2&obj%5Barray%5D%5B%5D=3
78 | * // with decodeURIComponents you'll get space=with space&obj[number]=123&obj[array][]=1&obj[array][]=2&obj[array][]=3
79 | *
80 | * stringifyQuery({
81 | * space: "with space",
82 | * obj: {
83 | * number: 123,
84 | * array: [1, 2, 3]
85 | * }
86 | * })
87 | *
88 | */
89 | export function stringifyQuery(queryObj, prefix, str = [], isArray) {
90 |
91 | for ( let param in queryObj ) {
92 |
93 | // include only own properties
94 | if ( queryObj.hasOwnProperty(param) ) {
95 | // TODO: fix param.replace(/\[\]$/, ''), which is quick patch
96 | let _key = prefix ? prefix + "[" + (isArray ? '' : param) + "]" : /* param */param.replace(/\[\]$/, ''),
97 | _val = queryObj[param];
98 |
99 | if ( typeof _val === 'undefined' || _val === '' || _val === null ) {
100 | continue
101 | } else if ( isObject(_val) ) {
102 | stringifyQuery(_val, _key, str, Array.isArray(_val))
103 | } else {
104 | str.push( encodeURIComponent(_key) + "=" + encodeURIComponent(_val) )
105 | }
106 | }
107 | }
108 |
109 | return str.join('&')
110 | }
111 |
112 |
113 | /**
114 | * parses simple query string, no nested params name support
115 | *
116 | * @param {String} queryStr query to parse
117 | * @return {Object} name: value parsed params
118 | *
119 | */
120 |
121 | export function parseQuery(queryStr) {
122 |
123 | let { get, set, isEmpty } = AWES.utils.object,
124 | parsed = {};
125 |
126 | queryStr = queryStr.trim().replace(/^(\?|#|&)/, '')
127 |
128 | if ( isEmpty(queryStr) ) return parsed
129 |
130 | let params = queryStr.split('&')
131 |
132 | params.forEach( param => {
133 | let [ name, value ] = param.split('=')
134 | name = decodeURIComponent(name)
135 | value = decodeURIComponent(value)
136 |
137 | try {
138 | let _value = JSON.parse(value)
139 | value = _value
140 | } catch(e) {}
141 |
142 | // check for array of params
143 | if ( /\[\]$/.test(name) ) {
144 |
145 | name = name.replace(/\[\]$/, '')
146 |
147 | // check if param already exists
148 | let _arr = get(parsed, name)
149 |
150 | if ( _arr ) {
151 | _arr.push(value)
152 | } else {
153 | set(parsed, name, [value])
154 | }
155 |
156 | } else {
157 | set(parsed, name, value)
158 | }
159 | })
160 |
161 | return parsed
162 | }
--------------------------------------------------------------------------------
/resources/vue/components/__tests__/__snapshots__/calendar.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Calendar matches overall snapshot 1`] = `
4 | "
5 | Mo
6 |
7 | Tu
8 |
9 | We
10 |
11 | Th
12 |
13 | Fr
14 |
15 | Sa
16 |
17 | Su
18 |
"
103 | `;
104 |
--------------------------------------------------------------------------------