├── .npmrc ├── .gitignore ├── src ├── preview.png ├── lib │ ├── encoder.js │ ├── Daterangepickerlicense.txt │ ├── Momentjslicense.txt │ ├── daterangepicker.less │ └── daterangepicker.js ├── calendar-settings.spa.js ├── qlik-date-picker.js └── calendar-settings.js ├── test ├── aw.config.js ├── requirejs-main.js ├── index.html ├── requirejs-config.js ├── stubs │ └── qlik.stub.js └── unit │ └── qlik-date-picker.spec.js ├── scripts ├── get-version.sh ├── install-ghr.sh ├── create-release.sh ├── get-latest-version.sh ├── verify-files.sh └── bump-version.sh ├── SenseDateRangePickerLicense.txt ├── README.md ├── package.json ├── gulpfile.js └── .circleci └── config.yml /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /src/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NOD507/SenseDateRangePicker/HEAD/src/preview.png -------------------------------------------------------------------------------- /test/aw.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | url: 'http://localhost:9676/test/index.html', 3 | glob: ['test/unit/**/*.spec.js'] 4 | }; -------------------------------------------------------------------------------- /test/requirejs-main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function init() { // eslint-disable-line func-names 4 | function run() { 5 | requirejs(window.awFiles, function () { 6 | mocha.run(); 7 | }); 8 | } 9 | requirejs(['./requirejs-config'], run); 10 | })(); -------------------------------------------------------------------------------- /scripts/get-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | 4 | echo "$(cat package.json \ 5 | | grep version \ 6 | | head -1 \ 7 | | awk -F: '{ print $2 }' \ 8 | | sed 's/[",]//g' \ 9 | | tr -d '[[:space:]]')" 10 | 11 | # Usage 12 | # $ get-bumped-version.sh 13 | 14 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test date-picker 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scripts/install-ghr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o verbose 3 | 4 | URL="https://github.com/tcnksm/ghr/releases/download/v0.5.4/ghr_v0.5.4_linux_386.zip" 5 | echo "Version to install: $URL" 6 | 7 | echo "Installing ghr" 8 | curl -L ${URL} > ghr.zip 9 | mkdir -p "$HOME/bin" 10 | export PATH="$HOME/bin:$PATH" 11 | unzip ghr.zip -d "$HOME/bin" 12 | rm ghr.zip 13 | -------------------------------------------------------------------------------- /scripts/create-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | 4 | echo "Creating release for version: $VERSION" 5 | echo "Artifact name: ./dist/${3}_${VERSION}.zip" 6 | $HOME/bin/ghr -t ${ghoauth} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} v${VERSION} "./dist/${3}_${4}.zip" 7 | 8 | 9 | # Usage 10 | # $ create-release.sh qlik-oss qsSimpleKPI qlik-multi-kpi 0.3.1 11 | -------------------------------------------------------------------------------- /test/requirejs-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | requirejs.config({ 4 | baseUrl: './', 5 | paths: { 6 | angular: './../node_modules/angular/angular', 7 | chai: './../node_modules/chai/chai', 8 | jquery: './../node_modules/jquery/dist/jquery', 9 | qlik: './stubs/qlik.stub', 10 | css: './../node_modules/require-css/css' 11 | }, 12 | shim: { 13 | angular: { 14 | deps: ['jquery'], 15 | exports: 'angular' 16 | }, 17 | qlik: { 18 | deps: ['angular'] 19 | } 20 | } 21 | }); -------------------------------------------------------------------------------- /test/stubs/qlik.stub.js: -------------------------------------------------------------------------------- 1 | define([], function () { 2 | 'use strict'; 3 | 4 | return { 5 | currApp: function () { 6 | return { 7 | getAppObjectList: () => { 8 | 9 | }, 10 | getStoryList: () => { 11 | 12 | }, 13 | getList: () => {} 14 | }; 15 | }, 16 | getGlobal: () => { 17 | return { 18 | getAppList: () => { 19 | return Promise.resolve(); 20 | } 21 | }; 22 | } 23 | }; 24 | }); -------------------------------------------------------------------------------- /scripts/get-latest-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | 4 | VERSION=$(curl --silent "https://api.github.com/repos/$1/$2/releases/latest" | # Get latest release from GitHub api 5 | grep '"tag_name":' | # Get tag line 6 | sed -E 's/.*"([^"]+)".*/\1/') # Pluck JSON value 7 | 8 | if [ -z "${VERSION}" ]; then 9 | VERSION="2.3.0" 10 | fi 11 | 12 | echo "${VERSION}" 13 | 14 | ### Inspired by https://gist.github.com/lukechilds/a83e1d7127b78fef38c2914c4ececc3c 15 | # Usage 16 | # $ get-latest-version.sh qlik-oss qsSimpleKPI 17 | # 0.12.0 18 | -------------------------------------------------------------------------------- /scripts/verify-files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # The build script has a known race-condition that sometimes causes it to not include all files 3 | # in the built zip. This script verifies the that the zip contains the correct number of files. 4 | 5 | set -o errexit 6 | 7 | echo "Verifying built file count" 8 | 9 | while read line; do 10 | if [[ $line =~ ^\"name\": ]]; then 11 | name=${line#*: \"} 12 | name=${name%\"*} 13 | fi 14 | done < package.json 15 | 16 | expected_file_count=$(($(find dist -type f | wc -l)-1)) 17 | zip_file_count=$(zipinfo dist/${name}_${VERSION}.zip | grep ^- | wc -l) 18 | 19 | if [ "${expected_file_count}" -ne "${zip_file_count}" ]; then 20 | # File count is incorrect 21 | echo "Expected file count ${expected_file_count}, but was ${zip_file_count}" 22 | exit 1 23 | fi 24 | echo "File count OK" 25 | exit 0 26 | -------------------------------------------------------------------------------- /scripts/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | 4 | join_by () { 5 | local IFS="$1"; shift; echo "$*"; 6 | } 7 | 8 | if [ "${CIRCLE_BRANCH}" == "qlik-date-picker" ]; then 9 | # get version from repo 10 | OLD_VERSION="$(scripts/get-latest-version.sh $1 $2)" 11 | echo "Latest GitHub release version: ${OLD_VERSION}" 12 | 13 | # split into array 14 | IFS='.' read -ra ARRAY_VERSION <<< "$OLD_VERSION" 15 | 16 | # bump minor 17 | ARRAY_VERSION[1]=$((ARRAY_VERSION[1]+1)) 18 | 19 | # join into string 20 | NEW_VERSION=$(join_by . ${ARRAY_VERSION[@]}) 21 | elif [[ ! -z "${CIRCLE_BRANCH}" && ! -z "${CIRCLE_BUILD_NUM}" ]]; then 22 | NEW_VERSION="$(echo ${CIRCLE_BRANCH} | sed -e 's/\//-/g')_${CIRCLE_BUILD_NUM}" 23 | else 24 | NEW_VERSION="dev" 25 | fi 26 | 27 | echo "Bumped version: ${NEW_VERSION}" 28 | echo "${NEW_VERSION}" > BUMPED_VERSION 29 | 30 | 31 | # Usage 32 | # $ bump-version.sh qlik-oss qsSimpleKPI 33 | -------------------------------------------------------------------------------- /src/lib/encoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010 - The OWASP Foundation 3 | *

4 | * The jquery-encoder is published by OWASP under the MIT license. 5 | *

6 | * This is encoder builds on jquery-encoder. It is refactored to suit the need for the QlikView Application. 7 | * Since it no longer has any dependencies to jQuery it now called 8 | * encoder 9 | */ 10 | define([], function() { 11 | var encoder = { 12 | encodeForHTML: function (input) { 13 | /** 14 | * Encodes input for use in HTML context 15 | */ 16 | if (input === undefined) { 17 | return ''; 18 | } 19 | let encoded = '', 20 | encodingDiv = document.createElement('div'); 21 | const textNode = document.createTextNode(input); 22 | encodingDiv.appendChild(textNode); 23 | encoded = encodingDiv.innerHTML; 24 | encodingDiv.removeChild(textNode); 25 | return encoded; 26 | } 27 | }; 28 | return encoder; 29 | }) -------------------------------------------------------------------------------- /SenseDateRangePickerLicense.txt: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright 2018 Nodier Torres 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/lib/Daterangepickerlicense.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2018 Dan Grossman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/lib/Momentjslicense.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SenseDateRangePicker 2 | This extension is part of the extension bundles for Qlik Sense. The repository is maintained and moderated by Qlik RD. 3 | 4 | Feel free to fork and suggest pull requests for improvements and bug fixes. Changes will be moderated and reviewed before inclusion in future bundle versions. Please note that emphasis is on backward compatibility, i.e. breaking changes will most likely not be approved. 5 | 6 | Usage documentation for the extension is available at https://help.qlik.com. 7 | 8 | # Developing the extension 9 | If you want to do code changes to the extension follow these simple steps to get going. 10 | 11 | 1. Get Qlik Sense Desktop 12 | 1. Create a new app and add Date picker to a sheet. 13 | 2. Clone the repository 14 | 3. Run `yarn install` 15 | 4. Run `npm run build` - to build a dev-version to the /dist folder. 16 | 5. Move the content of the /dist folder to the extension directory. Usually in `C:/Users//Documents/Qlik/Sense/Extensions/qlik-date-picker`. 17 | 18 | ## Release 19 | 1. `git checkout master && git pull` to make sure you're up to date. 20 | 2. `npm version ` 21 | 3. `git push && git push --tags` 22 | 23 | # Original Author 24 | [NOD507](https://github.com/NOD507) 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qlik-date-picker", 3 | "version": "2.28.0", 4 | "description": "A calendar object that allows a user to make selections in a date field.", 5 | "private": true, 6 | "homepage": "http://branch.qlik.com/#/project/5697a878dcc497f80ed514bf", 7 | "repository": "https://github.com/qlik-oss/SenseDateRangePicker/tree/qlik-date-picker", 8 | "author": "Nodier Torres", 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "gulp build", 12 | "build:zip": "gulp zip", 13 | "test:unit": "aw chrome -c ./test/aw.config.js" 14 | }, 15 | "devDependencies": { 16 | "@after-work.js/aw": "6.0.11", 17 | "angular": "1.7.2", 18 | "@babel/cli": "7.8.4", 19 | "@babel/core": "7.8.4", 20 | "babel-plugin-istanbul": "6.0.0", 21 | "del": "^2.0.2", 22 | "gulp": "4.0.0", 23 | "gulp-cssnano": "^2.1.0", 24 | "gulp-dest": "^0.2.3", 25 | "gulp-ignore": "^2.0.1", 26 | "gulp-less": "^3.0.3", 27 | "gulp-replace": "^0.5.4", 28 | "gulp-terser": "1.2.0", 29 | "gulp-util": "^3.0.7", 30 | "gulp-zip": "^3.0.2", 31 | "jquery": "^3.3.1", 32 | "less-plugin-autoprefix": "^1.5.1", 33 | "require-css": "^0.1.10", 34 | "requirejs": "^2.3.6", 35 | "uglify-save-license": "^0.4.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* global require process Buffer */ 2 | var gulp = require('gulp'); 3 | var cssnano = require('gulp-cssnano'); 4 | var gutil = require('gulp-util'); 5 | const terser = require('gulp-terser'); 6 | var saveLicense = require('uglify-save-license'); 7 | var pkg = require('./package.json'); 8 | var zip = require('gulp-zip'); 9 | 10 | var DIST = './dist', 11 | SRC = './src', 12 | NAME = pkg.name, 13 | VERSION = process.env.VERSION || 'local-dev'; 14 | 15 | gulp.task('qext', function () { 16 | var qext = { 17 | name: 'Date picker', 18 | type: 'visualization', 19 | description: pkg.description + '\nVersion: ' + VERSION, 20 | version: VERSION, 21 | icon: 'calendar', 22 | preview: 'preview.png', 23 | keywords: 'qlik-sense, visualization', 24 | author: pkg.author, 25 | homepage: pkg.homepage, 26 | license: pkg.license, 27 | repository: pkg.repository, 28 | dependencies: { 29 | 'qlik-sense': '>=5.5.x' 30 | } 31 | }; 32 | if (pkg.contributors) { 33 | qext.contributors = pkg.contributors; 34 | } 35 | var src = require('stream').Readable({ 36 | objectMode: true 37 | }); 38 | src._read = function () { 39 | this.push(new gutil.File({ 40 | cwd: '', 41 | base: '', 42 | path: NAME + '.qext', 43 | contents: Buffer.from(JSON.stringify(qext, null, 4)) 44 | })); 45 | this.push(null); 46 | }; 47 | return src.pipe(gulp.dest(DIST)); 48 | }); 49 | 50 | gulp.task('clean', function (ready) { 51 | var del = require('del'); 52 | del.sync([DIST]); 53 | ready(); 54 | }); 55 | 56 | gulp.task('less', function () { 57 | var less = require('gulp-less'); 58 | var LessPluginAutoPrefix = require('less-plugin-autoprefix'); 59 | var autoprefix = new LessPluginAutoPrefix({ 60 | browsers: ['last 2 versions'] 61 | }); 62 | return gulp.src(SRC + '/**/*.less') 63 | .pipe(less({ 64 | plugins: [autoprefix] 65 | })) 66 | .pipe(cssnano()) 67 | .pipe(gulp.dest(DIST)); 68 | }); 69 | 70 | gulp.task('add-assets', function () { 71 | return gulp.src([ 72 | SRC + '/**/*.png', 73 | SRC + '/**/*.txt' 74 | ]) 75 | .pipe(gulp.dest(DIST)); 76 | }); 77 | 78 | gulp.task('add-src', function () { 79 | return gulp.src(SRC + '/**/*.js') 80 | .pipe(terser({ 81 | mangle: false, 82 | output: { 83 | comments: saveLicense 84 | } 85 | })) 86 | .pipe(gulp.dest(DIST)); 87 | }); 88 | 89 | gulp.task('zip-build', function () { 90 | return gulp.src(DIST + '/**/*') 91 | .pipe(zip(`${NAME}_${VERSION}.zip`)) 92 | .pipe(gulp.dest(DIST)); 93 | }); 94 | 95 | gulp.task('build', 96 | gulp.series('clean', 'qext', 'less', 'add-assets', 'add-src') 97 | ); 98 | 99 | gulp.task('zip', 100 | gulp.series('build', 'zip-build') 101 | ); 102 | 103 | gulp.task('default', 104 | gulp.series('build') 105 | ); 106 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/qlik-date-picker 5 | docker: 6 | - image: circleci/node:stretch 7 | environment: 8 | GITHUB_ORG: "qlik-oss" 9 | GITHUB_REPO: "SenseDateRangePicker" 10 | PACKAGE_NAME: "qlik-date-picker" 11 | 12 | jobs: 13 | build: 14 | <<: *defaults 15 | steps: 16 | - checkout 17 | - attach_workspace: 18 | at: ~/qlik-date-picker 19 | - run: 20 | name: Install dependencies 21 | command: yarn install 22 | - run: 23 | name: Build and package 24 | command: | 25 | sudo chmod +x scripts/get-version.sh 26 | export VERSION=$(scripts/get-version.sh) 27 | echo "Version: $VERSION" 28 | npm run build:zip 29 | environment: 30 | NODE_ENV: production 31 | - persist_to_workspace: 32 | root: ~/qlik-date-picker 33 | paths: 34 | - dist 35 | - store_artifacts: 36 | path: dist 37 | destination: dist 38 | blackduck: 39 | docker: 40 | - image: circleci/node:stretch-browsers 41 | steps: 42 | - checkout 43 | - run: 44 | name: Install dependencies 45 | command: yarn install 46 | - run: 47 | name: BlackDuck scan 48 | command: curl -s https://detect.synopsys.com/detect.sh | bash -s -- \ 49 | --blackduck.url="https://qliktech.blackducksoftware.com" \ 50 | --blackduck.trust.cert=true \ 51 | --blackduck.username="svc-blackduck" \ 52 | --blackduck.password=${svc_blackduck} \ 53 | --detect.project.name="SenseDateRangePicker" 54 | deploy: 55 | <<: *defaults 56 | steps: 57 | - checkout 58 | - attach_workspace: 59 | at: ~/qlik-date-picker 60 | - run: 61 | name: Install ghr 62 | command: | 63 | sudo chmod +x scripts/install-ghr.sh 64 | scripts/install-ghr.sh 65 | - run: 66 | name: Create GitHub Release 67 | command: | 68 | sudo chmod +x scripts/get-version.sh 69 | export VERSION=$(scripts/get-version.sh) 70 | echo "Version: $VERSION" 71 | sudo chmod +x scripts/create-release.sh 72 | scripts/create-release.sh $GITHUB_ORG $GITHUB_REPO $PACKAGE_NAME $VERSION 73 | 74 | workflows: 75 | version: 2 76 | master_flow: 77 | jobs: 78 | - build: 79 | filters: # Required since jobs that requires this uses tag-filter 80 | tags: 81 | only: /.*/ 82 | - blackduck: 83 | filters: # Required since jobs that requires this uses tag-filter 84 | tags: 85 | only: /.*/ 86 | - deploy: 87 | requires: 88 | - build 89 | - blackduck 90 | filters: 91 | tags: 92 | only: 93 | - /^v.*/ 94 | branches: 95 | # Have to ignore branches to make the tags-filter work 96 | ignore: /.*/ 97 | -------------------------------------------------------------------------------- /src/calendar-settings.spa.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | var CalendarSettings = { 3 | component: "expandable-items", 4 | label: "Calendar Settings", 5 | items: { 6 | Options: { 7 | type: "items", 8 | label: "Options", 9 | items:{ 10 | SingleDateSwitch: { 11 | type: "boolean", 12 | component: "switch", 13 | label: "Calendario de una fecha", 14 | ref: "props.isSingleDate", 15 | options: [{ 16 | value: true, 17 | translation: "properties.on" 18 | }, { 19 | value: false, 20 | translation: "properties.off" 21 | }], 22 | defaultValue: false 23 | }, 24 | CustomRangesSwitch:{ 25 | type: "boolean", 26 | component: "switch", 27 | label: "Activar rangos", 28 | ref: "props.CustomRangesEnabled", 29 | options: [{ 30 | value: true, 31 | translation: "properties.on" 32 | }, { 33 | value: false, 34 | translation: "properties.off" 35 | }], 36 | defaultValue: true 37 | 38 | } 39 | 40 | } 41 | }, 42 | header1: { 43 | type: "items", 44 | label: "Language and labels", 45 | items: { 46 | Language:{ 47 | type: "string", 48 | ref: "props.locale", 49 | label: "Locale", 50 | defaultValue: "es" 51 | }, 52 | Format:{ 53 | type: "string", 54 | ref: "props.format", 55 | label: "Format", 56 | defaultValue: "DD/MM/YYYY" 57 | }, 58 | Separator:{ 59 | type: "string", 60 | ref: "props.separator", 61 | label: "Separator", 62 | defaultValue: " - " 63 | }, 64 | CustomRange:{ 65 | type: "string", 66 | ref: "props.customRangeLabel", 67 | label: "Custom Range", 68 | defaultValue: "Rango" 69 | }, 70 | defaultText: { 71 | type: "string", 72 | ref: "props.defaultText", 73 | label: "Default Text", 74 | defaultValue: "Seleccione un rango" 75 | }, 76 | Today:{ 77 | type: "string", 78 | ref: "props.today", 79 | label: "Today", 80 | defaultValue: "Hoy" 81 | }, 82 | Yesterday:{ 83 | type: "string", 84 | ref: "props.yesterday", 85 | label: "Yesterday", 86 | defaultValue: "Ayer" 87 | }, 88 | LastDays:{ 89 | type: "string", 90 | ref: "props.lastXDays", 91 | label: "Last $ days", 92 | defaultValue: "Últimos $ días" 93 | }, 94 | ThisMonth:{ 95 | type: "string", 96 | ref: "props.thisMonth", 97 | label: "This Month", 98 | defaultValue: "Mes Actual" 99 | }, 100 | LastMonth:{ 101 | type: "string", 102 | ref: "props.lastMonth", 103 | label: "Last Month", 104 | defaultValue: "Mes Anterior" 105 | } 106 | 107 | } 108 | } 109 | } 110 | }; 111 | return CalendarSettings; 112 | }) -------------------------------------------------------------------------------- /test/unit/qlik-date-picker.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['chai', 4 | '../../../src/calendar-settings', 5 | '../../../src/qlik-date-picker'], 6 | function (chai, calendarSettings, dateRangePicker) { 7 | const expect = chai.expect; 8 | 9 | describe('properties', function () { 10 | it('should have items', function () { 11 | expect(calendarSettings).to.have.a.property('items'); 12 | }); 13 | }); 14 | describe('dateRangePicker', function () { 15 | it('should have a paint method', function () { 16 | expect(dateRangePicker).to.have.a.property('paint'); 17 | }); 18 | }); 19 | describe('getFieldName', function () { 20 | it('should strip leading =', function () { 21 | var result = dateRangePicker.methods.getFieldName("=XXX"); 22 | expect(result).to.equal('XXX'); 23 | }); 24 | }); 25 | describe('createDate', function () { 26 | it('should convert a day number to a date string', function () { 27 | var result = dateRangePicker.methods.createDate(41276); 28 | expect(result).to.equal('20130102'); 29 | }); 30 | }); 31 | describe('createMoment', function () { 32 | it('should create a Moment object from a day number', function () { 33 | var result = dateRangePicker.methods.createMoment(41276); 34 | expect(result.format).to.be.a('function'); 35 | }); 36 | }); 37 | describe('createRanges', function () { 38 | it('should create a ranges object', function () { 39 | var result = dateRangePicker.methods.createRanges({ 40 | today: 'Idag', 41 | yesterday: 'Igår', 42 | lastXDays: 'Senaste $ dagarna', 43 | thisMonth: 'Denna månad', 44 | lastMonth: 'Föregående månad' 45 | }); 46 | expect(result).to.have.a.property('Idag'); 47 | }); 48 | }); 49 | describe('createDateStates', function () { 50 | it('should map date states', function () { 51 | var testData = [{ 52 | "qMatrix": [[{ "qText": "1/2/2013", "qNum": 41276, "qElemNumber": 132, "qState": "A" }], 53 | [{ "qText": "1/3/2013", "qNum": 41277, "qElemNumber": 499, "qState": "A" }], 54 | [{ "qText": "1/4/2013", "qNum": 41278, "qElemNumber": 501, "qState": "A" }], 55 | [{ "qText": "1/5/2013", "qNum": 41279, "qElemNumber": 502, "qState": "A" }], 56 | [{ "qText": "1/6/2013", "qNum": 41280, "qElemNumber": 500, "qState": "A" }], 57 | [{ "qText": "1/7/2013", "qNum": 41281, "qElemNumber": 130, "qState": "A" }], 58 | [{ "qText": "1/8/2013", "qNum": 41282, "qElemNumber": 142, "qState": "A" }], 59 | [{ "qText": "1/9/2013", "qNum": 41283, "qElemNumber": 335, "qState": "A" }], 60 | [{ "qText": "1/10/2013", "qNum": 41284, "qElemNumber": 433, "qState": "A" }], 61 | [{ "qText": "1/11/2013", "qNum": 41285, "qElemNumber": 414, "qState": "A" }], 62 | [{ "qText": "1/14/2013", "qNum": 41288, "qElemNumber": 406, "qState": "S", "qFrequency": "211" }], 63 | [{ "qText": "1/15/2013", "qNum": 41289, "qElemNumber": 394, "qState": "S", "qFrequency": "265" }], 64 | [{ "qText": "1/16/2013", "qNum": 41290, "qElemNumber": 386, "qState": "S", "qFrequency": "177" }], 65 | [{ "qText": "1/17/2013", "qNum": 41291, "qElemNumber": 348, "qState": "S", "qFrequency": "350" }], 66 | [{ "qText": "1/18/2013", "qNum": 41292, "qElemNumber": 108, "qState": "S", "qFrequency": "327" }], 67 | [{ "qText": "1/21/2013", "qNum": 41295, "qElemNumber": 99, "qState": "A" }], 68 | [{ "qText": "1/22/2013", "qNum": 41296, "qElemNumber": 365, "qState": "A" }], 69 | [{ "qText": "1/23/2013", "qNum": 41297, "qElemNumber": 34, "qState": "A" }], 70 | [{ "qText": "1/24/2013", "qNum": 41298, "qElemNumber": 426, "qState": "A" }]], 71 | "qTails": [], 72 | "qArea": { "qLeft": 0, "qTop": 0, "qWidth": 1, "qHeight": 505 } 73 | }]; 74 | var result = dateRangePicker.methods.createDateStates(testData); 75 | expect(result).to.have.a.property('rangeStart'); 76 | expect(result.rangeStart).to.be.equals(41288); 77 | expect(result['20130114']).to.be.equal('S'); 78 | }); 79 | }); 80 | describe('createHtml', function () { 81 | it('should use defaultText if no range is available', function () { 82 | var defaultText = 'DEFAULT TEXT'; 83 | var result = dateRangePicker.methods.createHtml({}, 'YYYY-MM-DD', 84 | { separator: '-', defaultText: defaultText }); 85 | expect(result).to.include(defaultText); 86 | }); 87 | }); 88 | 89 | }); -------------------------------------------------------------------------------- /src/lib/daterangepicker.less: -------------------------------------------------------------------------------- 1 | /** 2 | * A stylesheet for use with Bootstrap 3.x 3 | * @author: Dan Grossman http://www.dangrossman.info/ 4 | * @copyright: Copyright (c) 2012-2015 Dan Grossman. All rights reserved. 5 | * @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php 6 | * @website: https://www.improvely.com/ 7 | */ 8 | 9 | /* Nodier 10 | changed left and right rule names to prevent clash with sense client css */ 11 | 12 | #grid .qv-gridcell.zoom, .sheet-list #grid .qv-gridcell.zoom { 13 | z-index: 5000!important; 14 | } 15 | 16 | .qv-mode-edit .qv-object-qlik-date-picker .qv-object-content-container { 17 | div.requirements span { 18 | text-align: center; 19 | } 20 | } 21 | 22 | .qv-object-qlik-date-picker { 23 | .show-range { 24 | background: #fff; 25 | cursor: pointer; 26 | padding: 0px 0px; 27 | border: 0px solid #ccc; 28 | display: inline; 29 | } 30 | 31 | .lui-icon--expand, .lui-icon--camera { 32 | display: none; 33 | } 34 | } 35 | 36 | /* Container Appearance */ 37 | .qlik-daterangepicker { 38 | position: absolute; 39 | color: #595959; 40 | background: #fff; 41 | border-radius: 3px; 42 | border: 1px solid transparent; 43 | border-color: gray; 44 | width: 278px; 45 | min-width: 250px; 46 | margin: auto; 47 | margin-top: 1px; 48 | top: 100px; 49 | left: 20px; 50 | transition: opacity .2s ease-out; 51 | opacity: 1; 52 | z-index: 1021; 53 | box-shadow: 0 1px 2px rgba(0,0,0,.4); 54 | 55 | * { 56 | box-sizing: border-box; 57 | } 58 | 59 | /* Calendars */ 60 | &:before, &:after { 61 | position: absolute; 62 | display: inline-block; 63 | border-bottom-color: rgba(0, 0, 0, 0.2); 64 | content: ''; 65 | } 66 | &:before { 67 | top: -7px; 68 | border-right: 7px solid transparent; 69 | border-left: 7px solid transparent; 70 | border-bottom: 7px solid #ccc; 71 | } 72 | &:after { 73 | top: -6px; 74 | border-right: 6px solid transparent; 75 | border-bottom: 6px solid #fff; 76 | border-left: 6px solid transparent; 77 | } 78 | 79 | &.opensleft { 80 | &:before { 81 | right: 9px; 82 | } 83 | &:after { 84 | right: 10px; 85 | } 86 | } 87 | 88 | &.openscenter { 89 | &:before, &:after { 90 | left: 0; 91 | right: 0; 92 | width: 0; 93 | margin-left: auto; 94 | margin-right: auto; 95 | } 96 | } 97 | 98 | &.opensright { 99 | &:before { 100 | left: 44px; 101 | } 102 | &:after { 103 | left: 45px; 104 | } 105 | } 106 | 107 | &.dropup { 108 | margin-top: -5px; 109 | 110 | &:before { 111 | top: initial; 112 | bottom: -7px; 113 | border-bottom: initial; 114 | border-top: 7px solid #ccc; 115 | } 116 | &:after { 117 | top: initial; 118 | bottom: -6px; 119 | border-bottom: initial; 120 | border-top: 6px solid #fff; 121 | } 122 | } 123 | 124 | &.dropdown-menu { 125 | max-width: none; 126 | height: 310px; 127 | z-index: 5000!important; 128 | } 129 | 130 | &.single .ranges, &.single .calendar { 131 | float: none; 132 | } 133 | 134 | &.show-calendar .calendar { 135 | display: block; 136 | } 137 | 138 | .error_nodata { 139 | margin-left: 100px; 140 | font-size: 13px; 141 | font-weight: 600; 142 | margin-left: 100px; 143 | margin-top: 4px; 144 | } 145 | 146 | .calendar { 147 | display: none; 148 | max-width: 270px; 149 | margin: 8px; 150 | 151 | &.single .calendar-table { 152 | border: none; 153 | } 154 | 155 | th, td { 156 | white-space: nowrap; 157 | text-align: center; 158 | min-width: 32px; 159 | } 160 | 161 | th { 162 | opacity: 0.6; 163 | } 164 | 165 | &.active { 166 | th { 167 | opacity: 1; 168 | } 169 | .input-mini { 170 | border: 1px solid #52a2cc; 171 | transition: none; 172 | box-shadow: 0 0 1px 1px rgba(82,162,204,.7) 173 | } 174 | } 175 | } 176 | 177 | .calendar-table { 178 | border: 1px solid #fff; 179 | padding: 4px; 180 | border-radius: 4px; 181 | background: #fff; 182 | } 183 | 184 | table { 185 | width: 100%; 186 | margin: 0; 187 | background-color: transparent; 188 | border-collapse: collapse; 189 | border-spacing: 0; 190 | } 191 | 192 | td, th { 193 | text-align: center; 194 | width: 22px; 195 | height: 22px; 196 | padding: 5px; 197 | white-space: nowrap; 198 | cursor: pointer; 199 | } 200 | td.available .btn:hover, th.available .btn:hover { 201 | background-color: rgba(0,0,0,.05); 202 | } 203 | td.week, th.week { 204 | font-size: 80%; 205 | color: #ccc; 206 | } 207 | td.off, td.off.in-range, td.off.start-date, td.off.end-date { 208 | background-color: #fff; 209 | color: #999; 210 | } 211 | th.month, th.next, th.prev { 212 | font-size: 14px; 213 | line-height: 1.4; 214 | } 215 | th.month { 216 | width: auto; 217 | } 218 | th.next, th.prev { 219 | width: 35px; 220 | height:35px; 221 | opacity: 1; 222 | } 223 | th.next.disabled, th.prev.disabled { 224 | opacity: 0.6; 225 | cursor:default; 226 | } 227 | td.disabled, option.disabled { 228 | color: #999; 229 | cursor: default; 230 | } 231 | td.nodata { 232 | color: #999; 233 | } 234 | td.available:hover{ 235 | background-color: rgba(0,0,0,.05); 236 | } 237 | td.stateS, td.stateL { 238 | background-color: rgb(117,214,117); 239 | color: #fff; 240 | } 241 | td.available.stateS:hover, td.available.stateL:hover { 242 | background-color: rgba(117,214,117,0.7); 243 | } 244 | &.in-selection td.stateS, td.stateA { 245 | background-color: #dedede; 246 | } 247 | td.stateX { 248 | background-color: #a8a8a8; 249 | } 250 | td.stateO { 251 | background-color: #fff; 252 | font-weight: 600; 253 | } 254 | &.in-selection { 255 | td.start-date, td.end-date, td.available:hover { 256 | background-color: #51cc52; 257 | box-shadow: inset 0px -2px 0px 0px rgba(0,0,0,0.1); 258 | color: #fff; 259 | } 260 | td.in-range { 261 | color: #fff; 262 | background-color: #6bd36c; 263 | } 264 | } 265 | td.start-date { 266 | border-radius: 4px 0 0 4px; 267 | } 268 | td.end-date { 269 | border-radius: 0 4px 4px 0; 270 | } 271 | td.start-date.end-date { 272 | border-radius: 4px; 273 | } 274 | td.empty { 275 | background-color: #fff; 276 | color:#fff; 277 | cursor: default; 278 | pointer-events: none; 279 | } 280 | 281 | select.monthselect, select.yearselect { 282 | font-size: 12px; 283 | padding: 1px; 284 | height: auto; 285 | margin: 0; 286 | cursor: default; 287 | } 288 | select.monthselect { 289 | margin-right: 2%; 290 | width: 56%; 291 | } 292 | select.yearselect { 293 | width: 40%; 294 | } 295 | select.hourselect, select.minuteselect, select.secondselect, select.ampmselect { 296 | width: 50px; 297 | margin-bottom: 0; 298 | } 299 | 300 | .input-mini { 301 | border: 1px solid #ccc; 302 | border-radius: 4px; 303 | color: #555; 304 | height: 30px; 305 | line-height: 30px; 306 | display: block; 307 | vertical-align: middle; 308 | margin: 0 0 5px 0; 309 | padding: 0 6px 0 28px; 310 | width: 100%; 311 | } 312 | 313 | .qlik-daterangepicker_input { 314 | position: relative; 315 | i { 316 | position: absolute; 317 | left: 8px; 318 | top: 8px; 319 | } 320 | } 321 | 322 | .calendar-time { 323 | text-align: center; 324 | margin: 5px auto; 325 | line-height: 30px; 326 | position: relative; 327 | padding-left: 28px; 328 | select.disabled { 329 | color: #ccc; 330 | cursor: default; 331 | } 332 | } 333 | 334 | .ranges { 335 | height: 278px; 336 | font-size: 11px; 337 | background-color: rgb(250,250,250); 338 | color: rgb(107,107,107); 339 | float: none; 340 | text-align: left; 341 | .header { 342 | font-size: 13px; 343 | font-weight: bold; 344 | margin-left: 8px; 345 | margin-top: 4px; 346 | } 347 | ul { 348 | list-style: none; 349 | padding: 0; 350 | margin-top: 12px; 351 | padding-bottom: 8px; 352 | } 353 | li { 354 | font-size: 13px; 355 | font-weight: bold; 356 | background: rgb(237,237,237); 357 | border: 1px solid transparent; 358 | border-radius: 4px; 359 | color: rgb(101,101,101); 360 | padding: 5px 12px; 361 | margin-bottom: 6px; 362 | cursor: pointer; 363 | &.disabled { 364 | font-weight:normal; 365 | background-color: transparent; 366 | color: rgb(107,107,107); 367 | pointer-events:none; 368 | cursor:default; 369 | } 370 | } 371 | } 372 | 373 | .btn { 374 | display: inline-block; 375 | margin-bottom: 0; 376 | font-weight: normal; 377 | text-align: center; 378 | vertical-align: middle; 379 | touch-action: manipulation; 380 | cursor: pointer; 381 | background-image: none; 382 | border: 1px solid transparent; 383 | white-space: nowrap; 384 | } 385 | .btn-default { 386 | color: #333; 387 | background-color: #fff; 388 | border-color: #ccc; 389 | } 390 | .btn-xs{ 391 | padding: 1px 8px; 392 | font-size: 12px; 393 | line-height: 1.5; 394 | border-radius: 3px; 395 | } 396 | .disabled .btn{ 397 | color: #aaa; 398 | pointer-events: none; 399 | cursor: default; 400 | } 401 | 402 | .hide{ 403 | display:none; 404 | } 405 | } 406 | /*Small- device scrollbar */ 407 | @media screen and (max-width: 768px), screen and (max-height: 480px) { 408 | .qlik-daterangepicker { 409 | overflow-y: auto; 410 | 411 | .error_nodata { 412 | margin-left: 10px; 413 | } 414 | } 415 | } 416 | /* Larger Screen Styling */ 417 | @media (min-width: 564px) { 418 | .qlik-daterangepicker { 419 | width: auto; 420 | 421 | .ranges, .calendar { 422 | float: left; 423 | } 424 | 425 | .ranges ul { 426 | width: 160px; 427 | } 428 | 429 | &.single { 430 | .ranges, .calendar { 431 | float: left; 432 | } 433 | .ranges ul { 434 | width: 100%; 435 | } 436 | .calendar.dpleft { 437 | clear: none; 438 | } 439 | } 440 | 441 | .calendar { 442 | &.dpleft { 443 | clear: left; 444 | margin-right: 0; 445 | .calendar-table { 446 | border-right: none; 447 | border-top-right-radius: 0; 448 | border-bottom-right-radius: 0; 449 | padding-right: 12px; 450 | } 451 | } 452 | &.dpright { 453 | margin-left: 0; 454 | .calendar-table { 455 | border-left: none; 456 | border-top-left-radius: 0; 457 | border-bottom-left-radius: 0; 458 | } 459 | } 460 | } 461 | 462 | .dpleft .qlik-daterangepicker_input { 463 | padding-right: 12px; 464 | } 465 | } 466 | } 467 | 468 | @media (min-width: 730px) { 469 | .qlik-daterangepicker { 470 | .ranges { 471 | width: auto; 472 | float: left; 473 | } 474 | 475 | .calendar.dpleft { 476 | clear: none; 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /src/qlik-date-picker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Nodier Torres. All rights reserved. 4 | * 5 | * Copyrights licensed under the terms of the MIT license. 6 | * Original source 7 | */ 8 | define(["qlik", "jquery", "./lib/moment.min", "./calendar-settings", "./lib/encoder", "css!./lib/daterangepicker.css", "./lib/daterangepicker" 9 | ], 10 | function (qlik, $, moment, CalendarSettings, encoder) { 11 | 'use strict'; 12 | function createDate(num) { 13 | return moment((num - 25569) * 86400 * 1000).utc().format("YYYYMMDD").toString(); 14 | } 15 | function createMoment(str, format) { 16 | if (isNaN(str)) { 17 | return moment.utc(str, format); 18 | } 19 | return moment.utc(createDate(str), 'YYYYMMDD'); 20 | } 21 | function isQlikCloud(){ 22 | const qlikCloudRegEx = /\.(qlik-stage|qlikcloud)\.com/; 23 | const matcher = window.location.hostname.match(qlikCloudRegEx) || []; 24 | return matcher.length; 25 | } 26 | function createRanges(props) { 27 | var ranges = {}; 28 | if( !isQlikCloud() ) { 29 | var numberOf = props.numberOf, 30 | includeCurrent = props.previousOrLast; 31 | if(props.this == undefined) { 32 | props.this = "m"; 33 | } 34 | if(props.thisLabel == undefined) { 35 | props.thisLabel = props.thisMonth; 36 | } 37 | if(props.last == undefined) { 38 | props.last = "m"; 39 | } 40 | if(props.lastLabel == undefined) { 41 | props.lastLabel = props.lastMonth; 42 | } 43 | if(numberOf == undefined) { 44 | numberOf = 1; 45 | } 46 | if(includeCurrent == undefined) { 47 | includeCurrent = false; 48 | } 49 | } 50 | ranges[props.today] = [moment().startOf('day'), moment().startOf('day')]; 51 | ranges[props.yesterday] = [moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').startOf('day')]; 52 | ranges[props.lastXDays.replace("$", "7")] = [moment().subtract(6, 'days').startOf('day'), moment().startOf('day')]; 53 | ranges[props.lastXDays.replace("$", "30")] = [moment().subtract(29, 'days').startOf('day'), moment().startOf('day')]; 54 | if( isQlikCloud() ) { 55 | ranges[props.thisMonth] = [moment().startOf('month').startOf('day'), moment().endOf('month').startOf('day')]; 56 | ranges[props.lastMonth] = [moment().subtract(1, 'month').startOf('month').startOf('day'), moment().subtract(1, 'month').endOf('month').startOf('day')]; 57 | } else { 58 | if (props.this === "d") { 59 | ranges[props.thisLabel] = [moment().startOf('day'), moment().startOf('day')]; 60 | } else if (props.this === "m") { 61 | ranges[props.thisLabel] = [moment().startOf('month').startOf('day'), moment().endOf('month').startOf('day')]; 62 | } else if (props.this === "q") { 63 | ranges[props.thisLabel] = [moment().startOf('quarter').startOf('day'), moment().endOf('quarter').startOf('day')]; 64 | } else if (props.this === "y") { 65 | ranges[props.thisLabel] = [moment().startOf('year').startOf('day'), moment().endOf('year').startOf('day')]; 66 | } 67 | if(!includeCurrent) { 68 | if (props.last === "d") { 69 | ranges[props.lastLabel] = [moment().subtract(numberOf, 'days').startOf('day'), moment().subtract(1, 'days').startOf('day')]; 70 | } else if (props.last === "m") { 71 | ranges[props.lastLabel] = [moment().subtract(numberOf, 'months').startOf('month').startOf('day'), moment().subtract(1, 'months').endOf('month').startOf('day')]; 72 | } else if (props.last === "q") { 73 | ranges[props.lastLabel] = [moment().subtract(numberOf,'quarters').startOf('quarter').startOf('day'), moment().subtract(1, 'quarters').endOf('quarter').startOf('day')]; 74 | } else if (props.last === "y") { 75 | ranges[props.lastLabel] = [moment().subtract(numberOf,'years').startOf('year').startOf('day'), moment().subtract(1,'years').endOf('year').startOf('day')]; 76 | } 77 | } else { 78 | if (props.last === "d") { 79 | ranges[props.lastLabel] = [moment().subtract(numberOf -1, 'days').startOf('day'), moment().startOf('day')]; 80 | } else if (props.last === "m") { 81 | ranges[props.lastLabel] = [moment().subtract(numberOf - 1, 'months').startOf('month').startOf('day'), moment().endOf('month').startOf('day')]; 82 | } else if (props.last === "q") { 83 | ranges[props.lastLabel] = [moment().subtract(numberOf -1,'quarters').startOf('quarter').startOf('day'), moment().endOf('quarter').startOf('day')]; 84 | } else if (props.last === "y") { 85 | ranges[props.lastLabel] = [moment().subtract(numberOf -1,'years').startOf('year').startOf('day'), moment().endOf('year').startOf('day')]; 86 | } 87 | } 88 | } 89 | return ranges; 90 | } 91 | function createDateStates(pages) { 92 | var dateStates = {}; 93 | pages.forEach(function (page) { 94 | page.qMatrix.forEach(function (row) { 95 | var d = createDate(row[0].qNum); 96 | dateStates[d] = row[0].qState; 97 | //based on order numerically 98 | if (row[0].qState === 'S') { 99 | dateStates.rangeEnd = dateStates.rangeEnd || row[0].qNum; 100 | dateStates.rangeStart = row[0].qNum; 101 | } 102 | }); 103 | }); 104 | return dateStates; 105 | } 106 | function isEmpty(obj) { 107 | for(var key in obj) { 108 | if(obj.hasOwnProperty(key)) 109 | return false; 110 | } 111 | return true; 112 | } 113 | function createHtml(dateStates, DateFormat, props, sortAscending) { 114 | var html = '

', startRange, endRange; 115 | if( !isEmpty (dateStates) ) { 116 | html += '
'; 117 | html += '  '; 118 | if (dateStates.rangeStart) { 119 | startRange = createMoment(dateStates.rangeStart).format(DateFormat); 120 | endRange = (dateStates.rangeEnd && (dateStates.rangeEnd !== dateStates.rangeStart)) ? createMoment(dateStates.rangeEnd).format(DateFormat) : null; 121 | if( !sortAscending ) { 122 | html += startRange; 123 | if (endRange !== null) { 124 | html += encoder.encodeForHTML(props.separator) + endRange; 125 | } 126 | } else { 127 | if( endRange!== null) { 128 | html += endRange + encoder.encodeForHTML(props.separator) + startRange; 129 | } else { 130 | html += startRange; 131 | } 132 | } 133 | } else { 134 | html += encoder.encodeForHTML(props.defaultText); 135 | } 136 | html += ' '; 137 | html += '
'; 138 | } else { 139 | html += '    '; 140 | html += 'Add Date Field' + ''; 141 | } 142 | html += '
'; 143 | return html; 144 | } 145 | function getPosition( element ) { 146 | if (element.offset().left < 600) { 147 | return "right"; 148 | } 149 | else if (element.offset().right < 600) { 150 | return "left"; 151 | } 152 | else { 153 | "left"; 154 | } 155 | } 156 | function getTopPosition( element ) { 157 | if ($(window).height() - element.offset().top > 310) { 158 | return "down"; 159 | } 160 | else { 161 | return "up"; 162 | } 163 | } 164 | return { 165 | methods: { //for testability 166 | createDate: createDate, 167 | createMoment: createMoment, 168 | createRanges: createRanges, 169 | createDateStates: createDateStates, 170 | createHtml:createHtml 171 | }, 172 | initialProperties: { 173 | version: 1.0, 174 | qListObjectDef: { 175 | qDef: { 176 | autoSort: false, 177 | qSortCriterias: [ 178 | {qSortByNumeric: -1}, 179 | {qSortByState: 1}, 180 | ], 181 | }, 182 | qShowAlternatives: true, 183 | qFrequencyMode: "V", 184 | qInitialDataFetch: [{ 185 | qWidth: 1, 186 | qHeight: 10000 187 | }] 188 | }, 189 | advanced: false 190 | }, 191 | // Prevent conversion from and to this object 192 | exportProperties: null, 193 | importProperties: null, 194 | definition: CalendarSettings, 195 | support: { 196 | snapshot: false, 197 | export: false, 198 | exportData: false 199 | }, 200 | paint: function ($element, layout) { 201 | var self = this; 202 | var interactionState = this._interactionState; 203 | var noSelections = this.options.noSelections === true; 204 | 205 | // old sort order was ascending, check to see if the object was created before the change 206 | // to calcuate the range start and end dates in the createDateStates 207 | var sortAscending = layout && layout.qListObject && layout.qListObject.qSortCriterias && 208 | layout.qListObject.qSortCriterias.qSortByNumeric == "1"; 209 | 210 | function canInteract() { 211 | return interactionState === 1; 212 | } 213 | 214 | this.dateStates = createDateStates(layout.qListObject.qDataPages); 215 | if (!self.app) { 216 | self.app = qlik.currApp(this); 217 | } 218 | 219 | var qlikDateFormat = layout.qListObject.qDimensionInfo.qNumFormat.qFmt 220 | || self.app.model.layout.qLocaleInfo.qDateFmt; 221 | var outDateFormat = layout.props.format || qlikDateFormat; 222 | var minDate, maxDate, startDate, endDate; 223 | moment.locale(layout.props.locale); 224 | if (qlikDateFormat.indexOf('#') === -1) { 225 | minDate = moment.utc(moment(layout.props.minDate).toDate()); 226 | maxDate = moment.utc(moment(layout.props.maxDate).toDate()); 227 | startDate = moment.utc(moment(layout.props.startDate).toDate()); 228 | endDate = moment.utc(moment(layout.props.endDate).toDate()); 229 | } else { 230 | minDate = createMoment(layout.props.minDate, qlikDateFormat); 231 | maxDate = createMoment(layout.props.maxDate, qlikDateFormat); 232 | startDate = createMoment(layout.props.startDate, qlikDateFormat); 233 | endDate = createMoment(layout.props.endDate, qlikDateFormat); 234 | } 235 | $('#dropDown_' + layout.qInfo.qId).remove(); 236 | 237 | $element.html(createHtml(this.dateStates, outDateFormat, layout.props, sortAscending)); 238 | 239 | var config = { 240 | singleDatePicker: layout.props.isSingleDate, 241 | preventSelections: noSelections, 242 | "locale": { 243 | "format": outDateFormat, 244 | "separator": layout.props.separator 245 | }, 246 | "parentEl": "#grid", 247 | "autoUpdateInput": false, 248 | "autoApply": true, 249 | "opens": getPosition($element), 250 | "top": getTopPosition($element), 251 | "id": layout.qInfo.qId, 252 | getClass: function (date) { 253 | var d = date.format('YYYYMMDD'); 254 | if (self.dateStates[d]) { 255 | return 'state' + self.dateStates[d]; 256 | } 257 | return 'nodata'; 258 | } 259 | }; 260 | 261 | if (minDate.isValid()) { 262 | config.minDate = minDate; 263 | } 264 | 265 | if (maxDate.isValid()) { 266 | config.maxDate = maxDate; 267 | } 268 | 269 | if (startDate.isValid()) { 270 | config.startDate = startDate; 271 | } else { 272 | config.startDate = config.minDate; 273 | } 274 | 275 | if (endDate.isValid()) { 276 | config.endDate = endDate; 277 | } else { 278 | config.endDate = config.maxDate; 279 | } 280 | 281 | if (layout.props.CustomRangesEnabled) { 282 | config.locale.customRangeLabel = layout.props.customRangeLabel; 283 | config.ranges = createRanges(layout.props); 284 | } 285 | if (canInteract()) { 286 | $element.find('.show-range').qlikdaterangepicker(config, function (pickStart, pickEnd, label) { 287 | if (!noSelections && pickStart.isValid() && pickEnd.isValid()) { 288 | 289 | var lastIndex, lowIndex, highIndex, qElemNumbers; 290 | // The conversion to UTC below doesn't work correctly for Timestamp, 291 | // so checking the format which is '###0' for timestamps and doing different conversion formats. 292 | if (qlikDateFormat.indexOf('#') === -1) { 293 | //To support various time zones, converting dates to a UTC format so they can be compared correctly. 294 | pickStart = moment.utc(moment(pickStart).toDate()); 295 | pickEnd = moment.utc(moment(pickEnd).toDate()); 296 | } else { 297 | // Handling timestamp case separately 298 | pickStart = moment.utc(pickStart.format("YYYYMMDD").toString(), "YYYYMMDD"), 299 | pickEnd = moment.utc(pickEnd.format("YYYYMMDD").toString(), "YYYYMMDD"); 300 | } 301 | var dateBinarySearch = function(seachDate, lowIndex, highIndex) { 302 | if (lowIndex === highIndex) { 303 | return lowIndex; 304 | } 305 | var middleIndex = lowIndex + Math.ceil((highIndex - lowIndex) / 2); 306 | var middleDate = qlikDateFormat.indexOf('#') === -1 ? moment.utc(moment( 307 | layout.qListObject.qDataPages[0].qMatrix[middleIndex][0].qText, 308 | qlikDateFormat).toDate()): createMoment(layout.qListObject.qDataPages[0].qMatrix[middleIndex][0].qText, qlikDateFormat); 309 | // If the date object is created prior to September 2019, the order 310 | //of dates shall be ascending and needs to be handled separately 311 | if (sortAscending) { 312 | if (seachDate.isBefore(middleDate)) { 313 | return dateBinarySearch(seachDate, lowIndex, middleIndex - 1); 314 | } 315 | } 316 | // From September 2019, The matrix stores the dates from latest to earliest, so if the 317 | // sought date is after the middle date, pick the lower index span 318 | else { 319 | if (seachDate.isAfter(middleDate)) { 320 | return dateBinarySearch(seachDate, lowIndex, middleIndex - 1); 321 | } 322 | } 323 | return dateBinarySearch(seachDate, middleIndex, highIndex); 324 | }; 325 | if (sortAscending) { 326 | lastIndex = layout.qListObject.qDataPages[0].qMatrix.length - 1; 327 | // Elements are stored in ascending order, so pick out index of start first 328 | lowIndex = dateBinarySearch(pickStart, 0, lastIndex); 329 | // Index of end is guaranteed to be >= index of start 330 | highIndex = dateBinarySearch(pickEnd, lowIndex, lastIndex); 331 | qElemNumbers = layout.qListObject.qDataPages[0].qMatrix 332 | .slice(lowIndex, highIndex + 1).map(function (fieldValue) { 333 | return fieldValue[0].qElemNumber; 334 | }); 335 | } else { 336 | lastIndex = layout.qListObject.qDataPages[0].qMatrix.length - 1; 337 | // Elements are stored in reverse order, so pick out index of end first 338 | lowIndex = dateBinarySearch(pickEnd, 0, lastIndex); 339 | // Index of start is guaranteed to be >= index of end 340 | highIndex = dateBinarySearch(pickStart, lowIndex, lastIndex); 341 | qElemNumbers = layout.qListObject.qDataPages[0].qMatrix 342 | .slice(lowIndex, highIndex + 1).map(function (fieldValue) { 343 | var date; 344 | if (qlikDateFormat.indexOf('#') === -1) { 345 | date = moment.utc(moment(fieldValue[0].qText, qlikDateFormat).toDate()); 346 | } else { 347 | date = createMoment(fieldValue[0].qText, qlikDateFormat); 348 | } 349 | if(date.isSame(pickEnd) || date.isSame(pickStart)) { 350 | return fieldValue[0].qElemNumber; 351 | } else if(date.isBefore(pickEnd) && date.isAfter(pickStart)) { 352 | return fieldValue[0].qElemNumber; 353 | } else { 354 | return -1; 355 | } 356 | }); 357 | } 358 | self.backendApi.selectValues(0, qElemNumbers, false); 359 | } 360 | }); 361 | } 362 | } 363 | }; 364 | }); -------------------------------------------------------------------------------- /src/calendar-settings.js: -------------------------------------------------------------------------------- 1 | define(["qlik"], function (qlik) { 2 | var fieldList, fieldListPromise, CalendarSettings; 3 | 4 | function getPromise() { 5 | fieldListPromise = qlik.currApp().createGenericObject({ 6 | qFieldListDef: { 7 | qType: 'variable' 8 | } 9 | }).then(function (reply) { 10 | fieldList = reply.layout.qFieldList.qItems.filter(function (item) { 11 | return item.qTags && item.qTags.indexOf('$date') > -1; 12 | }).map(function (item) { 13 | return { 14 | value: item.qName, 15 | label: item.qName 16 | }; 17 | }); 18 | return fieldList; 19 | }); 20 | return fieldListPromise; 21 | } 22 | function isQlikCloud(){ 23 | const qlikCloudRegEx = /\.(qlik-stage|qlikcloud)\.com/; 24 | const matcher = window.location.hostname.match(qlikCloudRegEx) || []; 25 | return matcher.length; 26 | } 27 | 28 | var dimension = { 29 | type: "items", 30 | label: "Field", 31 | min: 1, 32 | max: 1, 33 | items: { 34 | field: { 35 | type: "string", 36 | ref: "qListObjectDef.qDef.qFieldDefs.0", 37 | label: "Date field", 38 | component: 'dropdown', 39 | options: function () { 40 | return getPromise(); 41 | }, 42 | show: function (data) { 43 | return !data.advanced; 44 | }, 45 | change: function (data) { 46 | var field = data.qListObjectDef.qDef.qFieldDefs[0]; 47 | data.props.minDate = { qStringExpression: '=Min( {1} [' + field + '])' }; 48 | data.props.maxDate = { qStringExpression: '=Max( {1} [' + field + '])' }; 49 | data.props.startDate = { qStringExpression: '=Min([' + field + '])' }; 50 | data.props.endDate = { qStringExpression: '=Max([' + field + '])' }; 51 | } 52 | }, 53 | fieldAdvanced: { 54 | type: "string", 55 | ref: "qListObjectDef.qDef.qFieldDefs.0", 56 | label: "Date field", 57 | expression: "always", 58 | expressionType: "dimension", 59 | show: function (data) { 60 | return data.advanced; 61 | } 62 | }, 63 | SingleDateSwitch: { 64 | type: "boolean", 65 | component: "switch", 66 | label: "Single date / interval", 67 | ref: "props.isSingleDate", 68 | options: [{ 69 | value: true, 70 | label: "Single date" 71 | }, { 72 | value: false, 73 | label: "Date interval" 74 | }], 75 | defaultValue: false 76 | }, 77 | advanced: { 78 | type: "boolean", 79 | component: "switch", 80 | label: "Advanced setup", 81 | ref: "advanced", 82 | options: [{ 83 | value: true, 84 | translation: "properties.on" 85 | }, { 86 | value: false, 87 | translation: "properties.off" 88 | }], 89 | defaultValue: false, 90 | show: function (data) { 91 | return data.qListObjectDef.qDef.qFieldDefs.length > 0 && 92 | data.qListObjectDef.qDef.qFieldDefs[0].length > 0; 93 | } 94 | }, 95 | minDate: { 96 | ref: "props.minDate", 97 | label: "Min date", 98 | type: "string", 99 | expression: "optional", 100 | show: function (data) { 101 | return data.advanced; 102 | } 103 | }, 104 | maxDate: { 105 | ref: "props.maxDate", 106 | label: "Max date", 107 | type: "string", 108 | expression: "optional", 109 | show: function (data) { 110 | return data.advanced; 111 | } 112 | }, 113 | startDate: { 114 | ref: "props.startDate", 115 | label: "Start date", 116 | type: "string", 117 | expression: "optional", 118 | show: function (data) { 119 | return data.advanced; 120 | } 121 | }, 122 | endDate: { 123 | ref: "props.endDate", 124 | label: "End date", 125 | type: "string", 126 | expression: "optional", 127 | show: function (data) { 128 | return data.advanced; 129 | } 130 | } 131 | } 132 | }; 133 | // This creates a divider in the property panel without showing any warnings 134 | var divider = { 135 | type: 'items', 136 | grouped: true, 137 | items: { divider: { type: 'items' } }, 138 | }; 139 | if( !isQlikCloud() ) { 140 | CalendarSettings = { 141 | component: "expandable-items", 142 | label: "Calendar Settings", 143 | items: { 144 | ranges: { 145 | type: "items", 146 | label: "Predefined ranges", 147 | grouped: true, 148 | items: { 149 | showPredefinedRanges: { 150 | type: 'items', 151 | items: { 152 | CustomRangesSwitch: { 153 | type: "boolean", 154 | component: "switch", 155 | label: "Show predefined ranges", 156 | ref: "props.CustomRangesEnabled", 157 | options: [{ 158 | value: true, 159 | translation: "properties.on" 160 | }, { 161 | value: false, 162 | translation: "properties.off" 163 | }], 164 | defaultValue: true 165 | }, 166 | CustomRange: { 167 | type: "string", 168 | ref: "props.customRangeLabel", 169 | label: "Custom Range", 170 | defaultValue: "Range", 171 | expression: "optional", 172 | show: function (data) { 173 | return data.props.CustomRangesEnabled; 174 | } 175 | }, 176 | Today: { 177 | type: "string", 178 | ref: "props.today", 179 | label: "Today", 180 | defaultValue: "Today", 181 | expression: "optional", 182 | show: function (data) { 183 | return data.props.CustomRangesEnabled; 184 | } 185 | }, 186 | Yesterday: { 187 | type: "string", 188 | ref: "props.yesterday", 189 | label: "Yesterday", 190 | defaultValue: "Yesterday", 191 | expression: "optional", 192 | show: function (data) { 193 | return data.props.CustomRangesEnabled; 194 | } 195 | }, 196 | LastDays: { 197 | type: "string", 198 | ref: "props.lastXDays", 199 | label: "Last $ days", 200 | defaultValue: "Last $ days", 201 | expression: "optional", 202 | show: function (data) { 203 | return data.props.CustomRangesEnabled; 204 | } 205 | }, 206 | }, 207 | }, 208 | showCustomRangeThis: { 209 | type: 'items', 210 | items: { 211 | ThisMonthDropDown: { 212 | type: "string", 213 | ref: "props.this", 214 | label: "This", 215 | component: "dropdown", 216 | defaultValue: 'm', 217 | show: function (data) { 218 | return data.props.CustomRangesEnabled; 219 | }, 220 | change: function (data) { 221 | if (data.props.this === 'd') { 222 | data.props.thisLabel = "This Day"; 223 | } else if(data.props.this === 'm') { 224 | data.props.thisLabel = "This Month"; 225 | } else if(data.props.this === 'q') { 226 | data.props.thisLabel = "This Quarter"; 227 | } else if(data.props.this === 'y') { 228 | data.props.thisLabel = "This Year"; 229 | } else if(data.props.this === 'n') { 230 | data.props.thisLabel = ""; 231 | } 232 | }, 233 | options: [{ 234 | value: 'd', 235 | label: 'Day' 236 | }, { 237 | value: 'm', 238 | label: 'Month' 239 | }, { 240 | value: 'q', 241 | label: 'Quarter' 242 | }, { 243 | value: 'y', 244 | label: 'Year' 245 | },{ 246 | value: 'n', 247 | label: 'None' 248 | }], 249 | }, 250 | ThisMonth: { 251 | type: "string", 252 | ref: "props.thisLabel", 253 | defaultValue: "This Month", 254 | expression: 'optional', 255 | show: function (data) { 256 | return data.props.CustomRangesEnabled; 257 | }, 258 | change: function (data) { 259 | if (data.props.this === 'd' && data.props.thisLabel === '') { 260 | data.props.thisLabel = "This Day"; 261 | } else if(data.props.this === 'm' && data.props.thisLabel === '') { 262 | data.props.thisLabel = "This Month"; 263 | } else if(data.props.this === 'q' && data.props.thisLabel === '') { 264 | data.props.thisLabel = "This Quarter"; 265 | } else if(data.props.this === 'y' && data.props.thisLabel === '') { 266 | data.props.thisLabel = "This Year"; 267 | } else if(data.props.this === 'n') { 268 | data.props.thisLabel = ""; 269 | } 270 | }, 271 | }, 272 | }, 273 | }, 274 | showCustomRangeLast: { 275 | type: 'items', 276 | items: { 277 | LastMonthDropDown: { 278 | type: "string", 279 | ref: "props.last", 280 | label: "Last", 281 | component: "dropdown", 282 | defaultValue: 'm', 283 | show: function (data) { 284 | return data.props.CustomRangesEnabled; 285 | }, 286 | change: function (data) { 287 | if (data.props.last === 'd') { 288 | data.props.lastLabel = "Last Day"; 289 | } else if(data.props.last === 'm') { 290 | data.props.lastLabel = "Last Month"; 291 | } else if(data.props.last === 'q') { 292 | data.props.lastLabel = "Last Quarter"; 293 | } else if(data.props.last === 'y') { 294 | data.props.lastLabel = "Last Year"; 295 | } else if(data.props.last === 'n') { 296 | data.props.lastLabel = ""; 297 | } 298 | }, 299 | options: [{ 300 | value: 'd', 301 | label: 'Day' 302 | }, { 303 | value: 'm', 304 | label: 'Month' 305 | }, { 306 | value: 'q', 307 | label: 'Quarter' 308 | }, { 309 | value: 'y', 310 | label: 'Year' 311 | },{ 312 | value: 'n', 313 | label: 'None' 314 | }], 315 | }, 316 | LastMonth: { 317 | type: "string", 318 | ref: "props.lastLabel", 319 | defaultValue: "Last Month", 320 | expression: 'optional', 321 | show: function (data) { 322 | return data.props.CustomRangesEnabled; 323 | }, 324 | change: function (data) { 325 | if (data.props.last === 'd' && data.props.lastLabel === '') { 326 | data.props.lastLabel = "Last Day"; 327 | } else if(data.props.last === 'm' && data.props.lastLabel === '') { 328 | data.props.lastLabel = "Last Month"; 329 | } else if(data.props.last === 'q' && data.props.lastLabel === '') { 330 | data.props.lastLabel = "Last Quarter"; 331 | } else if(data.props.last === 'y' && data.props.lastLabel === '') { 332 | data.props.lastLabel = "Last Year"; 333 | } else if(data.props.last === 'n') { 334 | data.props.lastLabel = ""; 335 | } 336 | }, 337 | }, 338 | numberOf: { 339 | type: 'number', 340 | ref: 'props.numberOf', 341 | label: 'Last number of', 342 | defaultValue: 1, 343 | show: function (data) { 344 | return data.props.CustomRangesEnabled && ['d','m','q','y'].indexOf(data.props.last) > -1; 345 | } 346 | }, 347 | previousOrLastValues: { 348 | ref: 'props.previousOrLast', 349 | type: 'boolean', 350 | label: 'Include current', 351 | component: 'checkbox', 352 | defaultValue: false, 353 | show: function (data) { 354 | return data.props.CustomRangesEnabled && ['d','m','q','y'].indexOf(data.props.last) > -1; 355 | } 356 | }, 357 | }, 358 | }, 359 | }, 360 | }, 361 | header1: { 362 | type: "items", 363 | label: "Language and labels", 364 | items: { 365 | Language: { 366 | type: "string", 367 | ref: "props.locale", 368 | label: "Locale", 369 | defaultValue: "en", 370 | expression: "optional" 371 | }, 372 | Format: { 373 | type: "string", 374 | ref: "props.format", 375 | label: "Format", 376 | defaultValue: "YYYY-MM-DD", 377 | expression: "optional" 378 | }, 379 | Separator: { 380 | type: "string", 381 | ref: "props.separator", 382 | label: "Separator", 383 | defaultValue: " - ", 384 | expression: "optional" 385 | }, 386 | defaultText: { 387 | type: "string", 388 | ref: "props.defaultText", 389 | label: "Default Text", 390 | expression: "optional", 391 | defaultValue: "Select date range" 392 | } 393 | } 394 | } 395 | } 396 | }; 397 | } else { 398 | CalendarSettings = { 399 | component: "expandable-items", 400 | label: "Calendar Settings", 401 | items: { 402 | ranges: { 403 | type: "items", 404 | label: "Predefined ranges", 405 | items: { 406 | CustomRangesSwitch: { 407 | type: "boolean", 408 | component: "switch", 409 | label: "Show predefined ranges", 410 | ref: "props.CustomRangesEnabled", 411 | options: [{ 412 | value: true, 413 | translation: "properties.on" 414 | }, { 415 | value: false, 416 | translation: "properties.off" 417 | }], 418 | defaultValue: true 419 | }, 420 | CustomRange: { 421 | type: "string", 422 | ref: "props.customRangeLabel", 423 | label: "Custom Range", 424 | defaultValue: "Range", 425 | expression: "optional", 426 | show: function (data) { 427 | return data.props.CustomRangesEnabled; 428 | } 429 | }, 430 | Today: { 431 | type: "string", 432 | ref: "props.today", 433 | label: "Today", 434 | defaultValue: "Today", 435 | expression: "optional", 436 | show: function (data) { 437 | return data.props.CustomRangesEnabled; 438 | } 439 | }, 440 | Yesterday: { 441 | type: "string", 442 | ref: "props.yesterday", 443 | label: "Yesterday", 444 | defaultValue: "Yesterday", 445 | expression: "optional", 446 | show: function (data) { 447 | return data.props.CustomRangesEnabled; 448 | } 449 | }, 450 | LastDays: { 451 | type: "string", 452 | ref: "props.lastXDays", 453 | label: "Last $ days", 454 | defaultValue: "Last $ days", 455 | expression: "optional", 456 | show: function (data) { 457 | return data.props.CustomRangesEnabled; 458 | } 459 | }, 460 | ThisMonth: { 461 | type: "string", 462 | ref: "props.thisMonth", 463 | label: "This Month", 464 | defaultValue: "This Month", 465 | expression: "optional", 466 | show: function (data) { 467 | return data.props.CustomRangesEnabled; 468 | } 469 | }, 470 | LastMonth: { 471 | type: "string", 472 | ref: "props.lastMonth", 473 | label: "Last Month", 474 | defaultValue: "Last Month", 475 | expression: "optional", 476 | show: function (data) { 477 | return data.props.CustomRangesEnabled; 478 | } 479 | } 480 | } 481 | }, 482 | header1: { 483 | type: "items", 484 | label: "Language and labels", 485 | items: { 486 | Language: { 487 | type: "string", 488 | ref: "props.locale", 489 | label: "Locale", 490 | defaultValue: "en", 491 | expression: "optional" 492 | }, 493 | Format: { 494 | type: "string", 495 | ref: "props.format", 496 | label: "Format", 497 | defaultValue: "YYYY-MM-DD", 498 | expression: "optional" 499 | }, 500 | Separator: { 501 | type: "string", 502 | ref: "props.separator", 503 | label: "Separator", 504 | defaultValue: " - ", 505 | expression: "optional" 506 | }, 507 | defaultText: { 508 | type: "string", 509 | ref: "props.defaultText", 510 | label: "Default Text", 511 | expression: "optional", 512 | defaultValue: "Select date range" 513 | } 514 | } 515 | } 516 | } 517 | }; 518 | } 519 | var about = { 520 | label: "About", 521 | component: "items", 522 | items: { 523 | header: { 524 | label: 'Date picker', 525 | style: 'header', 526 | component: 'text' 527 | }, 528 | paragraph1: { 529 | label: 'A calendar object that allows a user to make selections in a date field.', 530 | component: 'text' 531 | }, 532 | paragraph2: { 533 | label: 'Date picker is based upon an extension created by Nodier Torres.', 534 | component: 'text' 535 | } 536 | } 537 | }; 538 | var appearance = { 539 | uses: "settings", 540 | items: { 541 | general: { 542 | items: { 543 | details: { 544 | show: false 545 | } 546 | } 547 | }, 548 | } 549 | }; 550 | return { 551 | type: "items", 552 | component: "accordion", 553 | items: { 554 | dimension: dimension, 555 | settings: appearance, 556 | CalSettings: CalendarSettings, 557 | about: about 558 | } 559 | } 560 | }) -------------------------------------------------------------------------------- /src/lib/daterangepicker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @version: 2.1.13 3 | * @author: Dan Grossman http://www.dangrossman.info/ 4 | * @copyright: Copyright (c) 2012-2015 Dan Grossman. All rights reserved. 5 | * @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php 6 | * @website: https://www.improvely.com/ 7 | *** 8 | *Sense Extension 9 | *Nodier Torres 10 | *Added a few changes to the html template 11 | **Added if (this.endDate?this.endDate._isValid:false) at udpdateFormInputs. when clear selections endDate will be set to null to make default calendar without range selected 12 | *** 13 | */ 14 | 15 | (function(root, factory) { 16 | 17 | if (typeof define === 'function' && define.amd ) { 18 | define(['./moment.min', 'jquery', 'exports'], function(momentjs, $, exports) { 19 | root.qlikdaterangepicker = factory(root, exports, momentjs, $); 20 | }); 21 | 22 | } else if (typeof exports !== 'undefined') { 23 | var momentjs = require('moment'); 24 | var jQuery = (typeof window != 'undefined') ? window.jQuery : undefined; //isomorphic issue 25 | if (!jQuery) { 26 | try { 27 | jQuery = require('jquery'); 28 | if (!jQuery.fn) jQuery.fn = {}; //isomorphic issue 29 | } catch (err) { 30 | if (!jQuery) throw new Error('jQuery dependency not found'); 31 | } 32 | } 33 | 34 | factory(root, exports, momentjs, jQuery); 35 | 36 | // Finally, as a browser global. 37 | } else { 38 | root.qlikdaterangepicker = factory(root, {}, root.moment || moment, (root.jQuery || root.Zepto || root.ender || root.$)); 39 | } 40 | 41 | }(this || {}, function(root, qlikdaterangepicker, moment, $) { // 'this' doesn't exist on a server 42 | 43 | var DateRangePicker = function(element, options, cb) { 44 | 45 | //default settings for options 46 | this.parentEl = 'body'; 47 | this.element = $(element); 48 | this.startDate = moment().startOf('day'); 49 | this.endDate = moment().endOf('day'); 50 | this.minDate = false; 51 | this.maxDate = false; 52 | this.dateLimit = false; 53 | this.autoApply = false; 54 | this.singleDatePicker = false; 55 | this.showDropdowns = false; 56 | this.showWeekNumbers = false; 57 | this.timePicker = false; 58 | this.timePicker24Hour = false; 59 | this.timePickerIncrement = 1; 60 | this.timePickerSeconds = false; 61 | this.linkedCalendars = true; 62 | this.autoUpdateInput = true; 63 | this.ranges = {}; 64 | 65 | this.opens = 'right'; 66 | if (this.element.hasClass('pull-right')) 67 | this.opens = 'left'; 68 | this.drops = options.top; 69 | if (this.element.hasClass('dropup')) 70 | this.drops = 'up'; 71 | 72 | this.buttonClasses = 'btn btn-sm'; 73 | this.applyClass = 'btn-success'; 74 | this.cancelClass = 'btn-default'; 75 | 76 | this.locale = { 77 | format: 'MM/DD/YYYY', 78 | separator: ' - ', 79 | applyLabel: 'Apply', 80 | cancelLabel: 'Cancel', 81 | weekLabel: 'W', 82 | customRangeLabel: 'Custom Range', 83 | daysOfWeek: moment.weekdaysMin(), 84 | monthNames: moment.monthsShort(), 85 | firstDay: moment.localeData().firstDayOfWeek() 86 | }; 87 | 88 | this.callback = function() { }; 89 | 90 | //some state information 91 | this.isShowing = false; 92 | this.leftCalendar = {}; 93 | this.rightCalendar = {}; 94 | 95 | this.preventSelections = false; 96 | //custom options from user 97 | if (typeof options !== 'object' || options === null) 98 | options = {}; 99 | 100 | //allow setting options with data attributes 101 | //data-api options will be overwritten with custom javascript options 102 | options = $.extend(this.element.data(), options); 103 | error_nodata = "No data available for the range selected. Please select again." 104 | 105 | //html template for the picker UI 106 | if (typeof options.template !== 'string') 107 | options.template = ''; 138 | 139 | this.parentEl = (options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl); 140 | 141 | this.container = $(options.template).appendTo(this.parentEl); 142 | 143 | // 144 | // handle all the possible options overriding defaults 145 | // 146 | 147 | if (typeof options.locale === 'object') { 148 | 149 | if (typeof options.locale.format === 'string') 150 | this.locale.format = options.locale.format; 151 | 152 | if (typeof options.locale.separator === 'string') 153 | this.locale.separator = options.locale.separator; 154 | 155 | if (typeof options.locale.daysOfWeek === 'object') 156 | this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); 157 | 158 | if (typeof options.locale.monthNames === 'object') 159 | this.locale.monthNames = options.locale.monthNames.slice(); 160 | 161 | if (typeof options.locale.firstDay === 'number') 162 | this.locale.firstDay = options.locale.firstDay; 163 | 164 | if (typeof options.locale.applyLabel === 'string') 165 | this.locale.applyLabel = options.locale.applyLabel; 166 | 167 | if (typeof options.locale.cancelLabel === 'string') 168 | this.locale.cancelLabel = options.locale.cancelLabel; 169 | 170 | if (typeof options.locale.weekLabel === 'string') 171 | this.locale.weekLabel = options.locale.weekLabel; 172 | 173 | if (typeof options.locale.customRangeLabel === 'string') 174 | this.locale.customRangeLabel = options.locale.customRangeLabel; 175 | 176 | } 177 | 178 | if (typeof options.startDate === 'string') 179 | this.startDate = moment(options.startDate, this.locale.format); 180 | 181 | if (typeof options.endDate === 'string') 182 | this.endDate = moment(options.endDate, this.locale.format); 183 | 184 | if (typeof options.minDate === 'string') 185 | this.minDate = moment(options.minDate, this.locale.format); 186 | 187 | if (typeof options.maxDate === 'string') 188 | this.maxDate = moment(options.maxDate, this.locale.format); 189 | 190 | if (typeof options.startDate === 'object') 191 | this.startDate = moment(options.startDate); 192 | 193 | if (typeof options.endDate === 'object') 194 | this.endDate = moment(options.endDate); 195 | 196 | if (typeof options.minDate === 'object') 197 | this.minDate = moment(options.minDate); 198 | 199 | if (typeof options.maxDate === 'object') 200 | this.maxDate = moment(options.maxDate); 201 | 202 | // sanity check for bad options 203 | if (this.minDate && this.startDate.isBefore(this.minDate)) 204 | this.startDate = this.minDate.clone(); 205 | 206 | // sanity check for bad options 207 | if (this.maxDate && this.endDate.isAfter(this.maxDate)) 208 | this.endDate = this.maxDate.clone(); 209 | 210 | if (typeof options.applyClass === 'string') 211 | this.applyClass = options.applyClass; 212 | 213 | if (typeof options.cancelClass === 'string') 214 | this.cancelClass = options.cancelClass; 215 | 216 | if (typeof options.dateLimit === 'object') 217 | this.dateLimit = options.dateLimit; 218 | 219 | if (typeof options.opens === 'string') 220 | this.opens = options.opens; 221 | 222 | if (typeof options.top === 'string') 223 | this.top = options.top; 224 | 225 | if (typeof options.drops === 'string') 226 | this.drops = options.drops; 227 | 228 | if (typeof options.showWeekNumbers === 'boolean') 229 | this.showWeekNumbers = options.showWeekNumbers; 230 | 231 | if (typeof options.buttonClasses === 'string') 232 | this.buttonClasses = options.buttonClasses; 233 | 234 | if (typeof options.buttonClasses === 'object') 235 | this.buttonClasses = options.buttonClasses.join(' '); 236 | 237 | if (typeof options.showDropdowns === 'boolean') 238 | this.showDropdowns = options.showDropdowns; 239 | 240 | if (typeof options.singleDatePicker === 'boolean') { 241 | this.singleDatePicker = options.singleDatePicker; 242 | if (this.singleDatePicker) 243 | this.endDate = this.startDate.clone(); 244 | } 245 | 246 | if (typeof options.timePicker === 'boolean') 247 | this.timePicker = options.timePicker; 248 | 249 | if (typeof options.timePickerSeconds === 'boolean') 250 | this.timePickerSeconds = options.timePickerSeconds; 251 | 252 | if (typeof options.timePickerIncrement === 'number') 253 | this.timePickerIncrement = options.timePickerIncrement; 254 | 255 | if (typeof options.timePicker24Hour === 'boolean') 256 | this.timePicker24Hour = options.timePicker24Hour; 257 | 258 | if (typeof options.autoApply === 'boolean') 259 | this.autoApply = options.autoApply; 260 | 261 | if (typeof options.autoUpdateInput === 'boolean') 262 | this.autoUpdateInput = options.autoUpdateInput; 263 | 264 | if (typeof options.linkedCalendars === 'boolean') 265 | this.linkedCalendars = options.linkedCalendars; 266 | 267 | if (typeof options.isInvalidDate === 'function') 268 | this.isInvalidDate = options.isInvalidDate; 269 | 270 | if (typeof options.getClass === 'function') 271 | this.getClass = options.getClass; 272 | 273 | if (typeof options.preventSelections === 'boolean') 274 | this.preventSelections = options.preventSelections; 275 | 276 | // update day names order to firstDay 277 | if (this.locale.firstDay != 0) { 278 | var iterator = this.locale.firstDay; 279 | while (iterator > 0) { 280 | this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); 281 | iterator--; 282 | } 283 | } 284 | 285 | var start, end, range; 286 | 287 | //if no start/end dates set, check if an input element contains initial values 288 | if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') { 289 | if ($(this.element).is('input[type=text]')) { 290 | var val = $(this.element).val(), 291 | split = val.split(this.locale.separator); 292 | 293 | start = end = null; 294 | 295 | if (split.length == 2) { 296 | start = moment(split[0], this.locale.format); 297 | end = moment(split[1], this.locale.format); 298 | } else if (this.singleDatePicker && val !== "") { 299 | start = moment(val, this.locale.format); 300 | end = moment(val, this.locale.format); 301 | } 302 | if (start !== null && end !== null) { 303 | this.setStartDate(start); 304 | this.setEndDate(end); 305 | } 306 | } 307 | } 308 | 309 | if (typeof options.ranges === 'object') { 310 | for (range in options.ranges) { 311 | 312 | if (typeof options.ranges[range][0] === 'string') 313 | start = moment(options.ranges[range][0], this.locale.format); 314 | else 315 | start = moment(options.ranges[range][0]); 316 | 317 | if (typeof options.ranges[range][1] === 'string') 318 | end = moment(options.ranges[range][1], this.locale.format); 319 | else 320 | end = moment(options.ranges[range][1]); 321 | 322 | // If the start or end date exceed those allowed by the minDate or dateLimit 323 | // options, shorten the range to the allowable period. 324 | if (this.minDate && start.isBefore(this.minDate)) 325 | start = this.minDate.clone(); 326 | 327 | var maxDate = this.maxDate; 328 | if (this.dateLimit && start.clone().add(this.dateLimit).isAfter(maxDate)) 329 | maxDate = start.clone().add(this.dateLimit); 330 | if (maxDate && end.isAfter(maxDate)) 331 | end = maxDate.clone(); 332 | 333 | // If the end of the range is before the minimum or the start of the range is 334 | // after the maximum, disable this range option . 335 | var disabled = (this.minDate && end.isBefore(this.minDate)) || (maxDate && start.isAfter(maxDate)); 336 | 337 | var elem = document.createElement('textarea'); 338 | elem.innerHTML = range; 339 | var rangeHtml = elem.value; 340 | 341 | this.ranges[rangeHtml] = [start, end, disabled]; 342 | } 343 | 344 | var list = '
'; 351 | this.container.find('.ranges').prepend(list); 352 | } 353 | 354 | if (typeof cb === 'function') { 355 | this.callback = cb; 356 | } 357 | 358 | if (!this.timePicker) { 359 | this.startDate = this.startDate.startOf('day'); 360 | this.endDate = this.endDate.endOf('day'); 361 | this.container.find('.calendar-time').hide(); 362 | } 363 | 364 | //can't be used together for now 365 | if (this.timePicker && this.autoApply) 366 | this.autoApply = false; 367 | 368 | if (this.autoApply && typeof options.ranges !== 'object') { 369 | this.container.find('.ranges').hide(); 370 | } else if (this.autoApply) { 371 | this.container.find('.applyBtn, .cancelBtn').addClass('hide'); 372 | } 373 | 374 | if (this.singleDatePicker) { 375 | this.container.addClass('single'); 376 | this.container.find('.calendar.dpleft').addClass('single'); 377 | this.container.find('.calendar.dpleft').show(); 378 | this.container.find('.calendar.dpright').hide(); 379 | this.container.find('.qlik-daterangepicker_input input, .qlik-daterangepicker_input i').hide(); 380 | if (!this.timePicker) { 381 | this.container.find('.ranges').hide(); 382 | } 383 | } 384 | 385 | if (typeof options.ranges === 'undefined' && !this.singleDatePicker) { 386 | this.container.addClass('show-calendar'); 387 | } 388 | 389 | this.container.addClass('opens' + this.opens); 390 | 391 | if (this.isQlikCloud()) { 392 | //swap the position of the predefined ranges if opens right 393 | if (typeof options.ranges !== 'undefined' && this.opens == 'right') { 394 | var ranges = this.container.find('.ranges'); 395 | var html = ranges.clone(); 396 | ranges.remove(); 397 | this.container.find('.calendar.dpleft').parent().prepend(html); 398 | } 399 | } 400 | 401 | //apply CSS classes and labels to buttons 402 | this.container.find('.applyBtn, .cancelBtn').addClass(this.buttonClasses); 403 | if (this.applyClass.length) 404 | this.container.find('.applyBtn').addClass(this.applyClass); 405 | if (this.cancelClass.length) 406 | this.container.find('.cancelBtn').addClass(this.cancelClass); 407 | this.container.find('.applyBtn').html(this.locale.applyLabel); 408 | this.container.find('.cancelBtn').html(this.locale.cancelLabel); 409 | 410 | // 411 | // event listeners 412 | // 413 | this.container.find('.calendar') 414 | .on('click.qlik-daterangepicker', '.prev.available', $.proxy(this.clickPrev, this)) 415 | .on('click.qlik-daterangepicker', '.next.available', $.proxy(this.clickNext, this)) 416 | .on('click.qlik-daterangepicker', 'td.available', $.proxy(this.clickDate, this)) 417 | .on('mouseenter.qlik-daterangepicker', 'td.available', $.proxy(this.hoverDate, this)) 418 | .on('mouseleave.qlik-daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this)) 419 | .on('change.qlik-daterangepicker', 'select.yearselect', $.proxy(this.monthOrYearChanged, this)) 420 | .on('change.qlik-daterangepicker', 'select.monthselect', $.proxy(this.monthOrYearChanged, this)) 421 | .on('change.qlik-daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.timeChanged, this)) 422 | .on('click.qlik-daterangepicker', '.qlik-daterangepicker_input input', $.proxy(this.showCalendars, this)) 423 | //.on('keyup.qlik-daterangepicker', '.qlik-daterangepicker_input input', $.proxy(this.formInputsChanged, this)) 424 | .on('change.qlik-daterangepicker', '.qlik-daterangepicker_input input', $.proxy(this.formInputsChanged, this)); 425 | 426 | this.container.find('.ranges') 427 | .on('click.qlik-daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this)) 428 | .on('click.qlik-daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this)) 429 | .on('click.qlik-daterangepicker', 'li', $.proxy(this.clickRange, this)) 430 | .on('mouseenter.qlik-daterangepicker', 'li', $.proxy(this.hoverRange, this)) 431 | .on('mouseleave.qlik-daterangepicker', 'li', $.proxy(this.updateFormInputs, this)); 432 | 433 | if (this.element.is('input')) { 434 | this.element.on({ 435 | 'click.qlik-daterangepicker': $.proxy(this.show, this), 436 | 'focus.qlik-daterangepicker': $.proxy(this.show, this), 437 | 'keyup.qlik-daterangepicker': $.proxy(this.elementChanged, this), 438 | 'keydown.qlik-daterangepicker': $.proxy(this.keydown, this) 439 | }); 440 | } else { 441 | this.element.on('click.qlik-daterangepicker', $.proxy(this.toggle, this)); 442 | } 443 | 444 | // 445 | // if attached to a text input, set the initial value 446 | // 447 | 448 | if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) { 449 | this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); 450 | this.element.trigger('change'); 451 | } else if (this.element.is('input') && this.autoUpdateInput) { 452 | this.element.val(this.startDate.format(this.locale.format)); 453 | this.element.trigger('change'); 454 | } 455 | 456 | }; 457 | 458 | DateRangePicker.prototype = { 459 | 460 | constructor: DateRangePicker, 461 | 462 | setStartDate: function(startDate) { 463 | if (typeof startDate === 'string') 464 | this.startDate = moment(startDate, this.locale.format); 465 | 466 | if (typeof startDate === 'object') 467 | this.startDate = moment(startDate); 468 | 469 | if (!this.timePicker) 470 | this.startDate = this.startDate.startOf('day'); 471 | 472 | if (this.timePicker && this.timePickerIncrement) 473 | this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); 474 | 475 | if (this.minDate && this.startDate.isBefore(this.minDate)) 476 | this.startDate = this.minDate; 477 | 478 | if (this.maxDate && this.startDate.isAfter(this.maxDate)) 479 | this.startDate = this.maxDate; 480 | 481 | if (!this.isShowing) 482 | this.updateElement(); 483 | 484 | this.container.addClass('in-selection'); 485 | this.updateMonthsInView(); 486 | }, 487 | 488 | setEndDate: function(endDate) { 489 | if (typeof endDate === 'string') 490 | this.endDate = moment(endDate, this.locale.format); 491 | 492 | if (typeof endDate === 'object') 493 | this.endDate = moment(endDate); 494 | 495 | if (!this.timePicker) 496 | this.endDate = this.endDate.endOf('day'); 497 | 498 | if (this.timePicker && this.timePickerIncrement) 499 | this.endDate.minute(Math.round(this.endDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); 500 | 501 | if (this.endDate.isBefore(this.startDate)) 502 | this.endDate = this.startDate.clone(); 503 | 504 | if (this.maxDate.endOf('day') && this.endDate.isAfter(this.maxDate)) 505 | this.endDate = this.maxDate; 506 | 507 | if (this.dateLimit && this.startDate.clone().add(this.dateLimit).isBefore(this.endDate)) 508 | this.endDate = this.startDate.clone().add(this.dateLimit); 509 | 510 | if (!this.isShowing) 511 | this.updateElement(); 512 | 513 | this.updateMonthsInView(); 514 | }, 515 | 516 | isInvalidDate: function() { 517 | return false; 518 | }, 519 | 520 | updateView: function() { 521 | if (this.timePicker) { 522 | this.renderTimePicker('left'); 523 | this.renderTimePicker('right'); 524 | if (!this.endDate) { 525 | this.container.find('.dpright .calendar-time select').attr('disabled', 'disabled').addClass('disabled'); 526 | } else { 527 | this.container.find('.dpright .calendar-time select').removeAttr('disabled').removeClass('disabled'); 528 | } 529 | } 530 | this.container.find('.calendar.active').removeClass('active'); 531 | 532 | if (this.endDate) { 533 | this.container.find('input[name=qlik-daterangepicker_start]').closest('.calendar').addClass('active'); 534 | } else { 535 | this.container.find('input[name=qlik-daterangepicker_end]').closest('.calendar').addClass('active'); 536 | } 537 | this.updateMonthsInView(); 538 | this.updateCalendars(); 539 | this.updateFormInputs(); 540 | }, 541 | 542 | 543 | updateMonthsInView: function() { 544 | if (this.endDate) { 545 | 546 | //if both dates are visible already, do nothing 547 | if (!this.singleDatePicker && this.leftCalendar.month && this.rightCalendar.month && 548 | (this.startDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.startDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) 549 | && 550 | (this.endDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.endDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) 551 | ) { 552 | return; 553 | } 554 | 555 | this.leftCalendar.month = this.startDate.clone().date(2); 556 | if (!this.linkedCalendars && (this.endDate.month() != this.startDate.month() || this.endDate.year() != this.startDate.year())) { 557 | this.rightCalendar.month = this.endDate.clone().date(2); 558 | } else { 559 | this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); 560 | } 561 | 562 | } else { 563 | if (this.leftCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM') && this.rightCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM')) { 564 | this.leftCalendar.month = this.startDate.clone().date(2); 565 | this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); 566 | } 567 | } 568 | }, 569 | 570 | updateCalendars: function() { 571 | 572 | if (this.timePicker) { 573 | var hour, minute, second; 574 | if (this.endDate) { 575 | hour = parseInt(this.container.find('.dpleft .hourselect').val(), 10); 576 | minute = parseInt(this.container.find('.dpleft .minuteselect').val(), 10); 577 | second = this.timePickerSeconds ? parseInt(this.container.find('.dpleft .secondselect').val(), 10) : 0; 578 | if (!this.timePicker24Hour) { 579 | var ampm = this.container.find('.dpleft .ampmselect').val(); 580 | if (ampm === 'PM' && hour < 12) 581 | hour += 12; 582 | if (ampm === 'AM' && hour === 12) 583 | hour = 0; 584 | } 585 | } else { 586 | hour = parseInt(this.container.find('.dpright .hourselect').val(), 10); 587 | minute = parseInt(this.container.find('.dpright .minuteselect').val(), 10); 588 | second = this.timePickerSeconds ? parseInt(this.container.find('.dpright .secondselect').val(), 10) : 0; 589 | if (!this.timePicker24Hour) { 590 | var ampm = this.container.find('.dpright .ampmselect').val(); 591 | if (ampm === 'PM' && hour < 12) 592 | hour += 12; 593 | if (ampm === 'AM' && hour === 12) 594 | hour = 0; 595 | } 596 | } 597 | this.leftCalendar.month.hour(hour).minute(minute).second(second); 598 | this.rightCalendar.month.hour(hour).minute(minute).second(second); 599 | } 600 | 601 | this.renderCalendar('left'); 602 | this.renderCalendar('right'); 603 | 604 | // looks like we don't do anything with active ranges 605 | this.showCalendars(); 606 | }, 607 | 608 | isQlikCloud: function() { 609 | const qlikCloudRegEx = /\.(qlik-stage|qlikcloud)\.com/; 610 | const matcher = window.location.hostname.match(qlikCloudRegEx) || []; 611 | return matcher.length; 612 | }, 613 | 614 | renderCalendar: function(side) { 615 | // 616 | // Build the matrix of dates that will populate the calendar 617 | // 618 | var calendar = side == 'left' ? this.leftCalendar : this.rightCalendar; 619 | var month = calendar.month.month(); 620 | var year = calendar.month.year(); 621 | var hour = calendar.month.hour(); 622 | var minute = calendar.month.minute(); 623 | var second = calendar.month.second(); 624 | var daysInMonth = moment([year, month]).daysInMonth(); 625 | var firstDay = moment([year, month, 1]); 626 | var lastDay = moment([year, month, daysInMonth]); 627 | var lastMonth = moment(firstDay).subtract(1, 'month').month(); 628 | var lastYear = moment(firstDay).subtract(1, 'month').year(); 629 | var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); 630 | var dayOfWeek = firstDay.day(); 631 | //initialize a 6 rows x 7 columns array for the calendar 632 | var calendar = []; 633 | calendar.firstDay = firstDay; 634 | calendar.lastDay = lastDay; 635 | 636 | for (var i = 0; i < 6; i++) { 637 | calendar[i] = []; 638 | } 639 | 640 | //populate the calendar with date objects 641 | var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1; 642 | if (startDay > daysInLastMonth) 643 | startDay -= 7; 644 | 645 | var curDate 646 | if (dayOfWeek == this.locale.firstDay){ 647 | startDay = 1; 648 | curDate = moment([year, month, startDay, 12, minute, second]); 649 | } else { 650 | curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]); 651 | } 652 | 653 | var col, row; 654 | for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) { 655 | if (i > 0 && col % 7 === 0) { 656 | col = 0; 657 | row++; 658 | } 659 | calendar[row][col] = curDate.clone().hour(hour).minute(minute).second(second); 660 | curDate.hour(12); 661 | 662 | if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate)) { 663 | calendar[row][col] = this.minDate.clone(); 664 | } 665 | 666 | if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate)) { 667 | calendar[row][col] = this.maxDate.clone(); 668 | } 669 | 670 | } 671 | 672 | //make the calendar object available to hoverDate/clickDate 673 | if (side == 'left') { 674 | this.leftCalendar.calendar = calendar; 675 | } else { 676 | this.rightCalendar.calendar = calendar; 677 | } 678 | 679 | // 680 | // Display the calendar 681 | // 682 | var minDate = side == 'left' ? this.minDate : this.startDate; 683 | var maxDate = this.maxDate; 684 | var selected = side == 'left' ? this.startDate : this.endDate; 685 | 686 | var html = ''; 687 | html += ''; 688 | html += ''; 689 | 690 | // add empty cell for week number 691 | if (this.showWeekNumbers) 692 | html += ''; 693 | 694 | if ( (!this.linkedCalendars || side == 'left')) { 695 | html += ''; 698 | } else { 699 | html += ''; 700 | } 701 | 702 | var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY"); 703 | 704 | if (this.showDropdowns) { 705 | var currentMonth = calendar[1][1].month(); 706 | var currentYear = calendar[1][1].year(); 707 | var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); 708 | var minYear = (minDate && minDate.year()) || (currentYear - 50); 709 | var inMinYear = currentYear == minYear; 710 | var inMaxYear = currentYear == maxYear; 711 | 712 | var monthHtml = '"; 725 | 726 | var yearHtml = ''; 733 | 734 | dateHtml = monthHtml + yearHtml; 735 | } 736 | 737 | html += ''; 738 | if ((!this.linkedCalendars || side == 'right' || this.singleDatePicker)) { 739 | html += ''; 742 | } else { 743 | html += ''; 744 | } 745 | 746 | html += ''; 747 | html += ''; 748 | 749 | // add week number label 750 | if (this.showWeekNumbers) 751 | html += ''; 752 | 753 | $.each(this.locale.daysOfWeek, function(index, dayOfWeek) { 754 | html += ''; 755 | }); 756 | 757 | html += ''; 758 | html += ''; 759 | html += ''; 760 | 761 | //adjust maxDate to reflect the dateLimit setting in order to 762 | //grey out end dates beyond the dateLimit 763 | if (this.endDate == null && this.dateLimit) { 764 | var maxLimit = this.startDate.clone().add(this.dateLimit).endOf('day'); 765 | if (!maxDate || maxLimit.isBefore(maxDate)) { 766 | maxDate = maxLimit; 767 | } 768 | } 769 | 770 | for (var row = 0; row < 6; row++) { 771 | html += ''; 772 | 773 | // add week number 774 | if (this.showWeekNumbers) 775 | html += ''; 776 | 777 | for (var col = 0; col < 7; col++) { 778 | 779 | var classes = []; 780 | //grey out the dates in other months displayed at beginning and end of this calendar 781 | if (calendar[row][col].month() != calendar[1][1].month()){ 782 | classes.push('empty'); 783 | }else{ 784 | //highlight today's date 785 | if (calendar[row][col].isSame(new Date(), "day")) 786 | classes.push('today'); 787 | 788 | //highlight weekends 789 | if (calendar[row][col].isoWeekday() > 5) 790 | classes.push('weekend'); 791 | 792 | //don't allow selection of dates before the minimum date 793 | if (this.minDate && calendar[row][col].isBefore(this.minDate, 'day')) 794 | classes.push('off', 'disabled'); 795 | 796 | //don't allow selection of dates after the maximum date 797 | if (maxDate && calendar[row][col].isAfter(maxDate, 'day')) 798 | classes.push('off', 'disabled'); 799 | 800 | //don't allow selection of date if a custom function decides it's invalid 801 | if (this.isInvalidDate(calendar[row][col])) 802 | classes.push('off', 'disabled'); 803 | 804 | if(this.getClass) 805 | classes.push(this.getClass(calendar[row][col])); 806 | 807 | //highlight the currently selected start date 808 | if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD')) 809 | classes.push('active', 'start-date'); 810 | 811 | //highlight the currently selected end date 812 | if (this.endDate != null && calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD')) 813 | classes.push('active', 'end-date'); 814 | 815 | //highlight dates in-between the selected dates 816 | //if (this.endDate != null && calendar[row][col] > this.startDate && calendar[row][col] < this.endDate) 817 | // classes.push('in-range'); 818 | } 819 | var cname = '', disabled = false; 820 | for (var i = 0; i < classes.length; i++) { 821 | cname += classes[i] + ' '; 822 | if ( this.isQlikCloud() ) { 823 | if ( ['disabled','nodata','empty'].indexOf(classes[i]) > -1) { 824 | disabled = true; 825 | } 826 | } else { 827 | if ( ['disabled','empty'].indexOf(classes[i]) > -1) { 828 | disabled = true; 829 | } 830 | } 831 | } 832 | if (!disabled) 833 | cname += 'available'; 834 | 835 | html += ''; 836 | 837 | } 838 | html += ''; 839 | } 840 | 841 | html += ''; 842 | html += '
' + dateHtml + '
' + this.locale.weekLabel + '' + dayOfWeek + '
' + calendar[row][0].week() + '' + calendar[row][col].date() + '
'; 843 | 844 | this.container.find('.calendar.dp' + side + ' .calendar-table').html(html); 845 | 846 | }, 847 | 848 | renderTimePicker: function(side) { 849 | 850 | var html, selected, minDate, maxDate = this.maxDate; 851 | 852 | if (this.dateLimit && (!this.maxDate || this.startDate.clone().add(this.dateLimit).isAfter(this.maxDate))) 853 | maxDate = this.startDate.clone().add(this.dateLimit); 854 | 855 | if (side == 'left') { 856 | selected = this.startDate.clone(); 857 | minDate = this.minDate; 858 | } else if (side == 'right') { 859 | selected = this.endDate ? this.endDate.clone() : this.startDate.clone(); 860 | minDate = this.startDate; 861 | } 862 | 863 | // 864 | // hours 865 | // 866 | 867 | html = ' '; 894 | 895 | // 896 | // minutes 897 | // 898 | 899 | html += ': '; 921 | 922 | // 923 | // seconds 924 | // 925 | 926 | if (this.timePickerSeconds) { 927 | html += ': '; 949 | } 950 | 951 | // 952 | // AM/PM 953 | // 954 | 955 | if (!this.timePicker24Hour) { 956 | html += ''; 974 | } 975 | 976 | this.container.find('.calendar.dp' + side + ' .calendar-time div').html(html); 977 | 978 | }, 979 | 980 | updateFormInputs: function() { 981 | //ignore mouse movements while an above-calendar text input has focus 982 | if (this.container.find('input[name=qlik-daterangepicker_start]').is(":focus") || this.container.find('input[name=qlik-daterangepicker_end]').is(":focus")) 983 | return; 984 | 985 | this.container.find('input[name=qlik-daterangepicker_start]').val(this.startDate.format(this.locale.format)); 986 | 987 | if (this.endDate?this.endDate._isValid:false){ 988 | this.container.find('input[name=qlik-daterangepicker_end]').val(this.endDate.format(this.locale.format)); 989 | } 990 | else{ 991 | this.container.find('input[name=qlik-daterangepicker_end]').val("") 992 | } 993 | 994 | 995 | if (this.singleDatePicker || (this.endDate && (this.startDate.isBefore(this.endDate) || this.startDate.isSame(this.endDate)))) { 996 | this.container.find('button.applyBtn').removeAttr('disabled'); 997 | } else { 998 | this.container.find('button.applyBtn').attr('disabled', 'disabled'); 999 | } 1000 | 1001 | }, 1002 | 1003 | move: function() { 1004 | var parentOffset = { top: 0, left: 0 }, 1005 | containerTop; 1006 | var parentRightEdge = $(window).width(); 1007 | if (!this.parentEl.is('body')) { 1008 | parentOffset = { 1009 | top: this.parentEl.offset().top - this.parentEl.scrollTop(), 1010 | left: this.parentEl.offset().left - this.parentEl.scrollLeft() 1011 | }; 1012 | parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left; 1013 | } 1014 | 1015 | if (this.drops == 'up') 1016 | containerTop = this.element.offset().top - this.container.outerHeight() - parentOffset.top; 1017 | else 1018 | containerTop = this.element.offset().top + this.element.outerHeight() - parentOffset.top; 1019 | this.container[this.drops == 'up' ? 'addClass' : 'removeClass']('dropup'); 1020 | 1021 | if (this.opens == 'left') { 1022 | this.container.css({ 1023 | top: containerTop, 1024 | right: parentRightEdge - this.element.offset().left - this.element.outerWidth(), 1025 | left: 'auto' 1026 | }); 1027 | if (this.container.offset().left < 0) { 1028 | this.container.css({ 1029 | right: 'auto', 1030 | left: 9 1031 | }); 1032 | } 1033 | } else if (this.opens == 'center') { 1034 | this.container.css({ 1035 | top: containerTop, 1036 | left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2 1037 | - this.container.outerWidth() / 2, 1038 | right: 'auto' 1039 | }); 1040 | if (this.container.offset().left < 0) { 1041 | this.container.css({ 1042 | right: 'auto', 1043 | left: 9 1044 | }); 1045 | } 1046 | } else { 1047 | this.container.css({ 1048 | top: containerTop, 1049 | left: this.element.offset().left - parentOffset.left, 1050 | right: 'auto' 1051 | }); 1052 | if (this.container.offset().left + this.container.outerWidth() > $(window).width()) { 1053 | this.container.css({ 1054 | left: 'auto', 1055 | right: 0 1056 | }); 1057 | } 1058 | } 1059 | }, 1060 | 1061 | show: function(e) { 1062 | if (this.isShowing) return; 1063 | // Create a click proxy that is private to this instance of datepicker, for unbinding 1064 | this._outsideClickProxy = $.proxy(function(e) { this.outsideClick(e); }, this); 1065 | 1066 | // Bind global datepicker mousedown for hiding and 1067 | $(document) 1068 | .on('mousedown.qlik-daterangepicker', this._outsideClickProxy) 1069 | // also support mobile devices 1070 | .on('touchend.qlik-daterangepicker', this._outsideClickProxy) 1071 | // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them 1072 | .on('click.qlik-daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy) 1073 | // and also close when focus changes to outside the picker (eg. tabbing between controls) 1074 | .on('focusin.qlik-daterangepicker', this._outsideClickProxy); 1075 | 1076 | // Reposition the picker if the window is resized while it's open 1077 | $(window).on('resize.qlik-daterangepicker', $.proxy(function(e) { this.move(e); }, this)); 1078 | 1079 | this.oldStartDate = this.startDate.clone(); 1080 | this.oldEndDate = this.endDate.clone(); 1081 | 1082 | this.updateView(); 1083 | this.container.show(); 1084 | this.move(); 1085 | this.element.trigger('show.qlik-daterangepicker', this); 1086 | this.isShowing = true; 1087 | }, 1088 | 1089 | hide: function(e) { 1090 | if (!this.isShowing) return; 1091 | 1092 | //incomplete date selection, revert to last values 1093 | if (!this.endDate) { 1094 | this.startDate = this.oldStartDate.clone(); 1095 | this.endDate = this.oldEndDate.clone(); 1096 | } 1097 | 1098 | //if a new date range was selected, invoke the user callback function 1099 | //or if applyclicked 1100 | if (this.applyClicked || (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate))) 1101 | this.callback(this.startDate, this.endDate, this.chosenLabel); 1102 | 1103 | //if picker is attached to a text input, update it 1104 | this.updateElement(); 1105 | 1106 | $(document).off('.qlik-daterangepicker'); 1107 | $(window).off('.qlik-daterangepicker'); 1108 | this.container.removeClass('in-selection'); 1109 | this.applyClicked = false; 1110 | this.container.hide(); 1111 | this.element.trigger('hide.qlik-daterangepicker', this); 1112 | this.isShowing = false; 1113 | }, 1114 | 1115 | toggle: function(e) { 1116 | if (this.isShowing) { 1117 | this.hide(); 1118 | } else { 1119 | this.show(); 1120 | } 1121 | }, 1122 | 1123 | outsideClick: function(e) { 1124 | var target = $(e.target); 1125 | // if the page is clicked anywhere except within the daterangerpicker/button 1126 | // itself then call this.hide() 1127 | if ( 1128 | // ie modal dialog fix 1129 | e.type == "focusin" || 1130 | target.closest(this.element).length || 1131 | target.closest(this.container).length || 1132 | target.closest('.calendar-table').length 1133 | ) return; 1134 | this.hide(); 1135 | }, 1136 | 1137 | showCalendars: function() { 1138 | this.container.addClass('show-calendar'); 1139 | this.move(); 1140 | this.element.trigger('showCalendar.qlik-daterangepicker', this); 1141 | }, 1142 | 1143 | hideCalendars: function() { 1144 | this.container.removeClass('show-calendar'); 1145 | this.element.trigger('hideCalendar.qlik-daterangepicker', this); 1146 | }, 1147 | 1148 | hoverRange: function(e) { 1149 | //ignore mouse movements while an above-calendar text input has focus 1150 | if (this.container.find('input[name=qlik-daterangepicker_start]').is(":focus") || this.container.find('input[name=qlik-daterangepicker_end]').is(":focus")) 1151 | return; 1152 | 1153 | var label = e.target.innerHTML; 1154 | if (label == this.locale.customRangeLabel) { 1155 | this.updateView(); 1156 | } else { 1157 | var dates = this.ranges[label]; 1158 | this.container.find('input[name=qlik-daterangepicker_start]').val(dates[0].format(this.locale.format)); 1159 | 1160 | if (this.endDate) { 1161 | this.container.find('input[name=qlik-daterangepicker_end]').val(dates[1].format(this.locale.format)); 1162 | } 1163 | } 1164 | 1165 | }, 1166 | 1167 | clickRange: function(e) { 1168 | if (this.preventSelections) return; 1169 | 1170 | var label = e.target.innerHTML; 1171 | this.chosenLabel = label; 1172 | if (label == this.locale.customRangeLabel) { 1173 | this.showCalendars(); 1174 | } else { 1175 | var dates = this.ranges[label]; 1176 | this.startDate = dates[0]; 1177 | this.endDate = dates[1]; 1178 | 1179 | if (!this.timePicker) { 1180 | this.startDate.startOf('day'); 1181 | this.endDate.endOf('day'); 1182 | } 1183 | 1184 | this.hideCalendars(); 1185 | this.clickApply(); 1186 | } 1187 | }, 1188 | 1189 | clickPrev: function(e) { 1190 | if (this.preventSelections) return; 1191 | var cal = $(e.target).parents('.calendar'); 1192 | if (cal.hasClass('dpleft')) { 1193 | this.leftCalendar.month.subtract(1, 'month'); 1194 | if (this.linkedCalendars) 1195 | this.rightCalendar.month.subtract(1, 'month'); 1196 | } else { 1197 | this.rightCalendar.month.subtract(1, 'month'); 1198 | } 1199 | this.updateCalendars(); 1200 | }, 1201 | 1202 | clickNext: function(e) { 1203 | if (this.preventSelections) return; 1204 | 1205 | var cal = $(e.target).parents('.calendar'); 1206 | if (cal.hasClass('dpleft')) { 1207 | this.leftCalendar.month.add(1, 'month'); 1208 | } else { 1209 | this.rightCalendar.month.add(1, 'month'); 1210 | if (this.linkedCalendars) 1211 | this.leftCalendar.month.add(1, 'month'); 1212 | } 1213 | this.updateCalendars(); 1214 | }, 1215 | 1216 | hoverDate: function(e) { 1217 | //ignore mouse movements while an above-calendar text input has focus 1218 | if (this.container.find('input[name=qlik-daterangepicker_start]').is(":focus") || this.container.find('input[name=qlik-daterangepicker_end]').is(":focus")) 1219 | return; 1220 | 1221 | //ignore dates that can't be selected 1222 | if (!$(e.target).hasClass('available')) return; 1223 | 1224 | //have the text inputs above calendars reflect the date being hovered over 1225 | var title = $(e.target).attr('data-title'); 1226 | var row = title.substr(1, 1); 1227 | var col = title.substr(3, 1); 1228 | var cal = $(e.target).parents('.calendar'); 1229 | var date = cal.hasClass('dpleft') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; 1230 | 1231 | if (this.endDate) { 1232 | this.container.find('input[name=qlik-daterangepicker_start]').val(date.format(this.locale.format)); 1233 | } else { 1234 | this.container.find('input[name=qlik-daterangepicker_end]').val(date.format(this.locale.format)); 1235 | } 1236 | 1237 | //highlight the dates between the start date and the date being hovered as a potential end date 1238 | var leftCalendar = this.leftCalendar; 1239 | var rightCalendar = this.rightCalendar; 1240 | var startDate = this.startDate; 1241 | if (!this.endDate) { 1242 | this.container.find('.calendar td').each(function(index, el) { 1243 | 1244 | //skip week numbers, only look at dates 1245 | if ($(el).hasClass('week')) return; 1246 | //skip empty cells 1247 | if ($(el).hasClass('empty')) return; 1248 | 1249 | var title = $(el).attr('data-title'); 1250 | var row = title.substr(1, 1); 1251 | var col = title.substr(3, 1); 1252 | var cal = $(el).parents('.calendar'); 1253 | var dt = cal.hasClass('dpleft') ? leftCalendar.calendar[row][col] : rightCalendar.calendar[row][col]; 1254 | 1255 | if (dt.isAfter(startDate) && dt.isBefore(date)) { 1256 | $(el).addClass('in-range'); 1257 | } else { 1258 | $(el).removeClass('in-range'); 1259 | } 1260 | 1261 | }); 1262 | } 1263 | 1264 | }, 1265 | 1266 | clickDate: function(e) { 1267 | if (this.preventSelections) return; 1268 | if (!$(e.target).hasClass('available')) return; 1269 | 1270 | var title = $(e.target).attr('data-title'); 1271 | var row = title.substr(1, 1); 1272 | var col = title.substr(3, 1); 1273 | var cal = $(e.target).parents('.calendar'); 1274 | var date = cal.hasClass('dpleft') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; 1275 | 1276 | // 1277 | // this function needs to do a few things: 1278 | // * alternate between selecting a start and end date for the range, 1279 | // * if the time picker is enabled, apply the hour/minute/second from the select boxes to the clicked date 1280 | // * if autoapply is enabled, and an end date was chosen, apply the selection 1281 | // * if single date picker mode, and time picker isn't enabled, apply the selection immediately 1282 | // 1283 | 1284 | if (this.endDate || date.isBefore(this.startDate)) { 1285 | if (this.timePicker) { 1286 | var hour = parseInt(this.container.find('.dpleft .hourselect').val(), 10); 1287 | if (!this.timePicker24Hour) { 1288 | var ampm = cal.find('.ampmselect').val(); 1289 | if (ampm === 'PM' && hour < 12) 1290 | hour += 12; 1291 | if (ampm === 'AM' && hour === 12) 1292 | hour = 0; 1293 | } 1294 | var minute = parseInt(this.container.find('.dpleft .minuteselect').val(), 10); 1295 | var second = this.timePickerSeconds ? parseInt(this.container.find('.dpleft .secondselect').val(), 10) : 0; 1296 | date = date.clone().hour(hour).minute(minute).second(second); 1297 | } 1298 | this.endDate = null; 1299 | this.setStartDate(date.clone()); 1300 | } else { 1301 | if (this.timePicker) { 1302 | var hour = parseInt(this.container.find('.dpright .hourselect').val(), 10); 1303 | if (!this.timePicker24Hour) { 1304 | var ampm = this.container.find('.dpright .ampmselect').val(); 1305 | if (ampm === 'PM' && hour < 12) 1306 | hour += 12; 1307 | if (ampm === 'AM' && hour === 12) 1308 | hour = 0; 1309 | } 1310 | var minute = parseInt(this.container.find('.dpright .minuteselect').val(), 10); 1311 | var second = this.timePickerSeconds ? parseInt(this.container.find('.dpright .secondselect').val(), 10) : 0; 1312 | date = date.clone().hour(hour).minute(minute).second(second); 1313 | } 1314 | this.setEndDate(date.clone()); 1315 | if (this.autoApply) { 1316 | if (this.isQlikCloud()) { 1317 | this.clickApply() 1318 | } else { 1319 | this.clickApplyNoData(e); 1320 | } 1321 | } 1322 | } 1323 | 1324 | if (this.singleDatePicker) { 1325 | this.setEndDate(this.startDate); 1326 | if (!this.timePicker) { 1327 | if (this.isQlikCloud()) { 1328 | this.clickApply() 1329 | } else { 1330 | this.clickApplyNoData(e); 1331 | } 1332 | } 1333 | } 1334 | 1335 | this.updateView(); 1336 | }, 1337 | 1338 | clickApplyNoData: function(e) { 1339 | if (this.container.find(".in-range").hasClass("stateO") || 1340 | this.container.find(".in-range").hasClass("stateA") || 1341 | this.container.find(".in-range").hasClass("stateX") || 1342 | this.container.find(".in-range").hasClass("stateS")) 1343 | { 1344 | this.clickApply(); 1345 | } else if (this.container.find(".start-date").hasClass("nodata")) { 1346 | if ($(e.target).hasClass('nodata')) { 1347 | this.container.find(".error_nodata").css("display", "block"); 1348 | } else { 1349 | this.clickApply(); 1350 | } 1351 | } else { 1352 | this.clickApply(); 1353 | } 1354 | }, 1355 | 1356 | clickApply: function(e) { 1357 | if (this.preventSelections) return; 1358 | // set applyClicked to true so we know if 1359 | // startdate and enddate are selected or being set to default if nothing selected 1360 | // or we hide the date picker when clicking outside to cancel 1361 | this.applyClicked = true; 1362 | this.hide(); 1363 | this.element.trigger('apply.qlik-daterangepicker', this); 1364 | }, 1365 | 1366 | clickCancel: function(e) { 1367 | if (this.preventSelections) return; 1368 | this.startDate = this.oldStartDate; 1369 | this.endDate = this.oldEndDate; 1370 | this.hide(); 1371 | this.element.trigger('cancel.qlik-daterangepicker', this); 1372 | }, 1373 | 1374 | monthOrYearChanged: function(e) { 1375 | var isLeft = $(e.target).closest('.calendar').hasClass('dpleft'), 1376 | leftOrRight = isLeft ? 'left' : 'right', 1377 | cal = this.container.find('.calendar.'+leftOrRight); 1378 | 1379 | // Month must be Number for new moment versions 1380 | var month = parseInt(cal.find('.monthselect').val(), 10); 1381 | var year = cal.find('.yearselect').val(); 1382 | 1383 | if (!isLeft) { 1384 | if (year < this.startDate.year() || (year == this.startDate.year() && month < this.startDate.month())) { 1385 | month = this.startDate.month(); 1386 | year = this.startDate.year(); 1387 | } 1388 | } 1389 | 1390 | if (this.minDate) { 1391 | if (year < this.minDate.year() || (year == this.minDate.year() && month < this.minDate.month())) { 1392 | month = this.minDate.month(); 1393 | year = this.minDate.year(); 1394 | } 1395 | } 1396 | 1397 | if (this.maxDate) { 1398 | if (year > this.maxDate.year() || (year == this.maxDate.year() && month > this.maxDate.month())) { 1399 | month = this.maxDate.month(); 1400 | year = this.maxDate.year(); 1401 | } 1402 | } 1403 | 1404 | if (isLeft) { 1405 | this.leftCalendar.month.month(month).year(year); 1406 | if (this.linkedCalendars) 1407 | this.rightCalendar.month = this.leftCalendar.month.clone().add(1, 'month'); 1408 | } else { 1409 | this.rightCalendar.month.month(month).year(year); 1410 | if (this.linkedCalendars) 1411 | this.leftCalendar.month = this.rightCalendar.month.clone().subtract(1, 'month'); 1412 | } 1413 | this.updateCalendars(); 1414 | }, 1415 | 1416 | timeChanged: function(e) { 1417 | 1418 | var cal = $(e.target).closest('.calendar'), 1419 | isLeft = cal.hasClass('dpleft'); 1420 | 1421 | var hour = parseInt(cal.find('.hourselect').val(), 10); 1422 | var minute = parseInt(cal.find('.minuteselect').val(), 10); 1423 | var second = this.timePickerSeconds ? parseInt(cal.find('.secondselect').val(), 10) : 0; 1424 | 1425 | if (!this.timePicker24Hour) { 1426 | var ampm = cal.find('.ampmselect').val(); 1427 | if (ampm === 'PM' && hour < 12) 1428 | hour += 12; 1429 | if (ampm === 'AM' && hour === 12) 1430 | hour = 0; 1431 | } 1432 | 1433 | if (isLeft) { 1434 | var start = this.startDate.clone(); 1435 | start.hour(hour); 1436 | start.minute(minute); 1437 | start.second(second); 1438 | this.setStartDate(start); 1439 | if (this.singleDatePicker) { 1440 | this.endDate = this.startDate.clone(); 1441 | } else if (this.endDate && this.endDate.format('YYYY-MM-DD') == start.format('YYYY-MM-DD') && this.endDate.isBefore(start)) { 1442 | this.setEndDate(start.clone()); 1443 | } 1444 | } else if (this.endDate) { 1445 | var end = this.endDate.clone(); 1446 | end.hour(hour); 1447 | end.minute(minute); 1448 | end.second(second); 1449 | this.setEndDate(end); 1450 | } 1451 | 1452 | //update the calendars so all clickable dates reflect the new time component 1453 | this.updateCalendars(); 1454 | 1455 | //update the form inputs above the calendars with the new time 1456 | this.updateFormInputs(); 1457 | 1458 | //re-render the time pickers because changing one selection can affect what's enabled in another 1459 | this.renderTimePicker('left'); 1460 | this.renderTimePicker('right'); 1461 | }, 1462 | 1463 | formInputsChanged: function(e) { 1464 | var isRight = $(e.target).closest('.calendar').hasClass('dpright'); 1465 | var start = moment(this.container.find('input[name=qlik-daterangepicker_start]').val(), this.locale.format); 1466 | var end = moment(this.container.find('input[name=qlik-daterangepicker_end]').val(), this.locale.format); 1467 | 1468 | if (start.isValid() && end.isValid()) { 1469 | 1470 | if (isRight && end.isBefore(start)) 1471 | start = end.clone(); 1472 | 1473 | this.setStartDate(start); 1474 | this.setEndDate(end); 1475 | 1476 | if (isRight) { 1477 | this.container.find('input[name=qlik-daterangepicker_start]').val(this.startDate.format(this.locale.format)); 1478 | } else { 1479 | this.container.find('input[name=qlik-daterangepicker_end]').val(this.endDate.format(this.locale.format)); 1480 | } 1481 | 1482 | } 1483 | 1484 | this.updateCalendars(); 1485 | if (this.timePicker) { 1486 | this.renderTimePicker('left'); 1487 | this.renderTimePicker('right'); 1488 | } 1489 | }, 1490 | 1491 | elementChanged: function() { 1492 | if (!this.element.is('input')) return; 1493 | if (!this.element.val().length) return; 1494 | if (this.element.val().length < this.locale.format.length) return; 1495 | 1496 | var dateString = this.element.val().split(this.locale.separator), 1497 | start = null, 1498 | end = null; 1499 | 1500 | if (dateString.length === 2) { 1501 | start = moment(dateString[0], this.locale.format); 1502 | end = moment(dateString[1], this.locale.format); 1503 | } 1504 | 1505 | if (this.singleDatePicker || start === null || end === null) { 1506 | start = moment(this.element.val(), this.locale.format); 1507 | end = start; 1508 | } 1509 | 1510 | if (!start.isValid() || !end.isValid()) return; 1511 | 1512 | this.setStartDate(start); 1513 | this.setEndDate(end); 1514 | this.updateView(); 1515 | }, 1516 | 1517 | keydown: function(e) { 1518 | //hide on tab or enter 1519 | if ((e.keyCode === 9) || (e.keyCode === 13)) { 1520 | this.hide(); 1521 | } 1522 | }, 1523 | 1524 | updateElement: function() { 1525 | if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) { 1526 | this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); 1527 | this.element.trigger('change'); 1528 | } else if (this.element.is('input') && this.autoUpdateInput) { 1529 | this.element.val(this.startDate.format(this.locale.format)); 1530 | this.element.trigger('change'); 1531 | } 1532 | }, 1533 | 1534 | remove: function() { 1535 | this.container.remove(); 1536 | this.element.off('.qlik-daterangepicker'); 1537 | this.element.removeData(); 1538 | } 1539 | 1540 | }; 1541 | 1542 | $.fn.qlikdaterangepicker = function(options, callback) { 1543 | this.each(function() { 1544 | var el = $(this); 1545 | if (el.data('qlik-daterangepicker')) 1546 | el.data('qlik-daterangepicker').remove(); 1547 | el.data('qlik-daterangepicker', new DateRangePicker(el, options, callback)); 1548 | }); 1549 | return this; 1550 | }; 1551 | 1552 | return DateRangePicker; 1553 | 1554 | })); 1555 | --------------------------------------------------------------------------------