docs: change order of up-next items

docs: change order of up-next items

<template>
  <div>
    <doc-heading>Generic.Pages.upNext</doc-heading>
    <v-container
      fluid
      grid-list-xl
      pa-0
    >
      <v-layout>
        <v-flex
          v-for="(link, i) in links"
          :key="i"
        >
          <v-list
            class="grey lighten-3 pa-0"
            two-line
          >
            <core-item
              :to="link.link"
              :avatar="link.icon"
              :avatar-color="link.color"
              :text="link.target"
              :subtext="link.section"
            >
              <v-list-tile-action>
                <v-icon>mdi-arrow-right</v-icon>
              </v-list-tile-action>
            </core-item>
          </v-list>
        </v-flex>
      </v-layout>
    </v-container>
  </div>
</template>

<script>
  export default {
    props: {
      value: {
        type: Array,
        default: () => ([])
      }
    },

    computed: {
      links () {
        return this.value.map(this.genLink)
      }
    },

    methods: {
      genColor (section) {
        switch (section) {
          case 'components': return 'primary'
          default: return 'grey'
        }
      },
      genIcon (section) {
        switch (section) {
          case 'components': return 'mdi-view-dashboard'
        }
      },
      genLink (link) {
        const [section, target] = link.split('/')

        return {
          ...this.genSectionInfo(section),
          link,
          section,
          target
        }
      },
      genSectionInfo (section) {
        return {
          color: this.genColor(section),
          icon: this.genIcon(section)
        }
      }
    }
  }
</script>

<template>
  <div>
    <doc-heading>Generic.Pages.upNext</doc-heading>
    <v-container
      fluid
      grid-list-xl
      pa-0
    >
      <v-layout>
        <v-flex
          v-for="(link, i) in links"
          :key="i"
        >
          <v-list
            class="grey lighten-3 pa-0"
            two-line
          >
            <core-item
              :to="link.link"
              :avatar="link.icon"
              :avatar-color="link.color"
              :text="link.section"
              :subtext="link.target"
            >
              <v-list-tile-action>
                <v-icon>mdi-arrow-right</v-icon>
              </v-list-tile-action>
            </core-item>
          </v-list>
        </v-flex>
      </v-layout>
    </v-container>
  </div>
</template>

<script>
  export default {
    props: {
      value: {
        type: Array,
        default: () => ([])
      }
    },

    computed: {
      links () {
        return this.value.map(this.genLink)
      }
    },

    methods: {
      genColor (section) {
        switch (section) {
          case 'components': return 'primary'
          default: return 'grey'
        }
      },
      genIcon (section) {
        switch (section) {
          case 'components': return 'mdi-view-dashboard'
        }
      },
      genLink (link) {
        const [section, target] = link.split('/')

        return {
          ...this.genSectionInfo(section),
          link,
          section,
          target
        }
      },
      genSectionInfo (section) {
        return {
          color: this.genColor(section),
          icon: this.genIcon(section)
        }
      }
    }
  }
</script>


Update messaging-room.form.vue

Update messaging-room.form.vue

<template>

	<textarea
		v-model="text"
		ref="textarea"
		@keypress="onKeypress"
		@paste="onPaste"
		placeholder="%i18n:@input-message-here%"
		v-autocomplete="'text'"
	></textarea>

{{ file.name }}

	<mk-uploader ref="uploader" @uploaded="onUploaded"/>
	<button class="send" @click="send" :disabled="!canSend || sending" title="%i18n:@send%">
		<template v-if="!sending">%fa:paper-plane%</template><template v-if="sending">%fa:spinner .spin%</template>
	</button>
	<button class="attach-from-local" @click="chooseFile" title="%i18n:@attach-from-local%">
		%fa:upload%
	</button>
	<button class="attach-from-drive" @click="chooseFileFromDrive" title="%i18n:@attach-from-drive%">
		%fa:R folder-open%
	</button>
	<input ref="file" type="file" @change="onChangeFile"/>

</template>


made readOnly arg dynamic by parsing props


Tabs?

Tabs?

<template>

      <button v-bind:class="{ active: !topic_view }">Recent</button>
      <button v-bind:class="{ active: topic_view }">Topics</button>

      <breadcrumbs :list="breadcrumbsList">Content Breadcrumbs</breadcrumbs>

        {{ user_name }} -
        {{ kind_icon }}
        {{ content_name }}

{{ exercise_count }} Exercises - {{ content_count }} Contents

Last Active: {{ last_active }}

Exercises

          <progress-bar :progress-percent="exercise_progress" color="#4CAF50"></progress-bar>

Content

          <progress-bar :progress-percent="content_progress" color="#4CAF50"></progress-bar>

{{ questions_count }} Questions - Mastery Model: {{ mastery_model }}

Last Active: {{ last_active }}

{{ questions_answered }} Questions Answered - {{ attempts }} Attempts - {{ time_spent }} - {{ date_mastered
            }}

Mastered:

{{ exercise_mastered }} / {{ exercise_total }} Learners

{{ duration }}

Last Active: {{ last_active }}

{{ time_spent }}

          <progress-bar :progress-percent="video_progress"></progress-bar>

          <progress-bar :progress-percent="video_progress"></progress-bar>

{{ pages }} Pages

Last Active: {{ last_active }}

            Viewed - {{ time_spent }}
            Not Viewed

          <progress-bar :progress-percent="document_progress"></progress-bar>

</template>


Now when clicking the quick action and hovering somewhere else, the dropdown will stay open until the user clicks outside the dropdown

<template>

        <input class="checkbox-input" :checked="isSelected" @click.stop="toggleSelection" type="checkBox">

{{ map.name }}

      {{ lastUpdated }}

      {{ $tc(`MapCard.condensedViews`, numberViews )}}

        {{ $t(`MapCard.shared.${map.privacy}`) }}

      <MapQuickActions class="map--quick-actions" v-if="showInteractiveElements" :map="map" @open="openQuickActions" @close="closeQuickActions" @dataChanged="onDataChanged" :hasShadow="false" />

