CASS-487 Group members work

CASS-487 Group members work

<template>

                User Groups

                        Membership List

                                create new group

                    <cass-panel>
                        <cass-panel-item
                            :depth="0"
                            :label="group.name"
                            :nodes="group.subGroups"
                            :id="group.id"
                            v-for="group in userGroupDisplayList"
                            @showDetails="showGroupDetailsById"
                            :key="group" />
                    </cass-panel>

                                <template>

                                        Membership List

                                             No user groups are available to you

 0"
                                        class="section box py-4 px-4">

                                                            name

                                                            email

                                                            membership

                                                            {{ member.name }}

                                                            {{ member.email }}

 0">
                                                                Manager of

                                                                     0">,

                                                                        {{memMgrOf.name}}

 0">
                                                                Member of

                                                                     0">,

                                                                        {{memMemOf.name}}

                                </template>

                                <template>

                                        {{ currentUserGroupName }}

                                        Group Name
                                        <input
                                            @change="setCurrentUserGroupAsChanged"
                                            type="text"
                                            class="input"
                                            v-model="currentUserGroupName">

                                </template>

                                 1">

                                                {{ lineageObj.name }}

                                         0">
                                            {{ currentUserGroupDescription }}

                                            Group Description

                                        Group Description
                                        <input
                                            @change="setCurrentUserGroupAsChanged"
                                            type="text"
                                            class="input"
                                            v-model="currentUserGroupDescription">

                                                Group Members

                                                            add group members

                                                        name

                                                        email

                                                        role

                                                        {{ manager.name }}

                                                        {{ manager.email }}

                                                        manager

                                                                    change role

                                                        {{ member.name }}

                                                        {{ member.email }}

                                                        member

                                                                    change role

                                            save

                                            cancel

                                            create sub-group

                                            delete

                        Add members to '{{ currentUserGroupName }}'
                        <button
                            class="delete is-pulled-right"
                            aria-label="close"
                            @click="closeAddGroupMemberModal" />

                        <input
                            type="text"
                            class="input"
                            v-model="addMemberPersonFilter"
                            placeholder="search for person...">

                         No group members available. Users must be managers or members of any parent groups in order to be available for sub-groups.

 0">

                            Available members

                                        name
                                        email

                                                <input
                                                    :id="prs.shortId()"
                                                    :value="prs.shortId()"
                                                    name="prs.shortId()"
                                                    type="checkbox"
                                                    title="Add as member"
                                                    @change="removeFromSelectedNewManagers(prs.shortId())"
                                                    v-model="selectedNewMembers">

                                                <input
                                                    :id="prs.shortId()"
                                                    :value="prs.shortId()"
                                                    name="prs.shortId()"
                                                    type="checkbox"
                                                    title="Add as manager"
                                                    @change="removeFromSelectedNewMembers(prs.shortId())"
                                                    v-model="selectedNewManagers">

                                         {{ prs.getName() }}
                                         {{ prs.email }}

 0 || selectedNewManagers.length > 0"
                            class="button is-outlined is-primary is-small"
                            @click="applySelectedNewMembers"
                            title="Apply new members">

                                apply

                                cancel

</template>

<template>

                User Groups

                        Membership List

                                create new group

                    <cass-panel>
                        <cass-panel-item
                            :depth="0"
                            :label="group.name"
                            :nodes="group.subGroups"
                            :id="group.id"
                            v-for="group in userGroupDisplayList"
                            @showDetails="showGroupDetailsById"
                            :key="group" />
                    </cass-panel>

                                <template>

                                        Membership List

                                             No user groups are available to you

 0"
                                        class="section box py-4 px-4">

                                                            name

                                                            email

                                                            membership

                                                            {{ member.name }}

                                                            {{ member.email }}

 0">
                                                                Manager of

                                                                     0">,

                                                                        {{memMgrOf.name}}

 0">
                                                                Member of

                                                                     0">,

                                                                        {{memMemOf.name}}

                                </template>

                                <template>

                                        {{ currentUserGroupName }}

                                        Group Name
                                        <input
                                            @change="setCurrentUserGroupAsChanged"
                                            type="text"
                                            class="input"
                                            v-model="currentUserGroupName">

                                </template>

                                 1">

                                                {{ lineageObj.name }}

                                         0">
                                            {{ currentUserGroupDescription }}

                                            Group Description

                                        Group Description
                                        <input
                                            @change="setCurrentUserGroupAsChanged"
                                            type="text"
                                            class="input"
                                            v-model="currentUserGroupDescription">

                                        Group Members

                                                        name

                                                        email

                                                        role

                                                        {{ manager.name }}

                                                        {{ manager.email }}

                                                        manager

                                                                    change role

                                                        {{ member.name }}

                                                        {{ member.email }}

                                                        member

                                                                    change role

                                            save

                                            cancel

                                            create sub-group

                                            delete

</template>


