├── .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 | 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------