</template>

<template>

        <input class="checkbox-input" :checked="isSelected" @click.stop="toggleSelection" type="checkBox">

{{ map.name }}

      {{ lastUpdated }}

      {{ $tc(`MapCard.condensedViews`, numberViews )}}

        {{ $t(`MapCard.shared.${map.privacy}`) }}

      <MapQuickActions v-if="showInteractiveElements" :map="map" @open="openQuickActions" @close="closeQuickActions" @dataChanged="onDataChanged" :hasShadow="false" />

</template>


Fix player controls style on phones

Fix player controls style on phones

<template>

{{ song.title }}

                        {{ song.album.artist.name }} –
                        {{ song.album.name }}

                        <audio crossorigin="anonymous" controls></audio>

                <equalizer v-if="useEqualizer" v-show="showEqualizer"></equalizer>

                <sound-bar v-show="song.playbackState === 'playing'"></sound-bar>

                Info

                    <input type="range" id="volumeRange" max="10" step="0.1" class="player-volume">

</template>


:art:

:art:

<template>

{{ $t('@.do-not-use-in-production') }}

			<button class="nav" @click="$parent.isDrawerOpening = true"><fa icon="bars"/></button>
			<fa icon="circle"/>

				<slot>{{ $root.instanceName }}</slot>

			<slot name="func"></slot>

</template>


修正认证栏目列表显示问题

修正认证栏目列表显示问题

<template>

                        认证类型
                        显示标题
                        操作

                        {{ category.name }}
                        {{ category.display_name }}

                            <router-link type="button"
                            class="btn btn-primary btn-sm"
                            :to="{ name: 'certification:category:edit', params:{name:category.name}}" >编辑</router-link>

</template>


Adds min and max length to fg-input

Adds min and max length to fg-input

<template>
    <div class="form-group">
        <label v-if="label" :for="name">
            {{label}}
        </label>
        <input class="form-control border-input" v-bind="$props" :name="name" :value="value" :minlength="min"
               :maxlength="max" @input="$emit('input',$event.target.value)"/>
    </div>
</template>
<script>
    export default {
        props: {
            type: {
                type: String,
                default: 'text'
            },
            label: String,
            name: String,
            disabled: Boolean,
            placeholder: String,
            value: [String, Number],
            max: Number,
            min: Number
        }
    }

</script>
<style scoped lang="scss">
    label {
        display: flex;
        flex-flow: column;
    }
</style>

<template>
    <div class="form-group">
        <label v-if="label">
            {{label}}
        </label>
        <input class="form-control border-input" v-bind="$props" :value="value"
               @input="$emit('input',$event.target.value)">
    </div>
</template>
<script>
    export default {
        props: {
            type: {
                type: String,
                default: 'text'
            },
            label: String,
            name: String,
            disabled: Boolean,
            placeholder: String,
            value: [String, Number]
        }
    }

</script>
<style scoped lang="scss">
    label {
        display: flex;
        flex-flow: column;
    }
</style>


devop: 🔧 added cache and filters

devop: 🔧 added cache and filters

<template>

    <v-row justify-lg="space-between" justify-md="space-between">
      <v-col cols="12" md="6" lg="7">

My Batch

= 1" class="mew-body textMedium--text">
              You have added {{ blockCount }} ETH {{ pluralizeBlockCount }} for
              Minting. Please review and click "Proceed to Minting."

          <block-result-component
            v-for="(block, idx) in 3"
            :key="`loadingBlockBatchResult` + block"
            is-loading
            :show-add="false"
            :has-border="idx != 2"
          />

          <mew-alert
            title="No Eth Blocks added to batch minting!"
            description='To mint blocks in batches, find a block and click on "Add to Batch"'
            theme="info"
            has-white-background
            hide-close-icon
          />

          <block-result-component
            v-for="(block, idx) in blocks"
            :key="block.blockNumber"
            :block-handler="block"
            :is-loading="false"
            :show-add="false"
            :remove-me="removeMe"
            :has-border="idx != blocks.length - 1"
          />

      </v-col>
      <v-col cols="12" md="5" lg="4">

{{ totalAvailable }} Blocks

                {{ totalMintPrice }} {{ network.type.currencyName }}

                {{ totalMintPriceFiat }}

Network Fee

                Edit Priority

                {{ totalNetworkFee }} {{ network.type.currencyName }}

                {{ totalNetworkFiatFee }}

Total

                {{ totalTransactionPrice }} {{ network.type.currencyName }}

                {{ totalTransactionFiatPrice }}

          <mew-button
            :title="hasEnoughEth ? 'Proceed to Minting' : 'Not Enough Eth'"
            has-full-width
            :disabled="!hasEnoughEth || isLoading || isCartEmpty"
            :loading="isLoading"
            @click.native="mintBlocks"
          />

      </v-col>
    </v-row>

</template>


Added spacer to bottom of PID tuning tool.

<template>

Jet Engine PID Control

            <canvas id="myChart" width="800" height="400"></canvas>

            <canvas id="pidTermsChart" width="800" height="400"></canvas>

            <panel title="Simulation Settings">

                                Simulation Tick Period (ms):
                                <input v-model.number="simulationTickPeriod_ms" :disabled="simulationRunning" style="width: 80px;"/>

                                Plot Period (ms):
                                <input v-model.number="plotPeriod_ms" :disabled="simulationRunning" style="width: 80px;"/>

                        Fuel Flow Rate Limits (mL/min):

                        min <input v-model="fuelFlowRateMin_mlPmin" v-on:change="fuelFlowRateLimitsChanged" :disabled="simulationRunning" style="width: 80px;"/>
                        max <input v-model="fuelFlowRateMax_mlPmin" v-on:change="fuelFlowRateLimitsChanged" :disabled="simulationRunning" style="width: 80px;"/>

                        Run Mode:

                        <select v-model="selectedRunMode" :options="runModes" :disabled="simulationRunning" style="width: 250px; height: 30px; background-color: transparent;">
                            <option v-for="option in runModes" v-bind:value="option"  v-bind:key="option">
                            {{ option }}
                            </option>
                        </select>

                    <mn-button
                        :variant="!simulationRunning ? 'success' : 'danger'"
                        :onClick="startStopSimulation"
                        style="width: 100px; height: 50px;">