[#0] Fixes display of progress bar percentage maximum on client

<template>

        <transition name="translate">

Beta

                    <el-progress type="circle" :percentage="progress"></el-progress>
                    <p v-if="progress < 100">Provide more information to Zeus to get a prediction on your case

See Latest Prediction

FEEDBACK

RESET CONVERSATION

        </transition>

        <transition name="el-zoom-in-center">

                <el-row>
                    <el-col :sm="{span: 24, offset: 0}">

                    </el-col>
                    <el-col :sm="{span: 1, offset: 23}">

                    </el-col>
                    <el-col :sm="{span: 24, offset: 0}">

Here is our prediction after analyzing {{ report.data_set }} Régie du logement"s precedents:

                    </el-col>
                    <el-col :sm="{span: 5, offset: 1}">

                            <el-progress type="circle" :percentage="report.accuracy" :stroke-width="30" :width="250"></el-progress>

Prediction Probability

                    </el-col>
                    <el-col :sm="{span: 8, offset: 2}">

                            <el-carousel indicator-position="outside">

                                  <el-carousel-item :key="key" :name="key">

                                  </el-carousel-item>

No Regressor Available

                            </el-carousel>

                    </el-col>
                    <el-col :sm="{span: 5, offset: 2}">

Case Verdict

                                <el-col :sm="{span: 20, offset: 0}">

{{ key }}

                                </el-col>
                                <el-col :sm="{span: 2, offset: 2}">
                                    <icon name="check-square" class="outcome-check"></icon>
                                </el-col>

                    </el-col>
                    <el-col :sm="{span: 22, offset: 1}">

Here are {{ report.similar_case }} most similar precendents to your case

                                <el-table :data="report.precedent_table" @header-click="openSimilarPrecedent" stripe>

                                        <el-table-column prop="name" align="center" fixed="left"></el-table-column>

                                        <el-table-column :prop="header" :label="header" label-class-name="precedent-table-header" align="center"></el-table-column>

                                </el-table>

                    </el-col>
                    <el-col :sm="{span: 22, offset: 1}">

Need professional legal support? Contact us at 514-123-4567

FEEDBACK

RESET CONVERSATION

                    </el-col>
                </el-row>

        </transition>

        <el-dialog title="Feedback" :visible.sync="openFeedbackModal">
            <textarea id="feedback-input" v-model="feedback">
            </textarea>

                <el-button v-on:click="openFeedbackModal = false">Cancel</el-button>
                <el-button type="primary" v-on:click="submitFeedback()">Submit</el-button>

        </el-dialog>

</template>


Fix an error code being undefined

Fix an error code being undefined

even when an error retrieved.
Error code can be accessed inside
of status object.

<template>

    <ScrollingHeader
      :height="appbarHeight"
      :scrollPosition="scrollPosition"
      :alwaysVisible="fixedAppBar"
    >
      <ImmersiveToolbar
        v-if="immersivePage && !fullScreen"
        :appBarTitle="toolbarTitle || appBarTitle"
        :icon="immersivePageIcon"
        :route="immersivePageRoute"
        :primary="immersivePagePrimary"
        :height="headerHeight"
        @nav-icon-click="$emit('navIconClick')"
      />
      <AppBar
        v-else-if="!immersivePage && !fullScreen"
        ref="appBar"
        class="app-bar"
        :title="toolbarTitle || appBarTitle"
        :height="headerHeight"
        :navShown="navShown"
        @toggleSideNav="navShown = !navShown"
        @showLanguageModal="languageModalShown = true"
      >
        <slot slot="totalPointsMenuItem" name="totalPointsMenuItem"></slot>

          <slot name="app-bar-actions"></slot>

        <slot
          v-if="showSubNav"
          slot="sub-nav"
          name="sub-nav"
        >
        </slot>
      </AppBar>
      <KLinearLoader
        v-if="loading && !fullScreen"
        class="loader"
        :style="{top: `${appbarHeight}px`}"
        type="indeterminate"
        :delay="false"
      />
    </ScrollingHeader>

    <SideNav
      :navShown="navShown"
      :headerHeight="headerHeight"
      :width="navWidth"
      @toggleSideNav="navShown=!navShown"
    />

{{ contentComponentName }}

{{ routePath }}

      <AuthMessage
        v-if="notAuthorized"
        :authorizedRole="authorizedRole"
        :header="authorizationErrorHeader"
        :details="authorizationErrorDetails"
      />
      <AppError v-else-if="error" />
      <slot v-else></slot>

    <GlobalSnackbar />
    <UpdateNotification
      v-if="!loading && showNotification && !busy"
      :id="mostRecentNotification.id"
      :title="mostRecentNotification.title"
      :msg="mostRecentNotification.msg"
      :linkText="mostRecentNotification.linkText"
      :linkUrl="mostRecentNotification.linkUrl"
      @closeModal="dismissUpdateModal"
    />
    <LanguageSwitcherModal
      v-if="languageModalShown"
      :style="{ color: $coreTextDefault }"
      @close="languageModalShown = false"
    />

</template>


simplify import

simplify import

<template>

  <div>
    <core-base
      :immersivePage="currentPageIsImmersive"
      :topLevelPageName="topLevelPageName"
      :appBarTitle="appBarTitle"
      :immersivePageIcon="immersivePageIcon"
      :immersivePageRoute="toolbarRoute"
      :immersivePagePrimary="immersivePagePrimary"
    >

      <template v-if="showCoachNav">
        <nav-title
          :className="className"
          :classId="classId"
          :username="usernameForCurrentScope"
        />
        <top-nav class="top-nav" />
      </template>

      <!-- TODO need a better solution for passing in authMessage -->
      <component authorizedRole="adminOrCoach" :is="currentPage" />

    </core-base>
  </div>

</template>

<script>

  import { PageNames } from '../constants';
  import { UserScopes } from '../reportConstants';
  import { className } from '../state/getters/main';
  import { isAdmin, isCoach, isSuperuser } from 'kolibri.coreVue.vuex.getters';
  import { TopLevelPageNames } from 'kolibri.coreVue.vuex.constants';
  import authMessage from 'kolibri.coreVue.components.authMessage';
  import topNav from './top-nav';
  import classListPage from './class-list-page';
  import examsPage from './exams-page';
  import createExamPage from './create-exam-page';
  import examReportPage from './exam-report-page';
  import examReportDetailPage from './exam-report-detail-page';
  import groupsPage from './groups-page';
  import coreBase from 'kolibri.coreVue.components.coreBase';
  import learnerExerciseDetailPage from './reports/learner-exercise-detail-page';
  import recentItemsPage from './reports/recent-items-page';
  import channelListPage from './reports/channel-list-page';
  import itemListPage from './reports/item-list-page';
  import learnerListPage from './reports/learner-list-page';
  import navTitle from './nav-title';

  // lessons
  import { LessonsPageNames } from '../lessonsConstants';
  import LessonsRootPage from './lessons/LessonsRootPage';
  import LessonSummaryPage from './lessons/LessonSummaryPage';
  import LessonResourceSelectionPage from './lessons/LessonResourceSelectionPage';
  import LessonContentPreviewPage from './lessons/LessonContentPreviewPage';
  import LessonResourceUserReportPage from './reports/learner-exercise-detail-page/learner-exercise-report';
  import LessonResourceUserSummaryPage from './lessons/LessonResourceUserSummaryPage';

  // IDEA set up routenames that all use the same PageName instead of doing this?
  // See Content Preview routes in app.js + PageName handling here
  const selectionPages = [LessonsPageNames.SELECTION, LessonsPageNames.SELECTION_ROOT];
  const resourceUserPages = [
    LessonsPageNames.RESOURCE_USER_SUMMARY,
    LessonsPageNames.RESOURCE_USER_REPORT,
  ];

  const immersivePages = [
    ...selectionPages,
    ...resourceUserPages,
    LessonsPageNames.CONTENT_PREVIEW,
    LessonsPageNames.RESOURCE_CLASSROOM_REPORT,
  ];

  export default {
    name: 'coachRoot',
    $trs: {
      coachToolbarHeader: 'Coach',
      selectPageToolbarHeader: 'Select resources',
      resourceUserPageToolbarHeader: 'Lesson Report Details',
      previewContentPageToolbarHeader: 'Preview resources',
    },
    components: {
      authMessage,
      topNav,
      classListPage,
      examsPage,
      createExamPage,
      // reports
      examReportPage,
      examReportDetailPage,
      groupsPage,
      coreBase,
      learnerExerciseDetailPage,
      recentItemsPage,
      channelListPage,
      itemListPage,
      learnerListPage,
      navTitle,
      // lessons
      LessonsRootPage,
      LessonSummaryPage,
      LessonResourceSelectionPage,
      LessonContentPreviewPage,
      LessonResourceUserSummaryPage,
      LessonResourceUserReportPage,
    },
    computed: {
      topLevelPageName: () => TopLevelPageNames.COACH,
      currentPage() {
        const pageNameToComponentMap = {
          [PageNames.CLASS_LIST]: 'class-list-page',
          [PageNames.EXAMS]: 'exams-page',
          [PageNames.GROUPS]: 'groups-page',
          [PageNames.CREATE_EXAM]: 'create-exam-page',
          // reports
          [PageNames.RECENT_CHANNELS]: 'channel-list-page',
          [PageNames.RECENT_ITEMS_FOR_CHANNEL]: 'recent-items-page',
          [PageNames.RECENT_LEARNERS_FOR_ITEM]: 'learner-list-page',
          [PageNames.RECENT_LEARNER_ITEM_DETAILS]: 'learner-exercise-detail-page',
          [PageNames.TOPIC_CHANNELS]: 'channel-list-page',
          [PageNames.TOPIC_CHANNEL_ROOT]: 'item-list-page',
          [PageNames.TOPIC_ITEM_LIST]: 'item-list-page',
          [PageNames.TOPIC_LEARNERS_FOR_ITEM]: 'learner-list-page',
          [PageNames.TOPIC_LEARNER_ITEM_DETAILS]: 'learner-exercise-detail-page',
          [PageNames.LEARNER_LIST]: 'learner-list-page',
          [PageNames.LEARNER_CHANNELS]: 'channel-list-page',
          [PageNames.LEARNER_CHANNEL_ROOT]: 'item-list-page',
          [PageNames.LEARNER_ITEM_LIST]: 'item-list-page',
          [PageNames.LEARNER_ITEM_DETAILS]: 'learner-exercise-detail-page',
          [PageNames.EXAM_REPORT]: 'exam-report-page',
          [PageNames.EXAM_REPORT_DETAIL]: 'exam-report-detail-page',

          // lessons
          [LessonsPageNames.ROOT]: 'LessonsRootPage',
          [LessonsPageNames.SUMMARY]: 'LessonSummaryPage',
          [LessonsPageNames.SELECTION_ROOT]: 'LessonResourceSelectionPage',
          [LessonsPageNames.SELECTION]: 'LessonResourceSelectionPage',
          [LessonsPageNames.CONTENT_PREVIEW]: 'LessonContentPreviewPage',
          [LessonsPageNames.RESOURCE_USER_SUMMARY]: 'LessonResourceUserSummaryPage',
          [LessonsPageNames.RESOURCE_USER_REPORT]: 'LessonResourceUserReportPage',
        };
        if (!this.userCanAccessPage) {
          // TODO better solution
          return 'authMessage';
        }
        return pageNameToComponentMap[this.pageName];
      },
      showCoachNav() {
        return (
          this.pageName !== PageNames.CLASS_LIST &&
          this.userCanAccessPage &&
          !this.currentPageIsImmersive
        );
      },
      currentPageIsImmersive() {
        return immersivePages.includes(this.pageName);
      },
      userCanAccessPage() {
        return this.isCoach || this.isAdmin || this.isSuperuser;
      },
      appBarTitle() {
        if (this.currentPageIsImmersive) {
          if (this.pageName === LessonsPageNames.CONTENT_PREVIEW) {
            return this.$tr('previewContentPageToolbarHeader');
          }
          if (selectionPages.includes(this.pageName)) {
            return this.$tr('selectPageToolbarHeader');
          }
          if (resourceUserPages.includes(this.pageName)) {
            return this.$tr('resourceUserPageToolbarHeader');
          }
        }
        return this.$tr('coachToolbarHeader');
      },
      immersivePageIcon() {
        const backButtonPages = [LessonsPageNames.CONTENT_PREVIEW, ...resourceUserPages];
        if (backButtonPages.includes(this.pageName)) {
          return 'arrow_back';
        }
        return 'close';
      },
      immersivePagePrimary() {
        // TODO going to need to set a backgrund color
        if (this.pageName === LessonsPageNames.CONTENT_PREVIEW) {
          return false;
        }
        return true;
      },
      usernameForCurrentScope() {
        if (this.pageState.userScope === UserScopes.USER) {
          return this.pageState.userScopeName;
        }
        return null;
      },
    },
    vuex: {
      getters: {
        toolbarRoute: state => state.pageState.toolbarRoute,
        pageName: state => state.pageName,
        isAdmin,
        isCoach,
        isSuperuser,
        className,
        classList: state => state.classList,
        classId: state => state.classId,
        isLoading: state => state.core.loading,
        pageState: state => state.pageState,
      },
    },
  };

</script>

<style lang="stylus" scoped>

  @require '~kolibri.styles.definitions'

  .top-nav
    margin-bottom: 32px

</style>

<template>

  <div>
    <core-base
      :immersivePage="currentPageIsImmersive"
      :topLevelPageName="topLevelPageName"
      :appBarTitle="appBarTitle"
      :immersivePageIcon="immersivePageIcon"
      :immersivePageRoute="toolbarRoute"
      :immersivePagePrimary="immersivePagePrimary"
    >

      <template v-if="showCoachNav">
        <nav-title
          :className="className"
          :classId="classId"
          :username="usernameForCurrentScope"
        />
        <top-nav class="top-nav" />
      </template>

      <!-- TODO need a better solution for passing in authMessage -->
      <component authorizedRole="adminOrCoach" :is="currentPage" />

    </core-base>
  </div>

</template>

<script>

  import { PageNames } from '../constants';
  import * as ReportConstants from '../reportConstants';
  import { className } from '../state/getters/main';
  import { isAdmin, isCoach, isSuperuser } from 'kolibri.coreVue.vuex.getters';
  import { TopLevelPageNames } from 'kolibri.coreVue.vuex.constants';
  import authMessage from 'kolibri.coreVue.components.authMessage';
  import topNav from './top-nav';
  import classListPage from './class-list-page';
  import examsPage from './exams-page';
  import createExamPage from './create-exam-page';
  import examReportPage from './exam-report-page';
  import examReportDetailPage from './exam-report-detail-page';
  import groupsPage from './groups-page';
  import coreBase from 'kolibri.coreVue.components.coreBase';
  import learnerExerciseDetailPage from './reports/learner-exercise-detail-page';
  import recentItemsPage from './reports/recent-items-page';
  import channelListPage from './reports/channel-list-page';
  import itemListPage from './reports/item-list-page';
  import learnerListPage from './reports/learner-list-page';
  import navTitle from './nav-title';

  // lessons
  import { LessonsPageNames } from '../lessonsConstants';
  import LessonsRootPage from './lessons/LessonsRootPage';
  import LessonSummaryPage from './lessons/LessonSummaryPage';
  import LessonResourceSelectionPage from './lessons/LessonResourceSelectionPage';
  import LessonContentPreviewPage from './lessons/LessonContentPreviewPage';
  import LessonResourceUserReportPage from './reports/learner-exercise-detail-page/learner-exercise-report';
  import LessonResourceUserSummaryPage from './lessons/LessonResourceUserSummaryPage';

  // IDEA set up routenames that all use the same PageName instead of doing this?
  // See Content Preview routes in app.js + PageName handling here
  const selectionPages = [LessonsPageNames.SELECTION, LessonsPageNames.SELECTION_ROOT];
  const resourceUserPages = [
    LessonsPageNames.RESOURCE_USER_SUMMARY,
    LessonsPageNames.RESOURCE_USER_REPORT,
  ];

  const immersivePages = [
    ...selectionPages,
    ...resourceUserPages,
    LessonsPageNames.CONTENT_PREVIEW,
    LessonsPageNames.RESOURCE_CLASSROOM_REPORT,
  ];

  export default {
    name: 'coachRoot',
    $trs: {
      coachToolbarHeader: 'Coach',
      selectPageToolbarHeader: 'Select resources',
      resourceUserPageToolbarHeader: 'Lesson Report Details',
      previewContentPageToolbarHeader: 'Preview resources',
    },
    components: {
      authMessage,
      topNav,
      classListPage,
      examsPage,
      createExamPage,
      // reports
      examReportPage,
      examReportDetailPage,
      groupsPage,
      coreBase,
      learnerExerciseDetailPage,
      recentItemsPage,
      channelListPage,
      itemListPage,
      learnerListPage,
      navTitle,
      // lessons
      LessonsRootPage,
      LessonSummaryPage,
      LessonResourceSelectionPage,
      LessonContentPreviewPage,
      LessonResourceUserSummaryPage,
      LessonResourceUserReportPage,
    },
    computed: {
      topLevelPageName: () => TopLevelPageNames.COACH,
      currentPage() {
        const pageNameToComponentMap = {
          [PageNames.CLASS_LIST]: 'class-list-page',
          [PageNames.EXAMS]: 'exams-page',
          [PageNames.GROUPS]: 'groups-page',
          [PageNames.CREATE_EXAM]: 'create-exam-page',
          // reports
          [PageNames.RECENT_CHANNELS]: 'channel-list-page',
          [PageNames.RECENT_ITEMS_FOR_CHANNEL]: 'recent-items-page',
          [PageNames.RECENT_LEARNERS_FOR_ITEM]: 'learner-list-page',
          [PageNames.RECENT_LEARNER_ITEM_DETAILS]: 'learner-exercise-detail-page',
          [PageNames.TOPIC_CHANNELS]: 'channel-list-page',
          [PageNames.TOPIC_CHANNEL_ROOT]: 'item-list-page',
          [PageNames.TOPIC_ITEM_LIST]: 'item-list-page',
          [PageNames.TOPIC_LEARNERS_FOR_ITEM]: 'learner-list-page',
          [PageNames.TOPIC_LEARNER_ITEM_DETAILS]: 'learner-exercise-detail-page',
          [PageNames.LEARNER_LIST]: 'learner-list-page',
          [PageNames.LEARNER_CHANNELS]: 'channel-list-page',
          [PageNames.LEARNER_CHANNEL_ROOT]: 'item-list-page',
          [PageNames.LEARNER_ITEM_LIST]: 'item-list-page',
          [PageNames.LEARNER_ITEM_DETAILS]: 'learner-exercise-detail-page',
          [PageNames.EXAM_REPORT]: 'exam-report-page',
          [PageNames.EXAM_REPORT_DETAIL]: 'exam-report-detail-page',

          // lessons
          [LessonsPageNames.ROOT]: 'LessonsRootPage',
          [LessonsPageNames.SUMMARY]: 'LessonSummaryPage',
          [LessonsPageNames.SELECTION_ROOT]: 'LessonResourceSelectionPage',
          [LessonsPageNames.SELECTION]: 'LessonResourceSelectionPage',
          [LessonsPageNames.CONTENT_PREVIEW]: 'LessonContentPreviewPage',
          [LessonsPageNames.RESOURCE_USER_SUMMARY]: 'LessonResourceUserSummaryPage',
          [LessonsPageNames.RESOURCE_USER_REPORT]: 'LessonResourceUserReportPage',
        };
        if (!this.userCanAccessPage) {
          // TODO better solution
          return 'authMessage';
        }
        return pageNameToComponentMap[this.pageName];
      },
      showCoachNav() {
        return (
          this.pageName !== PageNames.CLASS_LIST &&
          this.userCanAccessPage &&
          !this.currentPageIsImmersive
        );
      },
      currentPageIsImmersive() {
        return immersivePages.includes(this.pageName);
      },
      userCanAccessPage() {
        return this.isCoach || this.isAdmin || this.isSuperuser;
      },
      appBarTitle() {
        if (this.currentPageIsImmersive) {
          if (this.pageName === LessonsPageNames.CONTENT_PREVIEW) {
            return this.$tr('previewContentPageToolbarHeader');
          }
          if (selectionPages.includes(this.pageName)) {
            return this.$tr('selectPageToolbarHeader');
          }
          if (resourceUserPages.includes(this.pageName)) {
            return this.$tr('resourceUserPageToolbarHeader');
          }
        }
        return this.$tr('coachToolbarHeader');
      },
      immersivePageIcon() {
        const backButtonPages = [LessonsPageNames.CONTENT_PREVIEW, ...resourceUserPages];
        if (backButtonPages.includes(this.pageName)) {
          return 'arrow_back';
        }
        return 'close';
      },
      immersivePagePrimary() {
        // TODO going to need to set a backgrund color
        if (this.pageName === LessonsPageNames.CONTENT_PREVIEW) {
          return false;
        }
        return true;
      },
      usernameForCurrentScope() {
        if (this.pageState.userScope === ReportConstants.UserScopes.USER) {
          return this.pageState.userScopeName;
        }
        return null;
      },
    },
    vuex: {
      getters: {
        toolbarRoute: state => state.pageState.toolbarRoute,
        pageName: state => state.pageName,
        isAdmin,
        isCoach,
        isSuperuser,
        className,
        classList: state => state.classList,
        classId: state => state.classId,
        isLoading: state => state.core.loading,
        pageState: state => state.pageState,
      },
    },
  };

</script>

<style lang="stylus" scoped>

  @require '~kolibri.styles.definitions'

  .top-nav
    margin-bottom: 32px

</style>


make book select widget collapsible

make book select widget collapsible

<template>

    Book

<nuxt-link :to="{ query: {book: book['@id']} }">{{ book.name }}</nuxt-link>

</template>


remove: line-height

remove: line-height

<template>
  <section class="entry-content" v-html="post.content.rendered"/>
</template>

<script>
export default {
  name: 'EntryContent',
  props: {
    post: {
      type: Object,
      default: () => {},
      require: true,
    },
  },
  data() {
    return {
      styleElement: null,
    };
  },
  watch: {
    '$route.path': function() {
      this.initStyleElement();
    },
    'post.content.rendered': function() {
      if (!this.post.content.rendered) {
        return;
      }
      this.$nextTick().then(() => {
        if (this.post.hasOwnProperty('attach')) {
          this.setCustomStyle();
          this.setCustomScript();
        }
      });
    },
  },
  created() {
    this.styleElement = document.getElementById('custom_style');
  },
  methods: {
    initStyleElement: function() {
      if (!this.styleElement) {
        let element = document.createElement('style');
        element.id = 'custom_style';
        document.head.appendChild(element);
        this.styleElement = document.getElementById('custom_style');
      }

      this.styleElement.innerHTML = '';
    },
    setCustomStyle: function() {
      this.initStyleElement();
      if (this.post.attach.custom.style) {
        this.styleElement.innerHTML = this.post.attach.custom.style;
      }
    },
    setCustomScript: function() {
      if (this.post.attach.custom.script) {
        eval(this.post.attach.custom.script);
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.entry-content /deep/ {
  margin-bottom: 1rem;

  a {
    padding-bottom: 1px;
    border-bottom: 1px solid $link-border-color;
    color: $link-color;

    &:hover {
      border-color: $link-color;
    }
    &:focus {
      outline: thin dotted;
      outline-offset: -2px;
    }
  }

  .anchor {
    position: absolute;
    right: 100%;
    border: 0;
    color: $grey-400;
    font-family: $font-family-monospace;
    user-select: none;
    @include until($desktop) {
      position: static;
    }
  }

  li + li {
    margin-top: 0.25rem;
  }

  > hr {
    height: 2rem;
    margin: 2rem 0;
    border: 0;
    color: $grey-600;
    font-size: $font-size-h2;
    text-align: center;

    &::before {
      content: '***';
    }
  }
}
</style>

<template>
  <section class="entry-content" v-html="post.content.rendered"/>
</template>

<script>
export default {
  name: 'EntryContent',
  props: {
    post: {
      type: Object,
      default: () => {},
      require: true,
    },
  },
  data() {
    return {
      styleElement: null,
    };
  },
  watch: {
    '$route.path': function() {
      this.initStyleElement();
    },
    'post.content.rendered': function() {
      if (!this.post.content.rendered) {
        return;
      }
      this.$nextTick().then(() => {
        if (this.post.hasOwnProperty('attach')) {
          this.setCustomStyle();
          this.setCustomScript();
        }
      });
    },
  },
  created() {
    this.styleElement = document.getElementById('custom_style');
  },
  methods: {
    initStyleElement: function() {
      if (!this.styleElement) {
        let element = document.createElement('style');
        element.id = 'custom_style';
        document.head.appendChild(element);
        this.styleElement = document.getElementById('custom_style');
      }

      this.styleElement.innerHTML = '';
    },
    setCustomStyle: function() {
      this.initStyleElement();
      if (this.post.attach.custom.style) {
        this.styleElement.innerHTML = this.post.attach.custom.style;
      }
    },
    setCustomScript: function() {
      if (this.post.attach.custom.script) {
        eval(this.post.attach.custom.script);
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.entry-content /deep/ {
  margin-bottom: 1rem;

  a {
    padding-bottom: 1px;
    border-bottom: 1px solid $link-border-color;
    color: $link-color;

    &:hover {
      border-color: $link-color;
    }
    &:focus {
      outline: thin dotted;
      outline-offset: -2px;
    }
  }

  .anchor {
    position: absolute;
    right: 100%;
    border: 0;
    color: $grey-400;
    font-family: $font-family-monospace;
    user-select: none;
    @include until($desktop) {
      position: static;
    }
  }

  li + li {
    margin-top: 0.25rem;
  }

  > hr {
    height: 2rem;
    margin: 2rem 0;
    border: 0;
    color: $grey-600;
    font-size: $font-size-h2;
    line-height: 1;
    text-align: center;

    &::before {
      content: '***';
    }
  }
}
</style>


Remove flexbox centered loading status

Remove flexbox centered loading status

<template>

      <KCircularLoader
        type="indeterminate"
        :delay="false"
      />

{{ $tr('loadingBook') }}

</template>


Formatting downloads

Formatting downloads

<template>

            Download Conversion Files

            {{ displayName }}

            Download Files

List All Downloads

          {{ downloads }}

</template>


added slots

added slots

<template lang="pug">
  li
    a(
      class="sidebar__item"
      v-if="!router"
      v-bind:href="item.href"
      v-on:click="click()"
    )
      v-icon(v-if="item.icon") {{ item.icon }}
      span(v-text="item.text")
      slot
    router-link(
      class="sidebar__item"
      active-class="sidebar__item--active"
      v-bind:exact="item.href === '/'"
      v-bind:to="item.href"
      v-on:click.native="click()"
      v-else
    )
      v-icon(v-if="item.icon") {{ item.icon }}
      span(v-text="item.text")
      slot
</template>

<script>
  import { closest } from '../../util/helpers'

  export default {
    name: 'sidebar-item',

    data () {
      return {
        sidebar: null,
        group: null
      }
    },

    props: {
      item: {
        type: Object,
        required: true
      },

      router: Boolean
    },

    computed: {
      group () {
        return closest.call(this, 'sidebar__group')
      },

      sidebar () {
        return closest.call(this, 'sidebar')
      }
    },

    methods: {
      click () {
        this.$vuetify.bus.pub(`sidebar-group:close:${this.sidebar}`, this.group)
      }
    }
  }
</script>

<template lang="pug">
  li
    a(
      class="sidebar__item"
      v-if="!router"
      v-bind:href="item.href"
      v-on:click="click()"
    )
      span(v-text="item.text")
    router-link(
      class="sidebar__item"
      active-class="sidebar__item--active"
      v-bind:exact="item.href === '/'"
      v-bind:to="item.href"
      v-on:click.native="click()"
      v-else
    )
      v-icon(v-if="item.icon") {{ item.icon }}
      span(v-text="item.text")
</template>

<script>
  import { closest } from '../../util/helpers'

  export default {
    name: 'sidebar-item',

    data () {
      return {
        sidebar: null,
        group: null
      }
    },

    props: {
      item: {
        type: Object,
        required: true
      },

      router: Boolean
    },

    computed: {
      group () {
        return closest.call(this, 'sidebar__group')
      },

      sidebar () {
        return closest.call(this, 'sidebar')
      }
    },

    methods: {
      click () {
        this.$vuetify.bus.pub(`sidebar-group:close:${this.sidebar}`, this.group)
      }
    }
  }
</script>


feat(components): empty pane

feat(components): empty pane

<template>

        <

          {{ data.name }}

        >

      <VueInput
        v-model="stateFilter"
        icon-left="search"
        placeholder="Filter state..."
        class="search flex-1 flat"
      />

      <VueButton
        v-if="$isChrome"
        v-tooltip="'Inspect DOM'"
        icon-left="code"
        class="flat icon-button"
        @click="inspectDOM()"
      />

      <VueButton
        v-if="fileIsPath"
        v-tooltip="{
          content: $t('ComponentInspector.openInEditor.tooltip', { file: data.file }),
          html: true
        }"
        icon-left="launch"
        class="flat icon-button"
        @click="openFile()"
      />

    <StateInspector
      :state="state"
      class="flex-1 overflow-y-auto"
      @edit-state="editState"
    />

    <VueIcon
      icon="device_hub"
      class="w-10 h-10 opacity-50"
    />

Select a component

</template>

<template>

        <

          {{ data.name }}

        >

      <VueInput
        v-model="stateFilter"
        icon-left="search"
        placeholder="Filter state..."
        class="search flex-1 flat"
      />

      <VueButton
        v-if="$isChrome"
        v-tooltip="'Inspect DOM'"
        icon-left="code"
        class="flat icon-button"
        @click="inspectDOM()"
      />

      <VueButton
        v-if="fileIsPath"
        v-tooltip="{
          content: $t('ComponentInspector.openInEditor.tooltip', { file: data.file }),
          html: true
        }"
        icon-left="launch"
        class="flat icon-button"
        @click="openFile()"
      />

    <StateInspector
      :state="state"
      class="flex-1 overflow-y-auto"
      @edit-state="editState"
    />

</template>


Update FileManager.vue

Update FileManager.vue


Keeping dynamic components alive.

Keeping dynamic components alive.


Show list of scouts

Show list of scouts

<template>

        Item Name: {{ purchasable.item }}

        Price: {{ purchasable.price | currency }}

        Description: {{ purchasable.description }}

        Purchase Limit: {{ purchasable.purchaser_limit }}

        Quantity Purchased: {{ purchasable.purchaser_count }}

        Scouts can pick a size for this item

        The minimum purchaser age is: {{ purchasable.minimum_age ? purchasable.minimum_age : 'not set' }}

        The maximum purchaser age is: {{ purchasable.maximum_age ? purchasable.maximum_age : 'not set' }}

Buyers

      <closable-error v-if="error">{{ error }}</closable-error>
      <centered-spinner v-if="loading"></centered-spinner>
      <paginated-items v-if="!error && !loading && showingBuyers"
                       :target="'buyers'"
                       :contents="orderedBuyers"
                       :showLinks="true"
                       :table="true">

              First Name

              Last Name

              Troop

            {{ props.item.scout.firstname }}

            {{ props.item.scout.lastname }}

            {{ props.item.scout.troop }}

      </paginated-items>

</template>

<template>

        Item Name: {{ purchasable.item }}

        Price: {{ purchasable.price | currency }}

        Description: {{ purchasable.description }}

        Purchase Limit: {{ purchasable.purchaser_limit }}

        Quantity Purchased: {{ purchasable.purchaser_count }}

        Scouts can pick a size for this item

        The minimum purchaser age is: {{ purchasable.minimum_age ? purchasable.minimum_age : 'not set' }}

        The maximum purchaser age is: {{ purchasable.maximum_age ? purchasable.maximum_age : 'not set' }}

Buyers

</template>


update

update

<template>

    <router-view class="main-router-view"></router-view>

© Leander Berg 2017

        Imprint
        Github Project

</template>


Увеличены миниатюры imageban

Увеличены миниатюры imageban


Guard against initial display error in IE 11

<template>

  <CoreFullscreen
    ref="epubRenderer"
    class="epub-renderer"
    :class="{small: windowIsSmall, scrolled: scrolled}"
    :style="epubRendererStyle"
    @changeFullscreen="isInFullscreen = $event"
  >

    <LoadingError v-if="errorLoading" :loaded="loaded" />

    <LoadingScreen v-else-if="!loaded" />

    <div
      class="epub-renderer-content"
      :dir="dir"
      @mousedown.stop="handleMouseDown"
      @keyup.esc="closeSideBar"
    >

      <TopBar
        ref="topBar"
        class="top-bar"
        :isInFullscreen="isInFullscreen"
        @tableOfContentsButtonClicked="handleTocToggle"
        @settingsButtonClicked="handleSettingToggle"
        @searchButtonClicked="handleSearchToggle"
        @fullscreenButtonClicked="$refs.epubRenderer.toggleFullscreen()"
      />

      <FocusLock
        :disabled="!tocSideBarIsOpen"
        :returnFocus="true"
      >
        <TocButton
          v-if="tocSideBarIsOpen"
          class="toc-button"
          @click="handleTocToggle"
        />

        <transition name="fade">
          <TableOfContentsSideBar
            v-show="tocSideBarIsOpen"
            ref="tocSideBar"
            :toc="toc"
            :currentSection="currentSection"
            class="side-bar side-bar-left"
            @tocNavigation="handleTocNavigation"
          />
        </transition>
      </FocusLock>

      <FocusLock
        :disabled="!settingsSideBarIsOpen"
        :returnFocus="false"
      >
        <SettingsButton
          v-if="settingsSideBarIsOpen"
          class="settings-button"
          @click="handleSettingToggle"
        />

        <transition name="fade">
          <SettingsSideBar
            v-show="settingsSideBarIsOpen"
            ref="settingsSideBar"
            class="side-bar side-bar-right"
            :theme="theme"
            :decreaseFontSizeDisabled="decreaseFontSizeDisabled"
            :increaseFontSizeDisabled="increaseFontSizeDisabled"
            @decreaseFontSize="decreaseFontSize"
            @increaseFontSize="increaseFontSize"
            @setTheme="setTheme"
          />
        </transition>
      </FocusLock>

      <FocusLock
        :disabled="!searchSideBarIsOpen"
        :returnFocus="false"
      >
        <SearchButton
          v-if="searchSideBarIsOpen"
          class="search-button"
          @click="handleSearchToggle"
        />

        <transition name="fade">
          <SearchSideBar
            v-show="searchSideBarIsOpen"
            ref="searchSideBar"
            class="side-bar side-bar-right"
            :book="book"
            @newSearchQuery="handleNewSearchQuery"
            @navigateToSearchResult="handleNavigateToSearchResult"
          />
        </transition>
      </FocusLock>

      <div
        class="navigation-and-epubjs"
        :style="backgroundColorStyle"
      >
        <div
          class="column epubjs-navigation"
        >
          <PreviousButton
            :color="navigationButtonColor"
            :style="backgroundColorStyle"
            :isRtl="isRtl"
            @goToPreviousPage="goToPreviousPage"
          />
        </div>
        <div
          ref="epubjsContainer"
          class="column epubjs-parent"
          :style="backgroundColorStyle"
        >
        </div>
        <div
          class="column epubjs-navigation"
        >
          <NextButton
            :color="navigationButtonColor"
            :style="backgroundColorStyle"
            :isRtl="isRtl"
            @goToNextPage="goToNextPage"
          />
        </div>
      </div>

      <BottomBar
        class="bottom-bar"
        :locationsAreReady="locations.length > 0"
        :heading="bottomBarHeading"
        :sliderValue="sliderValue"
        :sliderStep="sliderStep"
        @sliderChanged="handleSliderChanged"
      />
    </div>
  </CoreFullscreen>

</template>

<script>

  import Epub from 'epubjs/src/epub';
  import { EVENTS } from 'epubjs/src/utils/constants';

  import Mark from 'mark.js';
  import isEqual from 'lodash/isEqual';
  import Lockr from 'lockr';

  import FocusLock from 'vue-focus-lock';

  import { mapGetters } from 'vuex';
  import themeMixin from 'kolibri.coreVue.mixins.themeMixin';
  import CoreFullscreen from 'kolibri.coreVue.components.CoreFullscreen';
  import responsiveElement from 'kolibri.coreVue.mixins.responsiveElement';
  import responsiveWindow from 'kolibri.coreVue.mixins.responsiveWindow';
  import contentRendererMixin from 'kolibri.coreVue.mixins.contentRendererMixin';
  import { getContentLangDir } from 'kolibri.utils.i18n';

  import iFrameView from './SandboxIFrameView';
  import LoadingScreen from './LoadingScreen';
  import LoadingError from './LoadingError';
  import TopBar from './TopBar';
  import TableOfContentsSideBar from './TableOfContentsSideBar.vue';
  import SettingsSideBar from './SettingsSideBar';
  import SearchSideBar from './SearchSideBar';
  import BottomBar from './BottomBar';
  import PreviousButton from './PreviousButton';
  import NextButton from './NextButton';
  import TocButton from './TocButton';
  import SettingsButton from './SettingsButton';
  import SearchButton from './SearchButton';

  import { THEMES } from './EpubConstants';

  const FONT_SIZE_MIN = 8;
  const FONT_SIZE_MAX = 32;
  const FONT_SIZE_STEP = 4;

  const SIDE_BARS = {
    TOC: 'TOC',
    SEARCH: 'SEARCH',
    SETTINGS: 'SETTINGS',
  };

  const LOCATIONS_INTERVAL = 1000;

  const EPUB_RENDERER_SETTINGS_KEY = 'kolibriEpubRendererSettings';

  export default {
    name: 'EpubRendererIndex',
    components: {
      CoreFullscreen,
      TopBar,
      TableOfContentsSideBar,
      SettingsSideBar,
      SearchSideBar,
      LoadingScreen,
      BottomBar,
      PreviousButton,
      NextButton,
      FocusLock,
      TocButton,
      SettingsButton,
      SearchButton,
      LoadingError,
    },
    mixins: [responsiveWindow, responsiveElement, contentRendererMixin, themeMixin],
    data: () => ({
      book: null,
      rendition: null,
      toc: [],
      locations: [],
      loaded: false,
      errorLoading: false,
      sideBarOpen: null,
      theme: THEMES.WHITE,
      fontSize: null,
      isInFullscreen: false,
      markInstance: null,
      currentSection: null,
      searchQuery: null,
      sliderValue: 0,
      scrolled: false,

      currentLocation: null,
      updateContentStateInterval: null,
    }),
    computed: {
      ...mapGetters(['sessionTimeSpent']),
      savedLocation() {
        if (this.extraFields && this.extraFields.contentState) {
          return this.extraFields.contentState.savedLocation;
        }
        return null;
      },
      epubURL() {
        return this.defaultFile.storage_url;
      },
      backgroundColor() {
        return this.theme.backgroundColor;
      },
      backgroundColorStyle() {
        return {
          backgroundColor: this.backgroundColor,
        };
      },
      textColor() {
        return this.theme.textColor;
      },
      themeStyle() {
        const colorStyle = {
          'background-color': `${this.backgroundColor}!important`,
          color: `${this.textColor}!important`,
        };
        const alignmentStyle = {
          'text-align': `${this.isRtl ? 'right' : 'left'}!important`,
        };
        const fontSizeStyle = this.fontSize ? { 'font-size': `${this.fontSize}!important` } : {};

        const lineHeightStyle = {
          'line-height': `1.4em!important`,
        };

        // Display flex helps body size to it's content
        const htmlScrolledStyle = this.scrolled ? { display: 'flex' } : {};

        // Width style overrides the pixel width added by epub.js, and in conjunction with flex
        // above, helps SandboxIFrameView size containers according to true content width.
        // Padding override kills an arbitrary `padding:0 (width / 12)px` set by epub.js
        const bodyScrolledStyle = this.scrolled
          ? { width: 'auto!important', padding: '20px!important' }
          : {};

        return {
          html: { ...colorStyle, ...alignmentStyle, ...fontSizeStyle, ...htmlScrolledStyle },
          body: { ...colorStyle, ...alignmentStyle, ...fontSizeStyle, ...bodyScrolledStyle },
          p: { ...colorStyle, ...alignmentStyle, ...lineHeightStyle },
          h1: { ...colorStyle },
          h2: { ...colorStyle },
          h3: { ...colorStyle },
          h4: { ...colorStyle },
          h5: { ...colorStyle },
          'p:first-of-type::first-letter': { ...colorStyle },
          // help media not overflow their columns
          video: { 'max-width': '100%' },
          img: { 'max-width': '100%' },
        };
      },
      tocSideBarIsOpen() {
        return this.sideBarOpen === SIDE_BARS.TOC;
      },
      settingsSideBarIsOpen() {
        return this.sideBarOpen === SIDE_BARS.SETTINGS;
      },
      searchSideBarIsOpen() {
        return this.sideBarOpen === SIDE_BARS.SEARCH;
      },
      epubRendererStyle() {
        return {
          backgroundColor: this.$coreBgLight,
        };
      },
      navigationButtonColor() {
        return [THEMES.BLACK, THEMES.GREY].some(theme => isEqual(this.theme, theme))
          ? 'white'
          : 'black';
      },
      bottomBarHeading() {
        if (this.currentSection) {
          return this.currentSection.label.trim();
        }
        return '';
      },
      sliderStep() {
        if (this.locations.length > 0) {
          return Math.floor(Math.min(Math.max(100 / this.locations.length, 0.1), 100));
        }
        return 1;
      },
      decreaseFontSizeDisabled() {
        return this.fontSize === `${FONT_SIZE_MIN}px`;
      },
      increaseFontSizeDisabled() {
        return this.fontSize === `${FONT_SIZE_MAX}px`;
      },
      expectedTimeToRead() {
        const WORDS_PER_MINUTE = 300;
        const CHARS_PER_WORD = 10;
        const numberOfWords = (this.locations.length * LOCATIONS_INTERVAL) / CHARS_PER_WORD;
        const seconds = (numberOfWords * 60) / WORDS_PER_MINUTE;
        return seconds;
      },
      dir() {
        return getContentLangDir(this.lang);
      },
      isRtl() {
        return this.dir === 'rtl';
      },
    },
    watch: {
      sideBarOpen(newSideBar, oldSideBar) {
        this.$nextTick().then(() => {
          if (oldSideBar === SIDE_BARS.SEARCH) {
            this.clearMarks();
          }
          if (!newSideBar) {
            switch (oldSideBar) {
              case SIDE_BARS.TOC:
                this.$refs.topBar.focusOnTocButton();
                break;
              case SIDE_BARS.SETTINGS:
                this.$refs.topBar.focusOnSettingsButton();
                break;
              case SIDE_BARS.SEARCH:
                this.$refs.topBar.focusOnSearchButton();
                break;
              default:
                break;
            }
          }
          if (newSideBar === SIDE_BARS.SEARCH) {
            this.$refs.searchSideBar.focusOnInput();
            if (this.searchQuery) {
              this.clearMarks().then(this.createMarks(this.searchQuery));
            }
          }
        });
      },
    },
    created() {
      // Try to load the appropriate directional CSS for the particular content
      this.cssPromise = this.$options.contentModule.loadDirectionalCSS(this.dir);
    },
    beforeMount() {
      global.ePub = Epub;
      this.book = new Epub(this.epubURL);

      const { theme = this.theme, fontSize = this.fontSize } =
        Lockr.get(EPUB_RENDERER_SETTINGS_KEY) || {};
      this.theme = theme;
      this.fontSize = fontSize;
    },
    mounted() {
      Promise.all([this.cssPromise, this.book.ready]).then(() => {
        if (this.book.navigation) {
          this.toc = this.book.navigation.toc;
        }

        this.rendition = this.book.renderTo(this.$refs.epubjsContainer, {
          view: iFrameView,
          resizeOnOrientationChange: false,
          spread: 'auto',
          minSpreadWidth: 600,
        });

        this.rendition
          .display(this.savedLocation || undefined)
          .then(() => {
            const hasTables = this.rendition.getContents().reduce((hasTable, contents) => {
              return hasTable || contents.document.getElementsByTagName('table').length > 0;
            }, false);

            // Tables have issues rendering, so switch from paginated flow to scrolled
            if (hasTables) {
              this.scrolled = true;
              this.rendition.flow('scrolled');
              this.rendition.clear();
              // Re-renders and targets this.savedLocation (if set)
              return this.rendition.display(this.savedLocation || undefined);
            }

            // For paginated, update settings
            const bounds = this.$refs.epubjsContainer.getBoundingClientRect();
            Object.assign(this.rendition.manager.stage.settings, {
              width: bounds.width,
              height: bounds.height,
            });

            const resize = new Promise((resolve, reject) => {
              this.rendition.once(EVENTS.RENDITION.DISPLAYED, resolve);
              this.rendition.once(EVENTS.RENDITION.DISPLAY_ERROR, reject);
            });

            return this.rendition.q
              .enqueue(() => this.rendition.resize())
              .then(() => resize)
              .then(
                () => {},
                () => {
                  // In IE 11, EVENTS.RENDITION.DISPLAY_ERROR occurs when calling .resize(),
                  // so we'll try to redisplay, and if that's successful remove `errorLoading`
                  return this.rendition.display(this.savedLocation || undefined);
                }
              )
              .then(() => {
                this.errorLoading = false;
              });
          })
          .then(
            () => {
              this.handleReadyRendition();
            },
            () => {
              this.errorLoading = true;
            }
          );

        this.rendition.on(EVENTS.RENDITION.DISPLAY_ERROR, () => {
          this.errorLoading = true;
        });
      });
      this.book.on(EVENTS.BOOK.OPEN_FAILED, () => {
        this.errorLoading = true;
      });
    },
    beforeDestroy() {
      this.updateContentState();
      this.updateProgress();
      this.$emit('stopTracking');
      window.removeEventListener('mousedown', this.handleMouseDown, { passive: true });
      clearInterval(this.updateContentStateInterval);
    },
    destroyed() {
      delete global.ePub;
    },
    methods: {
      updateProgress() {
        if (this.locations.length > 0) {
          this.$emit('updateProgress', this.sessionTimeSpent / this.expectedTimeToRead);
        }
      },
      handleReadyRendition() {
        this.updateRenditionTheme(this.themeStyle);

        this.rendition.on(EVENTS.RENDITION.RELOCATED, location => this.relocatedHandler(location));
        this.rendition.on('keyup', this.handleKeyUps);
        this.rendition.on('click', () => this.closeSideBar());

        window.addEventListener('mousedown', this.handleMouseDown, { passive: true });

        this.loaded = true;

        this.book.locations.generate(LOCATIONS_INTERVAL).then(locations => {
          this.locations = locations;
          this.$emit('startTracking');
          this.updateContentStateInterval = setInterval(this.updateProgress, 30000);

          // Update current location, .currentLocation() can return Promise or value
          Promise.resolve()
            .then(() => this.rendition.currentLocation())
            .then(currentLocation => {
              if (currentLocation && currentLocation.start) {
                this.relocatedHandler(currentLocation);
              }
            });
        });
      },
      updateRenditionTheme(newTheme) {
        const themeName = JSON.stringify(newTheme);
        this.rendition.themes.register(themeName, newTheme);
        this.rendition.themes.select(themeName);
      },
      getIFrameView() {
        return this.rendition
          .views()
          .displayed()
          .filter(view => view instanceof iFrameView)
          .shift();
      },
      handleKeyUps(event) {
        const focus = () => {
          const view = this.getIFrameView();
          if (view) {
            view.focus();
          }
        };

        switch (event.which) {
          case 37:
            this.goToPreviousPage().then(focus);
            break;
          case 39:
            this.goToNextPage().then(focus);
            break;
        }
      },
      handleMouseDown(event) {
        // This check is necessary because event listeners don't seem to be removed on beforeDestroy
        if (this.$refs.epubRenderer) {
          let closeSideBar = false;
          if (this.tocSideBarIsOpen) {
            closeSideBar = !this.$refs.tocSideBar.$el.contains(event.target);
          } else if (this.settingsSideBarIsOpen) {
            closeSideBar = !this.$refs.settingsSideBar.$el.contains(event.target);
          } else if (this.searchSideBarIsOpen) {
            closeSideBar = !this.$refs.searchSideBar.$el.contains(event.target);
          }
          if (closeSideBar) {
            this.closeSideBar();
          }
        }
      },
      closeSideBar() {
        this.sideBarOpen = null;
      },
      goToNextPage() {
        return this.rendition.next();
      },
      goToPreviousPage() {
        return this.rendition.prev();
      },
      jumpToLocation(locationToJumpTo) {
        return this.rendition.display(locationToJumpTo);
      },
      handleTocToggle() {
        this.sideBarOpen === SIDE_BARS.TOC
          ? this.closeSideBar()
          : (this.sideBarOpen = SIDE_BARS.TOC);
      },
      handleSearchToggle() {
        this.sideBarOpen === SIDE_BARS.SEARCH
          ? this.closeSideBar()
          : (this.sideBarOpen = SIDE_BARS.SEARCH);
      },
      handleSettingToggle() {
        this.sideBarOpen === SIDE_BARS.SETTINGS
          ? this.closeSideBar()
          : (this.sideBarOpen = SIDE_BARS.SETTINGS);
      },
      handleTocNavigation(item) {
        this.jumpToLocation(item.href)
          .then(() => {
            this.closeSideBar();
          })
          .catch(() => {
            this.jumpToLocation(`xhtml/${item.href}`).then(() => {
              this.closeSideBar();
            });
          });
      },
      getCurrentFontSize() {
        const view = this.getIFrameView();

        // Use epub Contents class which will get computed font-size
        return view ? view.getContents().css('font-size', null) : null;
      },
      increaseFontSize() {
        const currentFontSize = this.getCurrentFontSize();
        const fontSizeNumericValue = parseFloat(currentFontSize);
        const newFontSizeNumericValue = Math.min(
          fontSizeNumericValue + FONT_SIZE_STEP,
          FONT_SIZE_MAX
        );
        this.setFontSize(`${newFontSizeNumericValue}px`);
      },
      decreaseFontSize() {
        const currentFontSize = this.getCurrentFontSize();
        const fontSizeNumericValue = parseFloat(currentFontSize);
        const newFontSizeNumericValue = Math.max(
          fontSizeNumericValue - FONT_SIZE_STEP,
          FONT_SIZE_MIN
        );
        this.setFontSize(`${newFontSizeNumericValue}px`);
      },
      expandIFrameView() {
        const view = this.getIFrameView();

        if (view) {
          // TODO: Figure out how to get this trigger the iframe containers to resize
        }
      },
      setTheme(theme) {
        this.theme = theme;
        this.persistSettings({ theme });
        this.updateRenditionTheme(this.themeStyle);
      },
      setFontSize(fontSize) {
        this.fontSize = fontSize;
        this.persistSettings({ fontSize });
        this.updateRenditionTheme(this.themeStyle);

        if (this.scrolled) {
          this.expandIFrameView();
        }
      },
      persistSettings(settings) {
        const saved = Lockr.get(EPUB_RENDERER_SETTINGS_KEY);
        Lockr.set(EPUB_RENDERER_SETTINGS_KEY, {
          ...saved,
          ...settings,
        });
      },
      handleNewSearchQuery(searchQuery) {
        this.searchQuery = searchQuery;
        this.clearMarks().then(this.createMarks(searchQuery));
      },
      handleNavigateToSearchResult(searchResult) {
        this.clearMarks()
          .then(() => this.jumpToLocation(searchResult.cfi))
          .then(() => this.createMarks(this.searchQuery));
      },
      clearMarks() {
        return new Promise(resolve => {
          if (this.markInstance) {
            this.markInstance.unmark({
              done: () => {
                this.markInstance = null;
                resolve();
              },
            });
          } else {
            resolve();
          }
        });
      },
      createMarks(searchQuery) {
        const view = this.getIFrameView();
        if (!view) {
          return Promise.resolve();
        }

        return new Promise(resolve => {
          const root = view.getContents().root();
          this.markInstance = new Mark(root.getElementsByTagName('body').item(0));
          this.markInstance.mark(searchQuery, {
            separateWordSearch: false,
            done: () => resolve(),
          });
        });
      },
      flattenToc(toc) {
        return [].concat(...toc.map(section => [section, ...this.flattenToc(section.subitems)]));
      },
      getCurrentSection(currentLocationStart) {
        let currentSection;
        if (currentLocationStart) {
          const flatToc = this.flattenToc(this.toc);
          const currentLocationHref = this.book.canonical(currentLocationStart.href);
          currentSection = flatToc.find(
            item => this.book.canonical(item.href) === currentLocationHref
          );
        }
        return currentSection;
      },
      updateCurrentSection(currentLocationStart) {
        this.currentSection = this.getCurrentSection(currentLocationStart);
      },
      relocatedHandler(location) {
        this.sliderValue = location.start.percentage * 100;
        this.updateCurrentSection(location.start);
        this.currentLocation = location.start.cfi;
        this.updateContentState();
      },
      handleSliderChanged(newSliderValue) {
        const indexOfLocationToJumpTo = Math.floor(
          (this.locations.length - 1) * (newSliderValue / 100)
        );
        const locationToJumpTo = this.locations[indexOfLocationToJumpTo];
        this.jumpToLocation(locationToJumpTo);
      },
      updateContentState() {
        let contentState;
        if (this.extraFields) {
          contentState = {
            ...this.extraFields.contentState,
            savedLocation: this.currentLocation || this.savedLocation,
          };
        } else {
          contentState = { savedLocation: this.currentLocation || this.savedLocation };
        }
        this.$emit('updateContentState', contentState);
      },
    },
  };

</script>

<style lang="scss" scoped>

  @import './EpubStyles';

  $navigation-button-small: 36px;
  $navigation-button-normal: 52px;

  .epub-renderer {
    position: relative;
    padding-top: calc(100% * 8.5 / 11);
    font-size: smaller;
  }

  .epub-renderer.small {
    padding-top: calc(100% * 11 / 8.5);
  }

  .epub-renderer.normalize-fullscreen {
    padding-top: 0 !important;
  }

  .epub-renderer-content {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
  }

  .top-bar {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
  }

  .side-bar {
    position: absolute;
    top: 36px;
    bottom: 54px;
  }

  .side-bar-left {
    left: 0;
  }

  .side-bar-right {
    right: 0;
  }

  .fade-enter-active,
  .fade-leave-active {
    transition: all 0.1s ease;
  }

  .fade-enter,
  .fade-leave-to {
    opacity: 0;
    transform: scale3d(0.3, 0.3, 0.3);
  }

  .toc-button,
  .settings-button,
  .search-button {
    position: absolute;
    top: 0;
    z-index: 6;
  }
  .settings-button {
    right: 72px;
  }

  .search-button {
    right: 36px;
  }

  .bottom-bar {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
  }

  .d-t {
    @include d-t;
  }

  .d-t-r {
    @include d-t-r;
  }

  .d-t-c {
    @include d-t-c;
  }

  .navigation-and-epubjs {
    position: absolute;
    top: 36px;
    right: 0;
    bottom: 54px;
    left: 0;
    white-space: nowrap;
  }

  .epubjs-navigation {
    width: $navigation-button-normal;
  }

  .epubjs-parent {
    position: relative;
    width: calc(100% - (#{$navigation-button-normal} * 2));
  }

  /deep/ .epub-container {
    position: absolute !important;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
  }

  .epub-renderer.small .epubjs-navigation {
    width: $navigation-button-small;
  }

  .epub-renderer.small .epubjs-parent {
    width: calc(100% - (#{$navigation-button-small} * 2));
  }

  .epub-renderer.scrolled {
    .epubjs-navigation {
      display: none;
    }

    .epubjs-parent {
      width: 100%;
    }

    /deep/ .epub-container .epub-view {
      min-width: 100%;
    }
  }

  .column {
    position: relative;
    display: inline-block;
    height: 100%;
    overflow: hidden;
    text-align: center;
    white-space: normal;
    vertical-align: top;
  }

</style>

<template>

  <CoreFullscreen
    ref="epubRenderer"
    class="epub-renderer"
    :class="{small: windowIsSmall, scrolled: scrolled}"
    :style="epubRendererStyle"
    @changeFullscreen="isInFullscreen = $event"
  >

    <LoadingError v-if="errorLoading" :loaded="loaded" />

    <LoadingScreen v-else-if="!loaded" />

    <div
      class="epub-renderer-content"
      :dir="dir"
      @mousedown.stop="handleMouseDown"
      @keyup.esc="closeSideBar"
    >

      <TopBar
        ref="topBar"
        class="top-bar"
        :isInFullscreen="isInFullscreen"
        @tableOfContentsButtonClicked="handleTocToggle"
        @settingsButtonClicked="handleSettingToggle"
        @searchButtonClicked="handleSearchToggle"
        @fullscreenButtonClicked="$refs.epubRenderer.toggleFullscreen()"
      />

      <FocusLock
        :disabled="!tocSideBarIsOpen"
        :returnFocus="true"
      >
        <TocButton
          v-if="tocSideBarIsOpen"
          class="toc-button"
          @click="handleTocToggle"
        />

        <transition name="fade">
          <TableOfContentsSideBar
            v-show="tocSideBarIsOpen"
            ref="tocSideBar"
            :toc="toc"
            :currentSection="currentSection"
            class="side-bar side-bar-left"
            @tocNavigation="handleTocNavigation"
          />
        </transition>
      </FocusLock>

      <FocusLock
        :disabled="!settingsSideBarIsOpen"
        :returnFocus="false"
      >
        <SettingsButton
          v-if="settingsSideBarIsOpen"
          class="settings-button"
          @click="handleSettingToggle"
        />

        <transition name="fade">
          <SettingsSideBar
            v-show="settingsSideBarIsOpen"
            ref="settingsSideBar"
            class="side-bar side-bar-right"
            :theme="theme"
            :decreaseFontSizeDisabled="decreaseFontSizeDisabled"
            :increaseFontSizeDisabled="increaseFontSizeDisabled"
            @decreaseFontSize="decreaseFontSize"
            @increaseFontSize="increaseFontSize"
            @setTheme="setTheme"
          />
        </transition>
      </FocusLock>

      <FocusLock
        :disabled="!searchSideBarIsOpen"
        :returnFocus="false"
      >
        <SearchButton
          v-if="searchSideBarIsOpen"
          class="search-button"
          @click="handleSearchToggle"
        />

        <transition name="fade">
          <SearchSideBar
            v-show="searchSideBarIsOpen"
            ref="searchSideBar"
            class="side-bar side-bar-right"
            :book="book"
            @newSearchQuery="handleNewSearchQuery"
            @navigateToSearchResult="handleNavigateToSearchResult"
          />
        </transition>
      </FocusLock>

      <div
        class="navigation-and-epubjs"
        :style="backgroundColorStyle"
      >
        <div
          class="column epubjs-navigation"
        >
          <PreviousButton
            :color="navigationButtonColor"
            :style="backgroundColorStyle"
            :isRtl="isRtl"
            @goToPreviousPage="goToPreviousPage"
          />
        </div>
        <div
          ref="epubjsContainer"
          class="column epubjs-parent"
          :style="backgroundColorStyle"
        >
        </div>
        <div
          class="column epubjs-navigation"
        >
          <NextButton
            :color="navigationButtonColor"
            :style="backgroundColorStyle"
            :isRtl="isRtl"
            @goToNextPage="goToNextPage"
          />
        </div>
      </div>

      <BottomBar
        class="bottom-bar"
        :locationsAreReady="locations.length > 0"
        :heading="bottomBarHeading"
        :sliderValue="sliderValue"
        :sliderStep="sliderStep"
        @sliderChanged="handleSliderChanged"
      />
    </div>
  </CoreFullscreen>

</template>

<script>

  import Epub from 'epubjs/src/epub';
  import { EVENTS } from 'epubjs/src/utils/constants';

  import Mark from 'mark.js';
  import isEqual from 'lodash/isEqual';
  import Lockr from 'lockr';

  import FocusLock from 'vue-focus-lock';

  import { mapGetters } from 'vuex';
  import themeMixin from 'kolibri.coreVue.mixins.themeMixin';
  import CoreFullscreen from 'kolibri.coreVue.components.CoreFullscreen';
  import responsiveElement from 'kolibri.coreVue.mixins.responsiveElement';
  import responsiveWindow from 'kolibri.coreVue.mixins.responsiveWindow';
  import contentRendererMixin from 'kolibri.coreVue.mixins.contentRendererMixin';
  import { getContentLangDir } from 'kolibri.utils.i18n';

  import iFrameView from './SandboxIFrameView';
  import LoadingScreen from './LoadingScreen';
  import LoadingError from './LoadingError';
  import TopBar from './TopBar';
  import TableOfContentsSideBar from './TableOfContentsSideBar.vue';
  import SettingsSideBar from './SettingsSideBar';
  import SearchSideBar from './SearchSideBar';
  import BottomBar from './BottomBar';
  import PreviousButton from './PreviousButton';
  import NextButton from './NextButton';
  import TocButton from './TocButton';
  import SettingsButton from './SettingsButton';
  import SearchButton from './SearchButton';

  import { THEMES } from './EpubConstants';

  const FONT_SIZE_MIN = 8;
  const FONT_SIZE_MAX = 32;
  const FONT_SIZE_STEP = 4;

  const SIDE_BARS = {
    TOC: 'TOC',
    SEARCH: 'SEARCH',
    SETTINGS: 'SETTINGS',
  };

  const LOCATIONS_INTERVAL = 1000;

  const EPUB_RENDERER_SETTINGS_KEY = 'kolibriEpubRendererSettings';

  export default {
    name: 'EpubRendererIndex',
    components: {
      CoreFullscreen,
      TopBar,
      TableOfContentsSideBar,
      SettingsSideBar,
      SearchSideBar,
      LoadingScreen,
      BottomBar,
      PreviousButton,
      NextButton,
      FocusLock,
      TocButton,
      SettingsButton,
      SearchButton,
      LoadingError,
    },
    mixins: [responsiveWindow, responsiveElement, contentRendererMixin, themeMixin],
    data: () => ({
      book: null,
      rendition: null,
      toc: [],
      locations: [],
      loaded: false,
      errorLoading: false,
      sideBarOpen: null,
      theme: THEMES.WHITE,
      fontSize: null,
      isInFullscreen: false,
      markInstance: null,
      currentSection: null,
      searchQuery: null,
      sliderValue: 0,
      scrolled: false,

      currentLocation: null,
      updateContentStateInterval: null,
    }),
    computed: {
      ...mapGetters(['sessionTimeSpent']),
      savedLocation() {
        if (this.extraFields && this.extraFields.contentState) {
          return this.extraFields.contentState.savedLocation;
        }
        return null;
      },
      epubURL() {
        return this.defaultFile.storage_url;
      },
      backgroundColor() {
        return this.theme.backgroundColor;
      },
      backgroundColorStyle() {
        return {
          backgroundColor: this.backgroundColor,
        };
      },
      textColor() {
        return this.theme.textColor;
      },
      themeStyle() {
        const colorStyle = {
          'background-color': `${this.backgroundColor}!important`,
          color: `${this.textColor}!important`,
        };
        const alignmentStyle = {
          'text-align': `${this.isRtl ? 'right' : 'left'}!important`,
        };
        const fontSizeStyle = this.fontSize ? { 'font-size': `${this.fontSize}!important` } : {};

        const lineHeightStyle = {
          'line-height': `1.4em!important`,
        };

        // Display flex helps body size to it's content
        const htmlScrolledStyle = this.scrolled ? { display: 'flex' } : {};

        // Width style overrides the pixel width added by epub.js, and in conjunction with flex
        // above, helps SandboxIFrameView size containers according to true content width.
        // Padding override kills an arbitrary `padding:0 (width / 12)px` set by epub.js
        const bodyScrolledStyle = this.scrolled
          ? { width: 'auto!important', padding: '20px!important' }
          : {};

        return {
          html: { ...colorStyle, ...alignmentStyle, ...fontSizeStyle, ...htmlScrolledStyle },
          body: { ...colorStyle, ...alignmentStyle, ...fontSizeStyle, ...bodyScrolledStyle },
          p: { ...colorStyle, ...alignmentStyle, ...lineHeightStyle },
          h1: { ...colorStyle },
          h2: { ...colorStyle },
          h3: { ...colorStyle },
          h4: { ...colorStyle },
          h5: { ...colorStyle },
          'p:first-of-type::first-letter': { ...colorStyle },
          // help media not overflow their columns
          video: { 'max-width': '100%' },
          img: { 'max-width': '100%' },
        };
      },
      tocSideBarIsOpen() {
        return this.sideBarOpen === SIDE_BARS.TOC;
      },
      settingsSideBarIsOpen() {
        return this.sideBarOpen === SIDE_BARS.SETTINGS;
      },
      searchSideBarIsOpen() {
        return this.sideBarOpen === SIDE_BARS.SEARCH;
      },
      epubRendererStyle() {
        return {
          backgroundColor: this.$coreBgLight,
        };
      },
      navigationButtonColor() {
        return [THEMES.BLACK, THEMES.GREY].some(theme => isEqual(this.theme, theme))
          ? 'white'
          : 'black';
      },
      bottomBarHeading() {
        if (this.currentSection) {
          return this.currentSection.label.trim();
        }
        return '';
      },
      sliderStep() {
        if (this.locations.length > 0) {
          return Math.floor(Math.min(Math.max(100 / this.locations.length, 0.1), 100));
        }
        return 1;
      },
      decreaseFontSizeDisabled() {
        return this.fontSize === `${FONT_SIZE_MIN}px`;
      },
      increaseFontSizeDisabled() {
        return this.fontSize === `${FONT_SIZE_MAX}px`;
      },
      expectedTimeToRead() {
        const WORDS_PER_MINUTE = 300;
        const CHARS_PER_WORD = 10;
        const numberOfWords = (this.locations.length * LOCATIONS_INTERVAL) / CHARS_PER_WORD;
        const seconds = (numberOfWords * 60) / WORDS_PER_MINUTE;
        return seconds;
      },
      dir() {
        return getContentLangDir(this.lang);
      },
      isRtl() {
        return this.dir === 'rtl';
      },
    },
    watch: {
      sideBarOpen(newSideBar, oldSideBar) {
        this.$nextTick().then(() => {
          if (oldSideBar === SIDE_BARS.SEARCH) {
            this.clearMarks();
          }
          if (!newSideBar) {
            switch (oldSideBar) {
              case SIDE_BARS.TOC:
                this.$refs.topBar.focusOnTocButton();
                break;
              case SIDE_BARS.SETTINGS:
                this.$refs.topBar.focusOnSettingsButton();
                break;
              case SIDE_BARS.SEARCH:
                this.$refs.topBar.focusOnSearchButton();
                break;
              default:
                break;
            }
          }
          if (newSideBar === SIDE_BARS.SEARCH) {
            this.$refs.searchSideBar.focusOnInput();
            if (this.searchQuery) {
              this.clearMarks().then(this.createMarks(this.searchQuery));
            }
          }
        });
      },
    },
    created() {
      // Try to load the appropriate directional CSS for the particular content
      this.cssPromise = this.$options.contentModule.loadDirectionalCSS(this.dir);
    },
    beforeMount() {
      global.ePub = Epub;
      this.book = new Epub(this.epubURL);

      const { theme = this.theme, fontSize = this.fontSize } =
        Lockr.get(EPUB_RENDERER_SETTINGS_KEY) || {};
      this.theme = theme;
      this.fontSize = fontSize;
    },
    mounted() {
      Promise.all([this.cssPromise, this.book.ready]).then(() => {
        if (this.book.navigation) {
          this.toc = this.book.navigation.toc;
        }

        this.rendition = this.book.renderTo(this.$refs.epubjsContainer, {
          view: iFrameView,
          resizeOnOrientationChange: false,
          spread: 'auto',
          minSpreadWidth: 600,
        });

        this.rendition
          .display(this.savedLocation || undefined)
          .then(() => {
            const hasTables = this.rendition.getContents().reduce((hasTable, contents) => {
              return hasTable || contents.document.getElementsByTagName('table').length > 0;
            }, false);

            // Tables have issues rendering, so switch from paginated flow to scrolled
            if (hasTables) {
              this.scrolled = true;
              this.rendition.flow('scrolled');
              this.rendition.clear();
              // Re-renders and targets this.savedLocation (if set)
              return this.rendition.display(this.savedLocation || undefined);
            }

            // For paginated, update settings
            const bounds = this.$refs.epubjsContainer.getBoundingClientRect();
            Object.assign(this.rendition.manager.stage.settings, {
              width: bounds.width,
              height: bounds.height,
            });

            return this.rendition.q.enqueue(() => {
              this.rendition.resize();
            });
          })
          .then(() => {
            this.handleReadyRendition();
          });

        this.rendition.on(EVENTS.RENDITION.DISPLAY_ERROR, () => {
          this.errorLoading = true;
        });
      });
      this.book.on(EVENTS.BOOK.OPEN_FAILED, () => {
        this.errorLoading = true;
      });
    },
    beforeDestroy() {
      this.updateContentState();
      this.updateProgress();
      this.$emit('stopTracking');
      window.removeEventListener('mousedown', this.handleMouseDown, { passive: true });
      clearInterval(this.updateContentStateInterval);
    },
    destroyed() {
      delete global.ePub;
    },
    methods: {
      updateProgress() {
        if (this.locations.length > 0) {
          this.$emit('updateProgress', this.sessionTimeSpent / this.expectedTimeToRead);
        }
      },
      handleReadyRendition() {
        this.updateRenditionTheme(this.themeStyle);

        this.rendition.on(EVENTS.RENDITION.RELOCATED, location => this.relocatedHandler(location));
        this.rendition.on('keyup', this.handleKeyUps);
        this.rendition.on('click', () => this.closeSideBar());

        window.addEventListener('mousedown', this.handleMouseDown, { passive: true });

        this.loaded = true;

        this.book.locations.generate(LOCATIONS_INTERVAL).then(locations => {
          this.locations = locations;
          this.$emit('startTracking');
          this.updateContentStateInterval = setInterval(this.updateProgress, 30000);

          // Update current location, .currentLocation() can return Promise or value
          Promise.resolve()
            .then(() => this.rendition.currentLocation())
            .then(currentLocation => {
              if (currentLocation) {
                this.relocatedHandler(currentLocation);
              }
            });
        });
      },
      updateRenditionTheme(newTheme) {
        const themeName = JSON.stringify(newTheme);
        this.rendition.themes.register(themeName, newTheme);
        this.rendition.themes.select(themeName);
      },
      getIFrameView() {
        return this.rendition
          .views()
          .displayed()
          .filter(view => view instanceof iFrameView)
          .shift();
      },
      handleKeyUps(event) {
        const focus = () => {
          const view = this.getIFrameView();
          if (view) {
            view.focus();
          }
        };

        switch (event.which) {
          case 37:
            this.goToPreviousPage().then(focus);
            break;
          case 39:
            this.goToNextPage().then(focus);
            break;
        }
      },
      handleMouseDown(event) {
        // This check is necessary because event listeners don't seem to be removed on beforeDestroy
        if (this.$refs.epubRenderer) {
          let closeSideBar = false;
          if (this.tocSideBarIsOpen) {
            closeSideBar = !this.$refs.tocSideBar.$el.contains(event.target);
          } else if (this.settingsSideBarIsOpen) {
            closeSideBar = !this.$refs.settingsSideBar.$el.contains(event.target);
          } else if (this.searchSideBarIsOpen) {
            closeSideBar = !this.$refs.searchSideBar.$el.contains(event.target);
          }
          if (closeSideBar) {
            this.closeSideBar();
          }
        }
      },
      closeSideBar() {
        this.sideBarOpen = null;
      },
      goToNextPage() {
        return this.rendition.next();
      },
      goToPreviousPage() {
        return this.rendition.prev();
      },
      jumpToLocation(locationToJumpTo) {
        return this.rendition.display(locationToJumpTo);
      },
      handleTocToggle() {
        this.sideBarOpen === SIDE_BARS.TOC
          ? this.closeSideBar()
          : (this.sideBarOpen = SIDE_BARS.TOC);
      },
      handleSearchToggle() {
        this.sideBarOpen === SIDE_BARS.SEARCH
          ? this.closeSideBar()
          : (this.sideBarOpen = SIDE_BARS.SEARCH);
      },
      handleSettingToggle() {
        this.sideBarOpen === SIDE_BARS.SETTINGS
          ? this.closeSideBar()
          : (this.sideBarOpen = SIDE_BARS.SETTINGS);
      },
      handleTocNavigation(item) {
        this.jumpToLocation(item.href)
          .then(() => {
            this.closeSideBar();
          })
          .catch(() => {
            this.jumpToLocation(`xhtml/${item.href}`).then(() => {
              this.closeSideBar();
            });
          });
      },
      getCurrentFontSize() {
        const view = this.getIFrameView();

        // Use epub Contents class which will get computed font-size
        return view ? view.getContents().css('font-size', null) : null;
      },
      increaseFontSize() {
        const currentFontSize = this.getCurrentFontSize();
        const fontSizeNumericValue = parseFloat(currentFontSize);
        const newFontSizeNumericValue = Math.min(
          fontSizeNumericValue + FONT_SIZE_STEP,
          FONT_SIZE_MAX
        );
        this.setFontSize(`${newFontSizeNumericValue}px`);
      },
      decreaseFontSize() {
        const currentFontSize = this.getCurrentFontSize();
        const fontSizeNumericValue = parseFloat(currentFontSize);
        const newFontSizeNumericValue = Math.max(
          fontSizeNumericValue - FONT_SIZE_STEP,
          FONT_SIZE_MIN
        );
        this.setFontSize(`${newFontSizeNumericValue}px`);
      },
      expandIFrameView() {
        const view = this.getIFrameView();

        if (view) {
          // TODO: Figure out how to get this trigger the iframe containers to resize
        }
      },
      setTheme(theme) {
        this.theme = theme;
        this.persistSettings({ theme });
        this.updateRenditionTheme(this.themeStyle);
      },
      setFontSize(fontSize) {
        this.fontSize = fontSize;
        this.persistSettings({ fontSize });
        this.updateRenditionTheme(this.themeStyle);

        if (this.scrolled) {
          this.expandIFrameView();
        }
      },
      persistSettings(settings) {
        const saved = Lockr.get(EPUB_RENDERER_SETTINGS_KEY);
        Lockr.set(EPUB_RENDERER_SETTINGS_KEY, {
          ...saved,
          ...settings,
        });
      },
      handleNewSearchQuery(searchQuery) {
        this.searchQuery = searchQuery;
        this.clearMarks().then(this.createMarks(searchQuery));
      },
      handleNavigateToSearchResult(searchResult) {
        this.clearMarks()
          .then(() => this.jumpToLocation(searchResult.cfi))
          .then(() => this.createMarks(this.searchQuery));
      },
      clearMarks() {
        return new Promise(resolve => {
          if (this.markInstance) {
            this.markInstance.unmark({
              done: () => {
                this.markInstance = null;
                resolve();
              },
            });
          } else {
            resolve();
          }
        });
      },
      createMarks(searchQuery) {
        const view = this.getIFrameView();
        if (!view) {
          return Promise.resolve();
        }

        return new Promise(resolve => {
          const root = view.getContents().root();
          this.markInstance = new Mark(root.getElementsByTagName('body').item(0));
          this.markInstance.mark(searchQuery, {
            separateWordSearch: false,
            done: () => resolve(),
          });
        });
      },
      flattenToc(toc) {
        return [].concat(...toc.map(section => [section, ...this.flattenToc(section.subitems)]));
      },
      getCurrentSection(currentLocationStart) {
        let currentSection;
        if (currentLocationStart) {
          const flatToc = this.flattenToc(this.toc);
          const currentLocationHref = this.book.canonical(currentLocationStart.href);
          currentSection = flatToc.find(
            item => this.book.canonical(item.href) === currentLocationHref
          );
        }
        return currentSection;
      },
      updateCurrentSection(currentLocationStart) {
        this.currentSection = this.getCurrentSection(currentLocationStart);
      },
      relocatedHandler(location) {
        this.sliderValue = location.start.percentage * 100;
        this.updateCurrentSection(location.start);
        this.currentLocation = location.start.cfi;
        this.updateContentState();
      },
      handleSliderChanged(newSliderValue) {
        const indexOfLocationToJumpTo = Math.floor(
          (this.locations.length - 1) * (newSliderValue / 100)
        );
        const locationToJumpTo = this.locations[indexOfLocationToJumpTo];
        this.jumpToLocation(locationToJumpTo);
      },
      updateContentState() {
        let contentState;
        if (this.extraFields) {
          contentState = {
            ...this.extraFields.contentState,
            savedLocation: this.currentLocation || this.savedLocation,
          };
        } else {
          contentState = { savedLocation: this.currentLocation || this.savedLocation };
        }
        this.$emit('updateContentState', contentState);
      },
    },
  };

</script>

<style lang="scss" scoped>

  @import './EpubStyles';

  $navigation-button-small: 36px;
  $navigation-button-normal: 52px;

  .epub-renderer {
    position: relative;
    padding-top: calc(100% * 8.5 / 11);
    font-size: smaller;
  }

  .epub-renderer.small {
    padding-top: calc(100% * 11 / 8.5);
  }

  .epub-renderer.normalize-fullscreen {
    padding-top: 0 !important;
  }

  .epub-renderer-content {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
  }

  .top-bar {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
  }

  .side-bar {
    position: absolute;
    top: 36px;
    bottom: 54px;
  }

  .side-bar-left {
    left: 0;
  }

  .side-bar-right {
    right: 0;
  }

  .fade-enter-active,
  .fade-leave-active {
    transition: all 0.1s ease;
  }

  .fade-enter,
  .fade-leave-to {
    opacity: 0;
    transform: scale3d(0.3, 0.3, 0.3);
  }

  .toc-button,
  .settings-button,
  .search-button {
    position: absolute;
    top: 0;
    z-index: 6;
  }
  .settings-button {
    right: 72px;
  }

  .search-button {
    right: 36px;
  }

  .bottom-bar {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
  }

  .d-t {
    @include d-t;
  }

  .d-t-r {
    @include d-t-r;
  }

  .d-t-c {
    @include d-t-c;
  }

  .navigation-and-epubjs {
    position: absolute;
    top: 36px;
    right: 0;
    bottom: 54px;
    left: 0;
    white-space: nowrap;
  }

  .epubjs-navigation {
    width: $navigation-button-normal;
  }

  .epubjs-parent {
    position: relative;
    width: calc(100% - (#{$navigation-button-normal} * 2));
  }

  /deep/ .epub-container {
    position: absolute !important;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
  }

  .epub-renderer.small .epubjs-navigation {
    width: $navigation-button-small;
  }

  .epub-renderer.small .epubjs-parent {
    width: calc(100% - (#{$navigation-button-small} * 2));
  }

  .epub-renderer.scrolled {
    .epubjs-navigation {
      display: none;
    }

    .epubjs-parent {
      width: 100%;
    }

    /deep/ .epub-container .epub-view {
      min-width: 100%;
    }
  }

  .column {
    position: relative;
    display: inline-block;
    height: 100%;
    overflow: hidden;
    text-align: center;
    white-space: normal;
    vertical-align: top;
  }

</style>