{{ !simulationRunning ? 'START' : 'STOP' }}

                    </mn-button>

            </panel>

            <panel title="Controls">

                    Fuel Flow Rate (mL/min)

                        <vue-slider
                            ref="slider"
                            v-model="fuelFlow_mlPmin"
                            :min="Number(fuelFlowRateMin_mlPmin)" :max="Number(fuelFlowRateMax_mlPmin)" :interval="(Number(fuelFlowRateMax_mlPmin) - Number(fuelFlowRateMin_mlPmin)) / 100.0"
                            :disabled="selectedRunMode === simulationRunModesEnum.MANUAL_CONTROL_RPM || selectedRunMode === simulationRunModesEnum.AUTO_RPM_STEP_CHANGES"
                            style="width:300px;" />

                    Velocity Set-Point (rpm)

                        <vue-slider
                            ref="slider"
                            v-model="rotVelSetPoint_rpm"
                            :min=0 :max=100000 :interval=1000
                            :disabled="selectedRunMode === simulationRunModesEnum.MANUAL_CONTROL_FUEL_RATE || selectedRunMode === simulationRunModesEnum.AUTO_RPM_STEP_CHANGES"
                            style="width:300px;"/>

            </panel>

            <panel title="PID Settings">
                PID Constants

                        Constant
                        Min.
                        Max.
                        Value

                        P
                        <input v-model="pidConstants.p.min" class="pid-limit"/>
                        <input v-model="pidConstants.p.max" class="pid-limit"/>

                        <vue-slider ref="slider" v-model="pidConstants.p.value"
                        :min="Number(pidConstants.p.min)" :max="Number(pidConstants.p.max)"
                        :interval="(Number(pidConstants.p.max) - Number(pidConstants.p.min)) / 100.0"/>

                        I
                        <input v-model="pidConstants.i.min" class="pid-limit"/>
                        <input v-model="pidConstants.i.max" class="pid-limit"/>

                        <vue-slider ref="slider" v-model="pidConstants.i.value"
                        :min="Number(pidConstants.i.min)" :max="Number(pidConstants.i.max)"
                        :interval="(Number(pidConstants.i.max) - Number(pidConstants.i.min)) / 100.0"/>

                        D
                        <input v-model="pidConstants.d.min" class="pid-limit"/>
                        <input v-model="pidConstants.d.max" class="pid-limit"/>

                        <vue-slider ref="slider" v-model="pidConstants.d.value"
                        :min="Number(pidConstants.d.min)" :max="Number(pidConstants.d.max)"
                        :interval="(Number(pidConstants.d.max) - Number(pidConstants.d.min)) / 100.0"/>

                Integral Limiting (Windup Control)

                    Mode: <select v-model="selIntegralLimitingMode" @change="setIntegralLimitingMode" style="width: 300px; height: 30px; background-color: transparent;">
                        <option v-for="option in integralLimitModes" v-bind:value="option" v-bind:key="String(option)">
                        {{ String(option) }}
                    </option>
                    </select>

                        min <input v-model="integralLimitingConstantMin" @change="setIntegralLimitingMode" :disabled="areIntegralLimitingConstantsDisabled" style="width: 50px;"/>
                           max <input v-model="integralLimitingConstantMax" @change="setIntegralLimitingMode" :disabled="areIntegralLimitingConstantsDisabled" style="width: 50px;"/>

            </panel>

</template>


refactor: ProjectSelectionRow uses now new methods in ProjectUtils

<template>

    <router-link style="text-decoration: none; color: initial" :to="'/zeta/overview/' + id">

 {{ name }}

    </router-link>

            Invite to project
            <button id="close-invite-modal" type="button" class="close" data-dismiss="modal" aria-label="Close">
              ×
            </button>

            <input v-model="inviteProjectName" type="text" class="form-control" id="inviteProjectName"
                   placeholder="E-Mail Address" autocomplete="off">

            <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
            <button v-on:click="invite" id="start-invite-btn" type="button" class="btn btn-info"
                    :disabled="inviteProjectName.trim().length === 0">Invite
            </button>

            Duplicate project
            <button id="close-duplicate-modal" type="button" class="close" data-dismiss="modal" aria-label="Close">
              ×
            </button>

            <input v-model="duplicateProjectName" type="text" class="form-control" id="duplicateProjectName"
                   placeholder="New Project Name" autocomplete="off">

            <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
            <button v-on:click="duplicate" id="start-duplicate-btn" type="button" class="btn btn-info"
                    :disabled="duplicateProjectName.trim().length === 0">Duplicate
            </button>

</template>


Update padding for epub renderer for better viewport alignment of bottom bar.

<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="contentDirection"
      @mousedown.stop="handleMouseDown"
      @keyup.esc="closeSideBar"
    >

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

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

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

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

        <SettingsSideBar
          v-show="settingsSideBarIsOpen"
          ref="settingsSideBar"
          class="side-bar side-bar-right"
          :theme="theme"
          :decreaseFontSizeDisabled="decreaseFontSizeDisabled"
          :increaseFontSizeDisabled="increaseFontSizeDisabled"
          @decreaseFontSize="handleChangeFontSize(-1)"
          @increaseFontSize="handleChangeFontSize(+1)"
          @setTheme="setTheme"
        />
      </FocusLock>

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

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

      <div
        class="navigation-and-epubjs"
        :style="{ backgroundColor }"
      >
        <div
          class="column epubjs-navigation"
        >
          <PreviousButton
            v-show="!isAtStart"
            :color="navigationButtonColor"
            :isRtl="contentIsRtl"
            :style="{ backgroundColor }"
            @goToPreviousPage="goToPreviousPage"
          />
        </div>
        <div
          ref="epubjsContainer"
          class="column epubjs-parent"
          :style="{ backgroundColor }"
        >
        </div>
        <div
          class="column epubjs-navigation"
        >
          <NextButton
            v-show="!isAtEnd"
            :color="navigationButtonColor"
            :isRtl="contentIsRtl"
            :style="{ backgroundColor }"
            @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 get from 'lodash/get';
  import clamp from 'lodash/clamp';
  import Lockr from 'lockr';
  import FocusLock from 'vue-focus-lock';
  import CoreFullscreen from 'kolibri.coreVue.components.CoreFullscreen';
  import responsiveElementMixin from 'kolibri.coreVue.mixins.responsiveElementMixin';
  import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin';
  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, darkThemeNames } 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: [responsiveWindowMixin, responsiveElementMixin],
    data() {
      return {
        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,
        visitedPages: {},
        updateContentStateInterval: null,
      };
    },
    computed: {
      isAtStart() {
        return get(this.rendition, 'location.atStart', false);
      },
      isAtEnd() {
        return get(this.rendition, 'location.atEnd', false);
      },
      savedLocation() {
        if (this.extraFields && this.extraFields.contentState) {
          return this.extraFields.contentState.savedLocation;
        }
        return null;
      },
      savedVisitedPages: {
        get() {
          if (this.extraFields && this.extraFields.contentState) {
            return this.extraFields.contentState.savedVisitedPages || {};
          }
          return {};
        },
        set(value) {
          this.visitedPages = value;
        },
      },
      epubURL() {
        return this.defaultFile.storage_url;
      },
      backgroundColor() {
        return this.theme.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`,
        };

        // In scrolled mode, display flex helps body size to it's content
        // In paged column mode, clear margins on <html> to avoid issues with rendering
        const htmlStyle = this.scrolled ? { display: 'flex' } : { margin: '0!important' };

        // 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, ...htmlStyle },
          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.$themeTokens.surface,
          borderColor: this.$themePalette.grey.v_300,
        };
      },
      navigationButtonColor() {
        return darkThemeNames.some(themeName => isEqual(this.theme.name, themeName))
          ? '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`;
      },
      /* eslint-disable kolibri/vue-no-unused-properties */
      /**
       * @public
       */
      defaultDuration() {
        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;
      },
      /* eslint-enable kolibri/vue-no-unused-properties */
    },
    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.contentDirection);
    },
    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) {
          if (this.forceDurationBasedProgress) {
            // update progress using total time user has spent on the epub
            this.$emit('updateProgress', this.durationBasedProgress);
          } else {
            // update progress using number of pages seen out of available pages
            this.$emit(
              'updateProgress',
              Object.keys(this.savedVisitedPages).length / this.locations.length
            );
          }
        }
      },
      storeVisitedPage(currentLocation) {
        let visited = this.savedVisitedPages;
        visited[currentLocation] = true;
        this.savedVisitedPages = visited;
      },
      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);
      },
      toggleMenu(sideBarName) {
        if (this.sideBarOpen === sideBarName) {
          this.closeSideBar();
        } else {
          this.sideBarOpen = sideBarName;
        }
      },
      handleTocToggle() {
        this.toggleMenu(SIDE_BARS.TOC);
      },
      handleSearchToggle() {
        this.toggleMenu(SIDE_BARS.SEARCH);
      },
      handleSettingToggle() {
        this.toggleMenu(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;
      },
      handleChangeFontSize(difference) {
        const fontSizeNumericValue = parseFloat(this.getCurrentFontSize());
        const newFontSizeNumericValue = clamp(
          fontSizeNumericValue + difference * FONT_SIZE_STEP,
          FONT_SIZE_MIN,
          FONT_SIZE_MAX
        );
        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.storeVisitedPage(this.currentLocation);
        this.updateProgress();
        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,
            savedVisitedPages: this.visitedPages || this.savedVisitedPages,
          };
        } else {
          contentState = {
            savedLocation: this.currentLocation || this.savedLocation,
            savedVisitedPages: this.visitedPages || this.savedVisitedPages,
          };
        }
        this.$emit('updateContentState', contentState);
      },
    },
  };

</script>

<style lang="scss" scoped>

  @import '~kolibri-design-system/lib/styles/definitions';
  @import './EpubStyles';

  $top-bar-height: 36px;
  $bottom-bar-height: 54px;
  $navigation-button-small: 36px;
  $navigation-button-normal: 52px;

  .epub-renderer {
    position: relative;
    max-height: 100%;
    padding-top: calc(100vh - #{$bottom-bar-height});
    overflow: hidden;
    font-size: smaller;
    border: solid 1px;
    border-radius: $radius;
  }

  .epub-renderer:fullscreen,
  .epub-renderer.small:fullscreen {
    padding-top: 0;
  }

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

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

  .side-bar {
    @extend %momentum-scroll;

    position: absolute;
    top: $top-bar-height;
    bottom: $bottom-bar-height;
  }

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

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

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

  .search-button {
    // Positioned to be in the exact same spot as the TopBar's SearchButton,
    // which is given opacity: 0 when this button is shown
    right: 32px;
  }

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

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

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

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

  .navigation-and-epubjs {
    @extend %momentum-scroll;

    position: absolute;
    top: $top-bar-height;
    right: 0;
    bottom: $bottom-bar-height;
    left: 0;
    max-height: calc(100vh - #{$top-bar-height + $bottom-bar-height});
    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="contentDirection"
      @mousedown.stop="handleMouseDown"
      @keyup.esc="closeSideBar"
    >

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

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

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

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

        <SettingsSideBar
          v-show="settingsSideBarIsOpen"
          ref="settingsSideBar"
          class="side-bar side-bar-right"
          :theme="theme"
          :decreaseFontSizeDisabled="decreaseFontSizeDisabled"
          :increaseFontSizeDisabled="increaseFontSizeDisabled"
          @decreaseFontSize="handleChangeFontSize(-1)"
          @increaseFontSize="handleChangeFontSize(+1)"
          @setTheme="setTheme"
        />
      </FocusLock>

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

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

      <div
        class="navigation-and-epubjs"
        :style="{ backgroundColor }"
      >
        <div
          class="column epubjs-navigation"
        >
          <PreviousButton
            v-show="!isAtStart"
            :color="navigationButtonColor"
            :isRtl="contentIsRtl"
            :style="{ backgroundColor }"
            @goToPreviousPage="goToPreviousPage"
          />
        </div>
        <div
          ref="epubjsContainer"
          class="column epubjs-parent"
          :style="{ backgroundColor }"
        >
        </div>
        <div
          class="column epubjs-navigation"
        >
          <NextButton
            v-show="!isAtEnd"
            :color="navigationButtonColor"
            :isRtl="contentIsRtl"
            :style="{ backgroundColor }"
            @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 get from 'lodash/get';
  import clamp from 'lodash/clamp';
  import Lockr from 'lockr';
  import FocusLock from 'vue-focus-lock';
  import CoreFullscreen from 'kolibri.coreVue.components.CoreFullscreen';
  import responsiveElementMixin from 'kolibri.coreVue.mixins.responsiveElementMixin';
  import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin';
  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, darkThemeNames } 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: [responsiveWindowMixin, responsiveElementMixin],
    data() {
      return {
        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,
        visitedPages: {},
        updateContentStateInterval: null,
      };
    },
    computed: {
      isAtStart() {
        return get(this.rendition, 'location.atStart', false);
      },
      isAtEnd() {
        return get(this.rendition, 'location.atEnd', false);
      },
      savedLocation() {
        if (this.extraFields && this.extraFields.contentState) {
          return this.extraFields.contentState.savedLocation;
        }
        return null;
      },
      savedVisitedPages: {
        get() {
          if (this.extraFields && this.extraFields.contentState) {
            return this.extraFields.contentState.savedVisitedPages || {};
          }
          return {};
        },
        set(value) {
          this.visitedPages = value;
        },
      },
      epubURL() {
        return this.defaultFile.storage_url;
      },
      backgroundColor() {
        return this.theme.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`,
        };

        // In scrolled mode, display flex helps body size to it's content
        // In paged column mode, clear margins on <html> to avoid issues with rendering
        const htmlStyle = this.scrolled ? { display: 'flex' } : { margin: '0!important' };

        // 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, ...htmlStyle },
          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.$themeTokens.surface,
          borderColor: this.$themePalette.grey.v_300,
        };
      },
      navigationButtonColor() {
        return darkThemeNames.some(themeName => isEqual(this.theme.name, themeName))
          ? '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`;
      },
      /* eslint-disable kolibri/vue-no-unused-properties */
      /**
       * @public
       */
      defaultDuration() {
        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;
      },
      /* eslint-enable kolibri/vue-no-unused-properties */
    },
    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.contentDirection);
    },
    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) {
          if (this.forceDurationBasedProgress) {
            // update progress using total time user has spent on the epub
            this.$emit('updateProgress', this.durationBasedProgress);
          } else {
            // update progress using number of pages seen out of available pages
            this.$emit(
              'updateProgress',
              Object.keys(this.savedVisitedPages).length / this.locations.length
            );
          }
        }
      },
      storeVisitedPage(currentLocation) {
        let visited = this.savedVisitedPages;
        visited[currentLocation] = true;
        this.savedVisitedPages = visited;
      },
      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);
      },
      toggleMenu(sideBarName) {
        if (this.sideBarOpen === sideBarName) {
          this.closeSideBar();
        } else {
          this.sideBarOpen = sideBarName;
        }
      },
      handleTocToggle() {
        this.toggleMenu(SIDE_BARS.TOC);
      },
      handleSearchToggle() {
        this.toggleMenu(SIDE_BARS.SEARCH);
      },
      handleSettingToggle() {
        this.toggleMenu(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;
      },
      handleChangeFontSize(difference) {
        const fontSizeNumericValue = parseFloat(this.getCurrentFontSize());
        const newFontSizeNumericValue = clamp(
          fontSizeNumericValue + difference * FONT_SIZE_STEP,
          FONT_SIZE_MIN,
          FONT_SIZE_MAX
        );
        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.storeVisitedPage(this.currentLocation);
        this.updateProgress();
        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,
            savedVisitedPages: this.visitedPages || this.savedVisitedPages,
          };
        } else {
          contentState = {
            savedLocation: this.currentLocation || this.savedLocation,
            savedVisitedPages: this.visitedPages || this.savedVisitedPages,
          };
        }
        this.$emit('updateContentState', contentState);
      },
    },
  };

</script>

<style lang="scss" scoped>

  @import '~kolibri-design-system/lib/styles/definitions';
  @import './EpubStyles';

  $top-bar-height: 36px;
  $bottom-bar-height: 54px;
  $navigation-button-small: 36px;
  $navigation-button-normal: 52px;

  .epub-renderer {
    position: relative;
    max-height: 100%;
    padding-top: calc(100% * 8.5 / 11);
    overflow: hidden;
    font-size: smaller;
    border: solid 1px;
    border-radius: $radius;
  }

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

  .epub-renderer:fullscreen,
  .epub-renderer.small:fullscreen {
    padding-top: 0;
  }

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

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

  .side-bar {
    @extend %momentum-scroll;

    position: absolute;
    top: $top-bar-height;
    bottom: $bottom-bar-height;
  }

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

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

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

  .search-button {
    // Positioned to be in the exact same spot as the TopBar's SearchButton,
    // which is given opacity: 0 when this button is shown
    right: 32px;
  }

  .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 {
    @extend %momentum-scroll;

    position: absolute;
    top: $top-bar-height;
    right: 0;
    bottom: $bottom-bar-height;
    left: 0;
    max-height: calc(100vh - #{$top-bar-height + $bottom-bar-height});
    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>


KIcon BackLink

KIcon BackLink

<template>

  <!-- TODO: move this to be a core KBackLink -->
  <span class="offset">
    <UiIconButton
      type="flat"
      class="icon"
      tabIndex="-1"
      aria-hidden="true"
      @click="go"
    >
      <KIcon
        icon="back"
        :style="{ fill: $themeTokens.primary, top: 0, height: '24px', width: '24px' }"
      />
    </UiIconButton>
    <KRouterLink :to="to" :text="text" />
  </span>

</template>

<script>

  import UiIconButton from 'kolibri.coreVue.components.UiIconButton';

  export default {
    name: 'BackLink',
    components: {
      UiIconButton,
    },
    props: {
      text: {
        type: String,
        required: true,
      },
      to: {
        type: Object,
        required: true,
      },
    },
    methods: {
      go() {
        this.$router.push(this.to);
      },
    },
  };

</script>

<style lang="scss" scoped>

  .offset {
    position: relative;
    left: -10px;
  }

</style>

<template>

  <!-- TODO: move this to be a core KBackLink -->
  <span class="offset">
    <UiIconButton
      type="flat"
      class="icon"
      tabIndex="-1"
      aria-hidden="true"
      @click="go"
    >
      <mat-svg
        v-if="!isRtl"
        name="arrow_back"
        category="navigation"
        :style="{ fill: $themeTokens.link }"
      />
      <mat-svg
        v-else
        name="arrow_forward"
        category="navigation"
        :style="{ fill: $themeTokens.link }"
      />
    </UiIconButton>
    <KRouterLink :to="to" :text="text" />
  </span>

</template>

<script>

  import UiIconButton from 'kolibri.coreVue.components.UiIconButton';

  export default {
    name: 'BackLink',
    components: {
      UiIconButton,
    },
    props: {
      text: {
        type: String,
        required: true,
      },
      to: {
        type: Object,
        required: true,
      },
    },
    methods: {
      go() {
        this.$router.push(this.to);
      },
    },
  };

</script>

<style lang="scss" scoped>

  .offset {
    position: relative;
    left: -10px;
  }

</style>


Migrate JP changes to vue single file component

<template>

Relion Processing

This page is for submitting processing jobs to Relion.

        This session ended at {{ sessionEndDateAsString }}. It may no longer be submitted for processing. OK

    <form novalidate v-bind:class="{loading: showSpinner}">

Project

                    Acquisition Software
                    <select name="projectAcquisitionSoftware"
                            v-if="!isFormReadOnly"
                            v-model="projectAcquisitionSoftware">
                        <option value="EPU">EPU</option>
                        <option value="SerialEM">SerialEM</option>
                    </select>

                    <input type="text" name="projectAcquisitionSoftware"
                           v-if="isFormReadOnly"
                           v-model="projectAcquisitionSoftware"
                           v-bind:readonly="true"
                           style="margin-bottom: 20px">

                    Movie File Name Extension
                    <select name="projectMovieFileNameExtension"
                            v-if="!isFormReadOnly"
                            v-model="projectMovieFileNameExtension">
                        <option value=".tif">.tif</option>
                        <option value=".tiff">.tiff</option>
                        <option value=".mrc">.mrc</option>
                    </select>

                    <input type="text" name="projectMovieFileNameExtension"
                           v-if="isFormReadOnly"
                           v-model="projectMovieFileNameExtension"
                           v-bind:readonly="true"
                           style="margin-bottom: 20px">

                    Gain Reference File
                    <input type="checkbox" name="projectGainReferenceFile"
                           v-model="projectGainReferenceFile"
                           v-bind:disabled="isFormReadOnly">

                    Gain Reference File Name
                    <input type="text" name="projectGainReferenceFileName"
                           v-validate="{ required: true, regex: /^[\w-]+\.[\w]{3,4}$/ }"
                           v-model="projectGainReferenceFileName"
                           v-bind:readonly="isFormReadOnly">
                    {{ errors.first('projectGainReferenceFileName') }}

Experiment

                    Voltage (kV)
                    <select name="voltage"
                            v-if="!isFormReadOnly"
                            v-model="voltage">
                        <option value="200">200</option>
                        <option value="300">300</option>
                    </select>

                    <input type="text"
                           v-if="isFormReadOnly"
                           v-model="voltage"
                           v-bind:readonly="true"
                           style="margin-bottom: 20px">

                    Spherical Aberration (mm)
                    <select name="sphericalAberration"
                            v-if="!isFormReadOnly"
                            v-model="sphericalAberration">
                        <option value="2.7">2.7 (Talos/Krios)</option>
                    </select>

                    <input type="text" name="sphericalAberration"
                           v-if="isFormReadOnly"
                           v-model="sphericalAberration"
                           v-bind:readonly="true"
                           style="margin-bottom: 20px">

                    Phase Plate Used
                    <input type="checkbox" name="findPhaseShift"
                           v-model="findPhaseShift"
                           v-bind:disabled="isFormReadOnly">

                    Pixel Size (Å/pixel)
                    <input type="text" name="pixelSize"
                           v-validate="'required|min_value:0.02|max_value:100'"
                           v-model="pixelSize"
                           v-on:keyup="onCalculateForMe"
                           v-bind:readonly="isFormReadOnly">
                    {{ errors.first('pixelSize') }}

                    Motion Correction Binning
                    <select name="voltage"
                            v-if="!isFormReadOnly"
                            v-model="motionCorrectionBinning">
                        <option value="1">1</option>
                        <option value="2">2</option>
                    </select>

                    <input type="text"
                           v-if="isFormReadOnly"
                           v-model="motionCorrectionBinning"
                           v-bind:readonly="true"
                           style="margin-bottom: 20px">

                    Dose per frame (e-/Å2)
                    <input type="text" name="dosePerFrame"
                           v-validate="'required|min_value:0.02|max_value:10'"
                           v-model="dosePerFrame"
                           v-bind:readonly="isFormReadOnly">
                    {{ errors.first('dosePerFrame') }}

                    Continue after CTF estimation
                    <input type="checkbox" name="pipelineDo1stPass"
                           v-model="pipelineDo1stPass"
                           v-bind:disabled="isFormReadOnly">

2D & 3D Classification

                    Do 2D Classification
                    <input type="checkbox" name="pipelineDo1stPassClassification2d"
                           v-model="pipelineDo1stPassClassification2d"
                           v-bind:disabled="isFormReadOnly">

                    Do 3D Classification
                    <input type="checkbox" name="pipelineDo1stPassClassification3d"
                           v-model="pipelineDo1stPassClassification3d"
                           v-bind:disabled="isFormReadOnly">

Particle Picking

                    Use crYOLO
                    <input type="checkbox" name="particleUseCryolo"
                           v-model="particleUseCryolo"
                           v-bind:disabled="isFormReadOnly">

                    Minimum Diameter (Å)
                    <input type="text" name="particleDiameterMin"
                           v-validate="'required|min_value:0.02|max_value:1024'"
                           v-model="particleDiameterMin"
                           v-bind:readonly="isFormReadOnly">
                    {{ errors.first('particleDiameterMin') }}

                    Maximum Diameter (Å)
                    <input type="text" name="particleDiameterMax"
                           v-validate="'required|min_value:0.02|max_value:4000'"
                           v-model="particleDiameterMax"
                           v-on:keyup="onCalculateForMe"
                           v-bind:readonly="isFormReadOnly">
                    {{ errors.first('particleDiameterMax') }}

                    Mask Diameter (Å)
                    <input type="text" name="particleMaskDiameter"
                           v-validate="'required|min_value:0.1|max_value:1024'"
                           v-model="particleMaskDiameter"
                           v-bind:readonly="isFormReadOnly || particleCalculateForMe">
                    {{ errors.first('particleMaskDiameter') }}

                    Box Size (px)
                    <input type="text" name="particleBoxSize"
                           v-validate="'required|min_value:0.1|max_value:1024'"
                           v-model="particleBoxSize"
                           v-bind:readonly="isFormReadOnly || particleCalculateForMe">
                    {{ errors.first('particleBoxSize') }}

                    Downsample Box Size (px)
                    <input type="text" name="particleBoxSizeSmall"
                           v-validate="'required|min_value:0.1|max_value:1024'"
                           v-model="particleBoxSizeSmall"
                           v-bind:readonly="isFormReadOnly || particleCalculateForMe">
                    {{ errors.first('particleBoxSizeSmall') }}

                    Calculate For Me
                    <input type="checkbox" name="particleCalculateForMe"
                           v-model="particleCalculateForMe"
                           @change="onCalculateForMe"
                           v-bind:disabled="isFormReadOnly">

Second Pass

                    Do Second Pass
                    <input type="checkbox" name="pipelineDo2ndPass"
                           v-model="pipelineDo2ndPass"
                           v-bind:disabled="isFormReadOnly">

                    Do 2D Classification
                    <input type="checkbox" name="pipelineDo2ndPassClassification2d"
                           v-model="pipelineDo2ndPassClassification2d"
                           v-bind:disabled="isFormReadOnly">

                    Do 3D Classification
                    <input type="checkbox" name="pipelineDo2ndPassClassification3d"
                           v-model="pipelineDo2ndPassClassification3d"
                           v-bind:disabled="isFormReadOnly">

            <button type="submit" class="button submit"
                    v-on:click.prevent="onStart"
                    v-if="!isJobStarted && !isJobStopped"
                    v-bind:disabled="errors.any()">
                Start Processing
            </button>

    </form>

</template>

<template>

Relion Processing

This page is for submitting processing jobs to Relion.

        This session ended at {{ sessionEndDateAsString }}. It may no longer be submitted for processing. OK

    <form novalidate v-bind:class="{loading: showSpinner}">

Project

                    Acquisition Software
                    <select name="projectAcquisitionSoftware"
                            v-if="!isFormReadOnly"
                            v-model="projectAcquisitionSoftware">
                        <option value="EPU">EPU</option>
                        <option value="SerialEM">SerialEM</option>
                    </select>

                    <input type="text" name="projectAcquisitionSoftware"
                           v-if="isFormReadOnly"
                           v-model="projectAcquisitionSoftware"
                           v-bind:readonly="true"
                           style="margin-bottom: 20px">

                    Movie File Name Extension
                    <select name="projectMovieFileNameExtension"
                            v-if="!isFormReadOnly"
                            v-model="projectMovieFileNameExtension">
                        <option value=".tif">.tif</option>
                        <option value=".tiff">.tiff</option>
                        <option value=".mrc">.mrc</option>
                    </select>

                    <input type="text" name="projectMovieFileNameExtension"
                           v-if="isFormReadOnly"
                           v-model="projectMovieFileNameExtension"
                           v-bind:readonly="true"
                           style="margin-bottom: 20px">

                    Gain Reference File
                    <input type="checkbox" name="projectGainReferenceFile"
                           v-model="projectGainReferenceFile"
                           v-bind:disabled="isFormReadOnly">

                    Gain Reference File Name
                    <input type="text" name="projectGainReferenceFileName"
                           v-validate="{ required: true, regex: /^[\w-]+\.[\w]{3,4}$/ }"
                           v-model="projectGainReferenceFileName"
                           v-bind:readonly="isFormReadOnly">
                    {{ errors.first('projectGainReferenceFileName') }}

Experiment

                    Voltage (kV)
                    <select name="voltage"
                            v-if="!isFormReadOnly"
                            v-model="voltage">
                        <option value="200">200</option>
                        <option value="300">300</option>
                    </select>

                    <input type="text"
                           v-if="isFormReadOnly"
                           v-model="voltage"
                           v-bind:readonly="true"
                           style="margin-bottom: 20px">

                    Spherical Aberration (mm)
                    <select name="sphericalAberration"
                            v-if="!isFormReadOnly"
                            v-model="sphericalAberration">
                        <option value="2.7">2.7 (Talos/Krios)</option>
                    </select>

                    <input type="text" name="sphericalAberration"
                           v-if="isFormReadOnly"
                           v-model="sphericalAberration"
                           v-bind:readonly="true"
                           style="margin-bottom: 20px">

                    Phase Plate Used
                    <input type="checkbox" name="findPhaseShift"
                           v-model="findPhaseShift"
                           v-bind:disabled="isFormReadOnly">

                    Pixel Size (Å/pixel)
                    <input type="text" name="pixelSize"
                           v-validate="'required|min_value:0.02|max_value:100'"
                           v-model="pixelSize"
                           v-on:keyup="onCalculateForMe"
                           v-bind:readonly="isFormReadOnly">
                    {{ errors.first('pixelSize') }}

                    Motion Correction Binning
                    <select name="voltage"
                            v-if="!isFormReadOnly"
                            v-model="motionCorrectionBinning">
                        <option value="1">1</option>
                        <option value="2">2</option>
                    </select>

                    <input type="text"
                           v-if="isFormReadOnly"
                           v-model="motionCorrectionBinning"
                           v-bind:readonly="true"
                           style="margin-bottom: 20px">

                    Dose per frame (e-/Å2)
                    <input type="text" name="dosePerFrame"
                           v-validate="'required|min_value:0.02|max_value:10'"
                           v-model="dosePerFrame"
                           v-bind:readonly="isFormReadOnly">
                    {{ errors.first('dosePerFrame') }}

                    Continue after CTF estimation
                    <input type="checkbox" name="pipelineDo1stPass"
                           v-model="pipelineDo1stPass"
                           v-bind:disabled="isFormReadOnly">

2D & 3D Classification

                    Do 2D Classification
                    <input type="checkbox" name="pipelineDo1stPassClassification2d"
                           v-model="pipelineDo1stPassClassification2d"
                           v-bind:disabled="isFormReadOnly">

                    Do 3D Classification
                    <input type="checkbox" name="pipelineDo1stPassClassification3d"
                           v-model="pipelineDo1stPassClassification3d"
                           v-bind:disabled="isFormReadOnly">

Particle Picking

                    Use crYOLO
                    <input type="checkbox" name="particleUseCryolo"
                           v-model="particleUseCryolo"
                           v-bind:disabled="isFormReadOnly">

                    Minimum Diameter (Å)
                    <input type="text" name="particleDiameterMin"
                           v-validate="'required|min_value:0.02|max_value:1024'"
                           v-model="particleDiameterMin"
                           v-bind:readonly="isFormReadOnly">
                    {{ errors.first('particleDiameterMin') }}

                    Maximum Diameter (Å)
                    <input type="text" name="particleDiameterMax"
                           v-validate="'required|min_value:0.02|max_value:4000'"
                           v-model="particleDiameterMax"
                           v-on:keyup="onCalculateForMe"
                           v-bind:readonly="isFormReadOnly">
                    {{ errors.first('particleDiameterMax') }}

                    Mask Diameter (Å)
                    <input type="text" name="particleMaskDiameter"
                           v-validate="'required|min_value:0.1|max_value:1024'"
                           v-model="particleMaskDiameter"
                           v-bind:readonly="isFormReadOnly || particleCalculateForMe">
                    {{ errors.first('particleMaskDiameter') }}

                    Box Size (px)
                    <input type="text" name="particleBoxSize"
                           v-validate="'required|min_value:0.1|max_value:1024'"
                           v-model="particleBoxSize"
                           v-bind:readonly="isFormReadOnly || particleCalculateForMe">
                    {{ errors.first('particleBoxSize') }}

                    Downsample Box Size (px)
                    <input type="text" name="particleBoxSizeSmall"
                           v-validate="'required|min_value:0.1|max_value:1024'"
                           v-model="particleBoxSizeSmall"
                           v-bind:readonly="isFormReadOnly || particleCalculateForMe">
                    {{ errors.first('particleBoxSizeSmall') }}

                    Calculate For Me
                    <input type="checkbox" name="particleCalculateForMe"
                           v-model="particleCalculateForMe"
                           @change="onCalculateForMe"
                           v-bind:disabled="isFormReadOnly">

Second Pass

                    Do Second Pass
                    <input type="checkbox" name="pipelineDo2ndPass"
                           v-model="pipelineDo2ndPass"
                           v-bind:disabled="isFormReadOnly">

                    Do 2D Classification
                    <input type="checkbox" name="pipelineDo2ndPassClassification2d"
                           v-model="pipelineDo2ndPassClassification2d"
                           v-bind:disabled="isFormReadOnly">

                    Do 3D Classification
                    <input type="checkbox" name="pipelineDo2ndPassClassification3d"
                           v-model="pipelineDo2ndPassClassification3d"
                           v-bind:disabled="isFormReadOnly">

            <button type="submit" class="button submit"
                    v-on:click.prevent="onStart"
                    v-if="!isJobStarted && !isJobStopped"
                    v-bind:disabled="errors.any()">
                Start Processing
            </button>

            <button type="submit" class="button submit"
                    v-on:click.prevent="onStop"
                    v-if="isJobStarted && !isJobStopped">
                Stop Processing
            </button>

            <button type="submit" class="button submit"
                    v-on:click.prevent="onStart"
                    v-if="isJobStarted && isJobStopped">
                Resume
            </button>

            <button type="submit" class="button submit"
                    v-on:click.prevent="onReset"
                    v-if="isJobStarted && isJobStopped">
                Reset
            </button>

                        {{ sessionEvent.timestamp_str }} : {{ sessionEvent.message }}

    </form>

</template>