Fix problem of skipping over having visited 1st page

<template>

  <CoreFullscreen
    ref="pdfRenderer"
    class="pdf-renderer"
    :style="{ backgroundColor: $themeTokens.text }"
    @changeFullscreen="isInFullscreen = $event"
  >
    <KLinearLoader
      v-if="documentLoading || firstPageHeight === null"
      class="progress-bar"
      :delay="false"
      :type="progress > 0 ? 'determinate' : 'indeterminate'"
      :progress="progress * 100"
    />

    <template v-else>
      <transition name="slide">
        <div
          v-if="showControls"
          class="fullscreen-header"
          :style="{ backgroundColor: this.$themePalette.grey.v_100 }"
        >
          <KIconButton
            class="button-zoom-in controls"
            aria-controls="pdf-container"
            icon="add"
            @click="zoomIn"
          />
          <KIconButton
            class="button-zoom-out controls"
            aria-controls="pdf-container"
            icon="remove"
            @click="zoomOut"
          />
          <KButton
            class="fullscreen-button"
            :primary="false"
            appearance="flat-button"
            :icon="isInFullscreen ? 'fullscreen_exit' : 'fullscreen'"
            @click="$refs.pdfRenderer.toggleFullscreen()"
          >
            {{ fullscreenText }}
          </KButton>
        </div>
      </transition>
      <RecycleList
        ref="recycleList"
        :items="pdfPages"
        :itemHeight="itemHeight"
        :emitUpdate="true"
        :style="{ height: `${elementHeight}px` }"
        class="pdf-container"
        keyField="index"
        @update="handleUpdate"
      >
        <template #default="{ item }">
          <PdfPage
            :key="item.index"
            :pageNum="item.index + 1"
            :pdfPage="pdfPages[item.index].page"
            :pageReady="pdfPages[item.index].resolved"
            :firstPageHeight="firstPageHeight || 0"
            :firstPageWidth="firstPageWidth || 0"
            :scale="scale || 1"
          />
        </template>
      </RecycleList>
    </template>
  </CoreFullscreen>

</template>

<script>

  import PDFJSLib from 'pdfjs-dist';
  import Hammer from 'hammerjs';
  import throttle from 'lodash/throttle';
  import debounce from 'lodash/debounce';
  import { RecycleList } from 'vue-virtual-scroller';
  import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
  // polyfill necessary for recycle list
  import 'intersection-observer';
  import responsiveElementMixin from 'kolibri.coreVue.mixins.responsiveElementMixin';
  import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin';
  import CoreFullscreen from 'kolibri.coreVue.components.CoreFullscreen';
  import urls from 'kolibri.urls';
  import PdfPage from './PdfPage';
  // Source from which PDFJS loads its service worker, this is based on the __publicPath
  // global that is defined in the Kolibri webpack pipeline, and the additional entry in the PDF
  // renderer's own webpack config
  PDFJSLib.PDFJS.workerSrc = urls.static(`${__kolibriModuleName}/pdfJSWorker-${__version}.js`);
  // How often should we respond to changes in scrolling to render new pages?
  const renderDebounceTime = 300;
  const scaleIncrement = 0.25;
  const MARGIN = 16;
  export default {
    name: 'PdfRendererIndex',
    components: {
      PdfPage,
      RecycleList,
      CoreFullscreen,
    },
    mixins: [responsiveWindowMixin, responsiveElementMixin],
    data: () => ({
      progress: null,
      scale: null,
      timeout: null,
      totalPages: null,
      firstPageHeight: null,
      firstPageWidth: null,
      pdfPages: [],
      recycleListIsMounted: false,
      isInFullscreen: false,
      currentLocation: 0,
      updateContentStateInterval: null,
      showControls: true,
      visitedPages: {},
    }),
    computed: {
      // Returns whether or not the current device is iOS.
      // Probably not perfect, but worked in testing.
      iOS() {
        const iDevices = [
          'iPad Simulator',
          'iPhone Simulator',
          'iPod Simulator',
          'iPad',
          'iPhone',
          'iPod',
        ];
        return iDevices.includes(navigator.platform);
      },
      documentLoading() {
        return this.progress < 1;
      },
      itemHeight() {
        return this.firstPageHeight * this.scale + MARGIN;
      },
      savedLocation() {
        if (this.extraFields && this.extraFields.contentState) {
          return this.extraFields.contentState.savedLocation;
        }
        return 0;
      },
      savedVisitedPages: {
        get() {
          if (this.extraFields && this.extraFields.contentState) {
            return this.extraFields.contentState.savedVisitedPages || {};
          }
          return {};
        },
        set(value) {
          this.visitedPages = value;
        },
      },
      fullscreenText() {
        return this.isInFullscreen ? this.$tr('exitFullscreen') : this.$tr('enterFullscreen');
      },
      /* eslint-disable kolibri/vue-no-unused-properties */
      /**
       * @public
       */
      defaultDuration() {
        return this.totalPages * 30;
      },
      /* eslint-enable kolibri/vue-no-unused-properties */
    },
    watch: {
      recycleListIsMounted(newVal) {
        // On iOS pinch zooming always targets the document no matter what.
        // meta viewport attrs for `user-scalable` are ignored in iOS because
        // Apple considered it an a11y issue not to. So pinch-zooming on iOS
        // does not work well because - even if you zoom in on the PDF, the whole
        // screen zooms too which is jarring.
        if (newVal === true && !this.iOS) {
          const hammer = Hammer(this.$refs.recycleList.$el);
          hammer.get('pinch').set({ enable: true });
          hammer.on('pinchin', throttle(this.zoomOut, 1000));
          hammer.on('pinchout', throttle(this.zoomIn, 1000));
        }
      },
      scale(newScale, oldScale) {
        // Listen to changes in scale, as we have to rerender every visible page if it changes.
        const noChange = newScale === oldScale;
        const firstChange = oldScale === null;
        if (!noChange && !firstChange) {
          this.$nextTick(() => {
            this.forceUpdateRecycleList();
          });
        }
      },
      elementHeight() {
        if (this.recycleListIsMounted) {
          this.debounceForceUpdateRecycleList();
        }
      },
      // Listen to change in scroll position to determine whether we show top control bar or not
      currentLocation(newPos, oldPos) {
        if (newPos > oldPos) {
          this.showControls = false;
        } else {
          this.showControls = true;
        }
      },
    },
    destroyed() {
      // Reset the overflow on the HTML tag that we set to hidden in created()
      window.document.getElementsByTagName('html')[0].style.overflow = 'auto';
    },
    created() {
      // Override, only on this component, the overflow style of the HTML tag
      // so that PDFRenderer can scroll itself.
      window.document.getElementsByTagName('html')[0].style.overflow = 'hidden';

      this.currentLocation = this.savedLocation;
      this.showControls = true; // Ensures it shows on load even if we're scrolled
      const loadPdfPromise = PDFJSLib.getDocument(this.defaultFile.storage_url);
      // pass callback to update loading bar
      loadPdfPromise.onProgress = loadingProgress => {
        this.progress = loadingProgress.loaded / loadingProgress.total;
      };
      this.prepComponentData = loadPdfPromise.then(pdfDocument => {
        // Get initial info from the loaded pdf document
        this.pdfDocument = pdfDocument;
        this.totalPages = this.pdfDocument.numPages;
        // init pdfPages array
        for (let i = 0; i < this.totalPages; i++) {
          this.pdfPages.push({
            page: null,
            resolved: false,
            index: i,
          });
        }
        // Is either the first page or the saved last page visited
        const firstPageToRender = parseInt(this.getSavedPosition() * this.totalPages);
        return this.getPage(firstPageToRender + 1).then(firstPage => {
          this.firstPageHeight = firstPage.view[3];
          this.firstPageWidth = firstPage.view[2];
          const screenSizeMultiplier = this.windowIsLarge ? 1.25 : this.windowIsSmall ? 1 : 1.125;
          this.scale = this.elementWidth / (this.firstPageWidth * screenSizeMultiplier);
          // Set the firstPageToRender into the pdfPages object so that we do not refetch the page
          // from PDFJS when we do our initial render
          // splice so changes are detected
          this.pdfPages.splice(firstPageToRender, 1, {
            ...this.pdfPages[firstPageToRender],
            page: firstPage,
            resolved: true,
          });
        });
      });
    },
    mounted() {
      // Retrieve the document and its corresponding object
      this.prepComponentData.then(() => {
        // Progress is NaN if loadingProgress.total is undefined
        if (isNaN(this.progress)) {
          this.progress = 1;
        }
        this.$emit('startTracking');
        this.updateContentStateInterval = setInterval(this.updateProgress, 30000);
        // Even if user does not pause while scrolling on first page, we store that as visited
        this.storeVisitedPage(1);
      });
    },
    beforeDestroy() {
      this.updateProgress();
      this.updateContentState();
      if (this.timeout) {
        clearTimeout(this.timeout);
      }
      if (this.pdfDocument) {
        this.pdfDocument.cleanup();
        this.pdfDocument.destroy();
      }
      this.$emit('stopTracking');
      clearInterval(this.updateContentStateInterval);
    },
    methods: {
      getPage(pageNum) {
        return this.pdfDocument.getPage(pageNum);
      },
      showPage(pageNum) {
        // Only bother getting it if the pdfPage object is not already cached in the array
        if (pageNum > 0 && pageNum <= this.totalPages && !this.pdfPages[pageNum - 1].resolved) {
          const pageIndex = pageNum - 1;
          this.getPage(pageNum).then(pdfPage => {
            // splice so changes are detected
            this.pdfPages.splice(pageIndex, 1, {
              ...this.pdfPages[pageIndex],
              page: pdfPage,
              resolved: true,
            });
          });
        }
      },
      storeVisitedPage(currentPageNum) {
        let visited = this.savedVisitedPages;
        visited[currentPageNum] = true;
        this.savedVisitedPages = visited;
      },
      // handle the recycle list update event
      handleUpdate: debounce(function(start, end) {
        // check that it is mounted
        if (!this.$refs.recycleList || !this.$refs.recycleList.$el) {
          return;
        }
        // check if this is the first update event
        if (!this.recycleListIsMounted) {
          this.recycleListIsMounted = true;
          // save height
          this.recycleListHeight = this.calculateRecycleListHeight();
          // scroll to saved position
          this.scrollTo(this.getSavedPosition());
          return;
        }
        // check if height has changed indicating a change in scale
        // in that case we need to scroll to correct place that is saved
        const currentRecycleListHeight = this.calculateRecycleListHeight();
        if (this.recycleListHeight !== currentRecycleListHeight) {
          this.recycleListHeight = currentRecycleListHeight;
          this.scrollTo(this.getSavedPosition());
        } else {
          // TODO: there is a miscalculation that causes a wrong position change on scale
          this.savePosition(this.calculatePosition());

          // determine how many pages user has viewed/visited
          let currentPage = parseInt(this.currentLocation * this.totalPages) + 1;
          this.storeVisitedPage(currentPage);
          this.updateProgress();
          this.updateContentState();
        }
        const startIndex = Math.floor(start) + 1;
        const endIndex = Math.ceil(end) + 1;
        for (let i = startIndex; i <= endIndex; i++) {
          this.showPage(i);
        }
      }, renderDebounceTime),
      zoomIn() {
        this.setScale(Math.min(scaleIncrement * 20, this.scale + scaleIncrement));
      },
      zoomOut() {
        this.setScale(Math.max(scaleIncrement / 2, this.scale - scaleIncrement));
      },
      setScale: throttle(function(scaleValue) {
        this.scale = scaleValue;
      }, 500),
      calculatePosition() {
        return this.$refs.recycleList.$el.scrollTop / this.$refs.recycleList.$el.scrollHeight;
      },
      savePosition(val) {
        this.currentLocation = val;
      },
      getSavedPosition() {
        return this.currentLocation;
      },
      scrollTo(relativePosition) {
        this.$refs.recycleList.$el.scrollTop =
          this.$refs.recycleList.$el.scrollHeight * relativePosition;
      },
      calculateRecycleListHeight() {
        return this.$refs.recycleList.$el.scrollHeight;
      },
      debounceForceUpdateRecycleList: debounce(function() {
        this.forceUpdateRecycleList();
      }, renderDebounceTime),
      forceUpdateRecycleList() {
        this.$refs.recycleList.updateVisibleItems(false);
      },
      updateProgress() {
        if (this.forceDurationBasedProgress) {
          // update progress using total time user has spent on the pdf
          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.totalPages
          );
        }
      },
      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);
      },
    },
    $trs: {
      exitFullscreen: {
        message: 'Exit fullscreen',
        context:
          "Learners can use the Esc key or the 'exit fullscreen' button to close the fullscreen view on the PDF Viewer.",
      },
      enterFullscreen: {
        message: 'Enter fullscreen',
        context:
          'Learners can use the full screen button in the upper right corner to open a PDF in fullscreen view.',
      },
    },
  };

</script>

<style lang="scss" scoped>

  @import '~kolibri-design-system/lib/styles/definitions';
  $controls-height: 40px;
  $top-bar-height: 32px;

  .pdf-renderer {
    @extend %momentum-scroll;
    @extend %dropshadow-2dp;

    position: relative;
    height: calc(100vh - #{$top-bar-height} - #{$controls-height} + 16px);
    overflow-y: hidden;
  }
  .controls {
    position: relative;
    z-index: 0; // Hide icons with transition
    margin: 0 4px;
  }
  .progress-bar {
    top: 50%;
    max-width: 200px;
    margin: 0 auto;
  }
  // enable horizontal scrolling
  /deep/ .recycle-list {
    .item-wrapper {
      overflow-x: auto;
    }
  }
  .fullscreen-button {
    margin: 0;
    svg {
      position: relative;
      top: 8px;
    }
  }
  .fullscreen-header {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    z-index: 7;
    display: flex;
    justify-content: flex-end;
    height: $controls-height;
  }
  .slide-enter-active {
    @extend %md-accelerate-func;

    transition: all 0.3s;
  }
  .slide-leave-active {
    @extend %md-decelerate-func;

    transition: all 0.3s;
  }
  .slide-enter,
  .slide-leave-to {
    transform: translateY(-40px);
  }

</style>

<template>

  <CoreFullscreen
    ref="pdfRenderer"
    class="pdf-renderer"
    :style="{ backgroundColor: $themeTokens.text }"
    @changeFullscreen="isInFullscreen = $event"
  >
    <KLinearLoader
      v-if="documentLoading || firstPageHeight === null"
      class="progress-bar"
      :delay="false"
      :type="progress > 0 ? 'determinate' : 'indeterminate'"
      :progress="progress * 100"
    />

    <template v-else>
      <transition name="slide">
        <div
          v-if="showControls"
          class="fullscreen-header"
          :style="{ backgroundColor: this.$themePalette.grey.v_100 }"
        >
          <KIconButton
            class="button-zoom-in controls"
            aria-controls="pdf-container"
            icon="add"
            @click="zoomIn"
          />
          <KIconButton
            class="button-zoom-out controls"
            aria-controls="pdf-container"
            icon="remove"
            @click="zoomOut"
          />
          <KButton
            class="fullscreen-button"
            :primary="false"
            appearance="flat-button"
            :icon="isInFullscreen ? 'fullscreen_exit' : 'fullscreen'"
            @click="$refs.pdfRenderer.toggleFullscreen()"
          >
            {{ fullscreenText }}
          </KButton>
        </div>
      </transition>
      <RecycleList
        ref="recycleList"
        :items="pdfPages"
        :itemHeight="itemHeight"
        :emitUpdate="true"
        :style="{ height: `${elementHeight}px` }"
        class="pdf-container"
        keyField="index"
        @update="handleUpdate"
      >
        <template #default="{ item }">
          <PdfPage
            :key="item.index"
            :pageNum="item.index + 1"
            :pdfPage="pdfPages[item.index].page"
            :pageReady="pdfPages[item.index].resolved"
            :firstPageHeight="firstPageHeight || 0"
            :firstPageWidth="firstPageWidth || 0"
            :scale="scale || 1"
          />
        </template>
      </RecycleList>
    </template>
  </CoreFullscreen>

</template>

<script>

  import PDFJSLib from 'pdfjs-dist';
  import Hammer from 'hammerjs';
  import throttle from 'lodash/throttle';
  import debounce from 'lodash/debounce';
  import { RecycleList } from 'vue-virtual-scroller';
  import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
  // polyfill necessary for recycle list
  import 'intersection-observer';
  import responsiveElementMixin from 'kolibri.coreVue.mixins.responsiveElementMixin';
  import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin';
  import CoreFullscreen from 'kolibri.coreVue.components.CoreFullscreen';
  import urls from 'kolibri.urls';
  import PdfPage from './PdfPage';
  // Source from which PDFJS loads its service worker, this is based on the __publicPath
  // global that is defined in the Kolibri webpack pipeline, and the additional entry in the PDF
  // renderer's own webpack config
  PDFJSLib.PDFJS.workerSrc = urls.static(`${__kolibriModuleName}/pdfJSWorker-${__version}.js`);
  // How often should we respond to changes in scrolling to render new pages?
  const renderDebounceTime = 300;
  const scaleIncrement = 0.25;
  const MARGIN = 16;
  export default {
    name: 'PdfRendererIndex',
    components: {
      PdfPage,
      RecycleList,
      CoreFullscreen,
    },
    mixins: [responsiveWindowMixin, responsiveElementMixin],
    data: () => ({
      progress: null,
      scale: null,
      timeout: null,
      totalPages: null,
      firstPageHeight: null,
      firstPageWidth: null,
      pdfPages: [],
      recycleListIsMounted: false,
      isInFullscreen: false,
      currentLocation: 0,
      updateContentStateInterval: null,
      showControls: true,
      visitedPages: {},
    }),
    computed: {
      // Returns whether or not the current device is iOS.
      // Probably not perfect, but worked in testing.
      iOS() {
        const iDevices = [
          'iPad Simulator',
          'iPhone Simulator',
          'iPod Simulator',
          'iPad',
          'iPhone',
          'iPod',
        ];
        return iDevices.includes(navigator.platform);
      },
      documentLoading() {
        return this.progress < 1;
      },
      itemHeight() {
        return this.firstPageHeight * this.scale + MARGIN;
      },
      savedLocation() {
        if (this.extraFields && this.extraFields.contentState) {
          return this.extraFields.contentState.savedLocation;
        }
        return 0;
      },
      savedVisitedPages: {
        get() {
          if (this.extraFields && this.extraFields.contentState) {
            return this.extraFields.contentState.savedVisitedPages || {};
          }
          return {};
        },
        set(value) {
          this.visitedPages = value;
        },
      },
      fullscreenText() {
        return this.isInFullscreen ? this.$tr('exitFullscreen') : this.$tr('enterFullscreen');
      },
      /* eslint-disable kolibri/vue-no-unused-properties */
      /**
       * @public
       */
      defaultDuration() {
        return this.totalPages * 30;
      },
      /* eslint-enable kolibri/vue-no-unused-properties */
    },
    watch: {
      recycleListIsMounted(newVal) {
        // On iOS pinch zooming always targets the document no matter what.
        // meta viewport attrs for `user-scalable` are ignored in iOS because
        // Apple considered it an a11y issue not to. So pinch-zooming on iOS
        // does not work well because - even if you zoom in on the PDF, the whole
        // screen zooms too which is jarring.
        if (newVal === true && !this.iOS) {
          const hammer = Hammer(this.$refs.recycleList.$el);
          hammer.get('pinch').set({ enable: true });
          hammer.on('pinchin', throttle(this.zoomOut, 1000));
          hammer.on('pinchout', throttle(this.zoomIn, 1000));
        }
      },
      scale(newScale, oldScale) {
        // Listen to changes in scale, as we have to rerender every visible page if it changes.
        const noChange = newScale === oldScale;
        const firstChange = oldScale === null;
        if (!noChange && !firstChange) {
          this.$nextTick(() => {
            this.forceUpdateRecycleList();
          });
        }
      },
      elementHeight() {
        if (this.recycleListIsMounted) {
          this.debounceForceUpdateRecycleList();
        }
      },
      // Listen to change in scroll position to determine whether we show top control bar or not
      currentLocation(newPos, oldPos) {
        if (newPos > oldPos) {
          this.showControls = false;
        } else {
          this.showControls = true;
        }
      },
    },
    destroyed() {
      // Reset the overflow on the HTML tag that we set to hidden in created()
      window.document.getElementsByTagName('html')[0].style.overflow = 'auto';
    },
    created() {
      // Override, only on this component, the overflow style of the HTML tag
      // so that PDFRenderer can scroll itself.
      window.document.getElementsByTagName('html')[0].style.overflow = 'hidden';

      this.currentLocation = this.savedLocation;
      this.showControls = true; // Ensures it shows on load even if we're scrolled
      const loadPdfPromise = PDFJSLib.getDocument(this.defaultFile.storage_url);
      // pass callback to update loading bar
      loadPdfPromise.onProgress = loadingProgress => {
        this.progress = loadingProgress.loaded / loadingProgress.total;
      };
      this.prepComponentData = loadPdfPromise.then(pdfDocument => {
        // Get initial info from the loaded pdf document
        this.pdfDocument = pdfDocument;
        this.totalPages = this.pdfDocument.numPages;
        // init pdfPages array
        for (let i = 0; i < this.totalPages; i++) {
          this.pdfPages.push({
            page: null,
            resolved: false,
            index: i,
          });
        }
        // Is either the first page or the saved last page visited
        const firstPageToRender = parseInt(this.getSavedPosition() * this.totalPages);
        return this.getPage(firstPageToRender + 1).then(firstPage => {
          this.firstPageHeight = firstPage.view[3];
          this.firstPageWidth = firstPage.view[2];
          const screenSizeMultiplier = this.windowIsLarge ? 1.25 : this.windowIsSmall ? 1 : 1.125;
          this.scale = this.elementWidth / (this.firstPageWidth * screenSizeMultiplier);
          // Set the firstPageToRender into the pdfPages object so that we do not refetch the page
          // from PDFJS when we do our initial render
          // splice so changes are detected
          this.pdfPages.splice(firstPageToRender, 1, {
            ...this.pdfPages[firstPageToRender],
            page: firstPage,
            resolved: true,
          });
        });
      });
    },
    mounted() {
      // Retrieve the document and its corresponding object
      this.prepComponentData.then(() => {
        // Progress is NaN if loadingProgress.total is undefined
        if (isNaN(this.progress)) {
          this.progress = 1;
        }
        this.$emit('startTracking');
        this.updateContentStateInterval = setInterval(this.updateProgress, 30000);
      });
    },
    beforeDestroy() {
      this.updateProgress();
      this.updateContentState();
      if (this.timeout) {
        clearTimeout(this.timeout);
      }
      if (this.pdfDocument) {
        this.pdfDocument.cleanup();
        this.pdfDocument.destroy();
      }
      this.$emit('stopTracking');
      clearInterval(this.updateContentStateInterval);
    },
    methods: {
      getPage(pageNum) {
        return this.pdfDocument.getPage(pageNum);
      },
      showPage(pageNum) {
        // Only bother getting it if the pdfPage object is not already cached in the array
        if (pageNum > 0 && pageNum <= this.totalPages && !this.pdfPages[pageNum - 1].resolved) {
          const pageIndex = pageNum - 1;
          this.getPage(pageNum).then(pdfPage => {
            // splice so changes are detected
            this.pdfPages.splice(pageIndex, 1, {
              ...this.pdfPages[pageIndex],
              page: pdfPage,
              resolved: true,
            });
          });
        }
      },
      storeVisitedPage(currentPageNum) {
        let visited = this.savedVisitedPages;
        visited[currentPageNum] = true;
        this.savedVisitedPages = visited;
      },
      // handle the recycle list update event
      handleUpdate: debounce(function(start, end) {
        // check that it is mounted
        if (!this.$refs.recycleList || !this.$refs.recycleList.$el) {
          return;
        }
        // check if this is the first update event
        if (!this.recycleListIsMounted) {
          this.recycleListIsMounted = true;
          // save height
          this.recycleListHeight = this.calculateRecycleListHeight();
          // scroll to saved position
          this.scrollTo(this.getSavedPosition());
          return;
        }
        // check if height has changed indicating a change in scale
        // in that case we need to scroll to correct place that is saved
        const currentRecycleListHeight = this.calculateRecycleListHeight();
        if (this.recycleListHeight !== currentRecycleListHeight) {
          this.recycleListHeight = currentRecycleListHeight;
          this.scrollTo(this.getSavedPosition());
        } else {
          // TODO: there is a miscalculation that causes a wrong position change on scale
          this.savePosition(this.calculatePosition());

          // determine how many pages user has viewed/visited
          let currentPage = parseInt(this.currentLocation * this.totalPages) + 1;
          this.storeVisitedPage(currentPage);
          this.updateProgress();
          this.updateContentState();
        }
        const startIndex = Math.floor(start) + 1;
        const endIndex = Math.ceil(end) + 1;
        for (let i = startIndex; i <= endIndex; i++) {
          this.showPage(i);
        }
      }, renderDebounceTime),
      zoomIn() {
        this.setScale(Math.min(scaleIncrement * 20, this.scale + scaleIncrement));
      },
      zoomOut() {
        this.setScale(Math.max(scaleIncrement / 2, this.scale - scaleIncrement));
      },
      setScale: throttle(function(scaleValue) {
        this.scale = scaleValue;
      }, 500),
      calculatePosition() {
        return this.$refs.recycleList.$el.scrollTop / this.$refs.recycleList.$el.scrollHeight;
      },
      savePosition(val) {
        this.currentLocation = val;
      },
      getSavedPosition() {
        return this.currentLocation;
      },
      scrollTo(relativePosition) {
        this.$refs.recycleList.$el.scrollTop =
          this.$refs.recycleList.$el.scrollHeight * relativePosition;
      },
      calculateRecycleListHeight() {
        return this.$refs.recycleList.$el.scrollHeight;
      },
      debounceForceUpdateRecycleList: debounce(function() {
        this.forceUpdateRecycleList();
      }, renderDebounceTime),
      forceUpdateRecycleList() {
        this.$refs.recycleList.updateVisibleItems(false);
      },
      updateProgress() {
        if (this.forceDurationBasedProgress) {
          // update progress using total time user has spent on the pdf
          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.totalPages
          );
        }
      },
      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);
      },
    },
    $trs: {
      exitFullscreen: {
        message: 'Exit fullscreen',
        context:
          "Learners can use the Esc key or the 'exit fullscreen' button to close the fullscreen view on the PDF Viewer.",
      },
      enterFullscreen: {
        message: 'Enter fullscreen',
        context:
          'Learners can use the full screen button in the upper right corner to open a PDF in fullscreen view.',
      },
    },
  };

</script>

<style lang="scss" scoped>

  @import '~kolibri-design-system/lib/styles/definitions';
  $controls-height: 40px;
  $top-bar-height: 32px;

  .pdf-renderer {
    @extend %momentum-scroll;
    @extend %dropshadow-2dp;

    position: relative;
    height: calc(100vh - #{$top-bar-height} - #{$controls-height} + 16px);
    overflow-y: hidden;
  }
  .controls {
    position: relative;
    z-index: 0; // Hide icons with transition
    margin: 0 4px;
  }
  .progress-bar {
    top: 50%;
    max-width: 200px;
    margin: 0 auto;
  }
  // enable horizontal scrolling
  /deep/ .recycle-list {
    .item-wrapper {
      overflow-x: auto;
    }
  }
  .fullscreen-button {
    margin: 0;
    svg {
      position: relative;
      top: 8px;
    }
  }
  .fullscreen-header {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    z-index: 7;
    display: flex;
    justify-content: flex-end;
    height: $controls-height;
  }
  .slide-enter-active {
    @extend %md-accelerate-func;

    transition: all 0.3s;
  }
  .slide-leave-active {
    @extend %md-decelerate-func;

    transition: all 0.3s;
  }
  .slide-enter,
  .slide-leave-to {
    transform: translateY(-40px);
  }

</style>


[#43] create data placeholder for d3

[#43] create data placeholder for d3

<template>

        <transition name="translate">

Beta

                    <el-progress type="circle" :percentage="progress"></el-progress>

Provide more information to Zeus to get a prediction on your case

FEEDBACK

RESET CONVERSATION

        </transition>

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

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

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

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

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

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

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

Prediction Accuracy

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

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

                                    <el-carousel-item :key="key" :name="key">
                                        <vue-chart
                                            type="bar"
                                            :data="chartData"
                                            :options="{scales: {yAxes: [{ticks: {beginAtZero: true}}]}}"
                                            :update-config="{duration: 800, easing: 'easeOutBounce'}"
                                        ></vue-chart>
                                        properties
                                    </el-carousel-item>

                            </el-carousel>

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

Case Verdict

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

{{ key }}

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

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

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

                                <el-table :data="report.similar_precedents_table" stripe>

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

                                        <el-table-column :prop="fact" :label="fact" align="center"></el-table-column>

                                        <el-table-column :prop="outcome" :label="outcome" align="center"></el-table-column>

                                </el-table>

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

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

FEEDBACK

RESET CONVERSATION

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

        </transition>

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

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

        </el-dialog>

</template>


Issue17_点击选规格

<template>

{{message.name}}

              {{message.delivery_mode.text}}
              商家配送
              /
              {{message.order_lead_time}}分钟送达
              /
              {{message.piecewise_agent_fee.tips}}

              公告:
              {{message.promotion_info}}

        <activity :active="message.activities[0]"></activity>

        {{message.activities.length}}个活动

    <transition name="fade">

{{message.name}}

          <rating-star class="star" :rating="message.rating"></rating-star>

          优惠信息

        <activity v-for="item in message.activities" :active="item" :key="item.id" class="active"></activity>

          商家公告

{{message.promotion_info}}

    </transition>

        商品

        评价

        店铺

              {{item.name}}
              {{categoryCount[index]}}

                {{menu.name}}
                {{menu.description}}

                      {{food.name}}

                      {{food.description}}

                      月售{{food.month_sales}}
                      好评率{{food.satisfy_rate}}%

                      {{food.activity.benefit_text}}
                      {{food.activity.applicable_quantity_text}}

                      ¥
                      {{food.specfoods[0].price}}
                      起

                        {{foodCount[menuIndex][foodIndex]}}

                        选规格

评价

店铺

    <transition name="fade">

    </transition>

    <transition name="zoom">

{{selectContent.name}}

        {{selectContent.specifications[0].name}}

            {{name}}

            ¥
            {{specPrice}}

            选好了

    </transition>

</template>

<template>

{{message.name}}

              {{message.delivery_mode.text}}
              商家配送
              /
              {{message.order_lead_time}}分钟送达
              /
              {{message.piecewise_agent_fee.tips}}

              公告:
              {{message.promotion_info}}

        <activity :active="message.activities[0]"></activity>

        {{message.activities.length}}个活动

    <transition name="fade">

{{message.name}}

          <rating-star class="star" :rating="message.rating"></rating-star>

          优惠信息

        <activity v-for="item in message.activities" :active="item" :key="item.id" class="active"></activity>

          商家公告

{{message.promotion_info}}

    </transition>

        商品

        评价

        店铺

              {{item.name}}
              {{categoryCount[index]}}

                {{menu.name}}
                {{menu.description}}

                      {{food.name}}

                      {{food.description}}

                      月售{{food.month_sales}}
                      好评率{{food.satisfy_rate}}%

                      {{food.activity.benefit_text}}
                      {{food.activity.applicable_quantity_text}}

                      ¥
                      {{food.specfoods[0].price}}
                      起

                        {{foodCount[menuIndex][foodIndex]}}

                        选规格

评价

店铺

</template>


Fix ozaria link in encouragement modal

Fix ozaria link in encouragement modal

<template>
    <base-modal v-if="show" class="encouragement-modal">
        <template slot="header">

                    {{ $t('teacher_ozaria_encouragement_modal.title') }}

                    {{ $t('teacher_ozaria_encouragement_modal.sub_title') }}

        </template>

        <template slot="body">

                            <youtube
                                    class="player"
                                    ref="player"
                                    video-id="prJJvvI3WJM"
                                    @playing="startPlay"
                                    :player-vars="playerVars"
                            />

{{ $t('teacher_ozaria_encouragement_modal.bullet1') }}

{{ $t('teacher_ozaria_encouragement_modal.bullet2') }}

{{ $t('teacher_ozaria_encouragement_modal.bullet3') }}

{{ $t('teacher_ozaria_encouragement_modal.bullet4') }}

        </template>

        <template slot="footer">

                        {{ $t('teacher_ozaria_encouragement_modal.cancel') }}

                        {{ $t('teacher_ozaria_encouragement_modal.accept') }}

                    {{ $t('teacher_ozaria_encouragement_modal.you_can_return') }}

        </template>
    </base-modal>
</template>


Keep the sidebar visible

Keep the sidebar visible

Signed-off-by: Joas Schilling <ab43a7c9cb5b2380afc4ddf8b3e2583169b39a02@schilljs.com>

<template>

		<input id="file-upload"
			ref="fileUploadInput"
			multiple
			type="file"
			tabindex="-1"
			aria-hidden="true"
			class="hidden-visually"
			@change="handleFileInput">

			<form class="new-message-form"
				@submit.prevent>

					<NcActions ref="attachmentsMenu"
						:container="container"
						:boundaries-element="containerElement"
						:disabled="disabled"
						:aria-label="t('spreed', 'Share files to the conversation')"
						:aria-haspopup="true">
						<template #icon>
							<Paperclip :size="16" />
						</template>

						<NcActionButton v-if="canUploadFiles"
							:close-after-click="true"
							@click.prevent="clickImportInput">
							<template #icon>
								<Upload :size="20" />
							</template>
							{{ t('spreed', 'Upload from device') }}
						</NcActionButton>
						<NcActionButton v-if="canShareFiles"
							:close-after-click="true"
							@click.prevent="handleFileShare">
							<template #icon>
								<Folder :size="20" />
							</template>
							{{ shareFromNextcloudLabel }}
						</NcActionButton>
						<template v-if="canShareFiles">
							<NcActionButton v-for="(fileTemplate, i) in fileTemplateOptions"
								:key="i"
								:close-after-click="true"
								:icon="fileTemplate.iconClass"
								@click.prevent="showTextFileDialog = i">
								{{ fileTemplate.label }}
							</NcActionButton>
						</template>
						<NcActionButton v-if="canCreatePoll"
							:close-after-click="true"
							@click.prevent="toggleSimplePollsEditor(true)">
							<template #icon>
								<Poll :size="20" />
							</template>
							{{ t('spreed', 'Create new poll') }}
						</NcActionButton>
					</NcActions>

						<NcEmojiPicker v-if="!disabled"
							:container="container"
							:close-on-select="false"
							@select="addEmoji">
							<NcButton :disabled="disabled"
								:aria-label="t('spreed', 'Add emoji')"
								type="tertiary-no-background"
								:aria-haspopup="true">
								<template #icon>
									<EmoticonOutline :size="16" />
								</template>
							</NcButton>
						</NcEmojiPicker>

						<NcButton v-else
							type="tertiary"
							:aria-label="t('spreed', 'Add emoji')"
							:disabled="true">
							<template #icon>
								<EmoticonOutline :size="16" />
							</template>
						</NcButton>

						<Quote :is-new-message-form-quote="true"
							:parent-id="messageToBeReplied.id"
							v-bind="messageToBeReplied" />

					<AdvancedInput ref="advancedInput"
						v-model="text"
						:token="token"
						:active-input="!disabled"
						:placeholder-text="placeholderText"
						:aria-label="placeholderText"
						@update:contentEditable="contentEditableToParsed"
						@submit="handleSubmit({ silent: false })"
						@files-pasted="handlePastedFiles" />

				<AudioRecorder v-if="!hasText && canUploadFiles"
					:disabled="disabled"
					@recording="handleRecording"
					@audio-file="handleAudioFile" />

				<template v-else>
					<NcActions :force-menu="true">

						<NcActionButton :close-after-click="true"
							icon="icon-upload"
							:title="t('spreed', 'Send without notification')"
							@click.prevent="handleSubmit({ silent: true })">
							{{ silentSendInfo }}
							<BellOff slot="icon"
								:size="16" />
						</NcActionButton>
					</NcActions>

					<NcButton :disabled="disabled"
						type="tertiary"
						native-type="submit"
						:title="t('spreed', 'Send message')"
						:aria-label="t('spreed', 'Send message')"
						@click.prevent="handleSubmit({ silent: false })">
						<template #icon>
							<Send :size="16" />
						</template>
					</NcButton>
				</template>
			</form>

		<SimplePollsEditor v-if="showSimplePollsEditor"
			:token="token"
			@close="toggleSimplePollsEditor(false)" />

		<NcModal v-if="showTextFileDialog !== false"
			size="small"
			@close="dismissTextFileCreation">

					{{ t('spreed', 'Create and share a new file') }}

				<form class="new-text-file__form"
					@submit.prevent="handleCreateTextFile">
					<NcTextField id="new-file-form-name"
						ref="textFileTitleInput"
						:error="!!newFileError"
						:helper-text="newFileError"
						:label="t('spreed', 'Name of the new file')"
						:placeholder="textFileTitle"
						:value.sync="textFileTitle" />
				</form>

					<NcButton type="tertiary"
						@click="dismissTextFileCreation">
						{{ t('spreed', 'Close') }}
					</NcButton>
					<NcButton type="primary"
						@click="handleCreateTextFile">
						{{ t('spreed', 'Create file') }}
					</NcButton>

		</NcModal>

</template>


Fix #3841

Fix #3841

<template>
  <form method="POST" action="#" accept-charset="UTF-8" class="form-horizontal" id="store"
        enctype="multipart/form-data">
    <input name="_token" type="hidden" value="xxx">

          <button type="button" class="close" data-dismiss="alert" v-bind:aria-label="$t('firefly.close')">×</button>
          {{ $t("firefly.flash_error") }} {{ error_message }}

          <button type="button" class="close" data-dismiss="alert" v-bind:aria-label="$t('firefly.close')">×</button>
          {{ $t("firefly.flash_success") }}

                 1">{{ $t('firefly.single_split') }} {{ index + 1 }} / {{
                    transactions.length
                  }}
                {{ $t('firefly.transaction_journal_information') }}

 1" x>
                <button type="button" v-on:click="deleteTransaction(index, $event)" class="btn btn-xs btn-danger"></button>

                  <transaction-description v-if="transactionType.toLowerCase() !== 'reconciliation'"
                                           v-model="transaction.description"
                                           :index="index"
                                           :error="transaction.errors.description"
                  >
                  </transaction-description>
                  <account-select v-if="transactionType.toLowerCase() !== 'reconciliation'"
                                  inputName="source[]"
                                  v-bind:inputDescription="$t('firefly.source_account')"
                                  :accountName="transaction.source_account.name"
                                  :accountTypeFilters="transaction.source_account.allowed_types"
                                  :transactionType="transactionType"
                                  :index="index"
                                  v-on:clear:value="clearSource(index)"
                                  v-on:select:account="selectedSourceAccount(index, $event)"
                                  :error="transaction.errors.source_account"
                  ></account-select>

                          {{ $t('firefly.source_account_reconciliation') }}

                  <account-select v-if="transactionType.toLowerCase() !== 'reconciliation'"
                                  inputName="destination[]"
                                  v-bind:inputDescription="$t('firefly.destination_account')"
                                  :accountName="transaction.destination_account.name"
                                  :accountTypeFilters="transaction.destination_account.allowed_types"
                                  :transactionType="transactionType"
                                  :index="index"
                                  v-on:clear:value="clearDestination(index)"
                                  v-on:select:account="selectedDestinationAccount(index, $event)"
                                  :error="transaction.errors.destination_account"
                  ></account-select>

                          {{ $t('firefly.destination_account_reconciliation') }}

                  <standard-date
                      v-model="transaction.date"
                      :index="index"
                      :error="transaction.errors.date"
                  >
                  </standard-date>

                    <transaction-type
                        :source="transaction.source_account.type"
                        :destination="transaction.destination_account.type"
                        v-on:set:transactionType="setTransactionType($event)"
                        v-on:act:limitSourceType="limitSourceType($event)"
                        v-on:act:limitDestinationType="limitDestinationType($event)"
                    ></transaction-type>

                  <amount
                      :source="transaction.source_account"
                      :destination="transaction.destination_account"
                      v-model="transaction.amount"
                      :error="transaction.errors.amount"
                      :transactionType="transactionType"
                  ></amount>
                  <foreign-amount v-if="transactionType.toLowerCase() !== 'reconciliation'"
                                  :source="transaction.source_account"
                                  :destination="transaction.destination_account"
                                  v-model="transaction.foreign_amount"
                                  :transactionType="transactionType"
                                  :error="transaction.errors.foreign_amount"
                                  :no_currency="$t('firefly.none_in_select_list')"
                                  v-bind:title="$t('form.foreign_amount')"
                  ></foreign-amount>

                  <budget
                      :transactionType="transactionType"
                      v-model="transaction.budget"
                      :error="transaction.errors.budget_id"
                      :no_budget="$t('firefly.none_in_select_list')"
                  ></budget>
                  <category
                      :transactionType="transactionType"
                      v-model="transaction.category"
                      :error="transaction.errors.category"
                  ></category>
                  <tags
                      :transactionType="transactionType"
                      :tags="transaction.tags"
                      v-model="transaction.tags"
                      :error="transaction.errors.tags"
                  ></tags>
                  <bill
                      :transactionType="transactionType"
                      v-model="transaction.bill"
                      :error="transaction.errors.bill_id"
                      :no_bill="$t('firefly.none_in_select_list')"
                  ></bill>
                  <custom-transaction-fields
                      v-model="transaction.custom_fields"
                      :error="transaction.errors.custom_errors"
                  ></custom-transaction-fields>

              <button class="btn btn-default" type="button" @click="addTransaction">{{
                  $t('firefly.add_another_split')
                }}
              </button>

 1">

              {{ $t('firefly.split_transaction_title') }}

            <group-description
                :error="group_title_errors"
                v-model="group_title"
            ></group-description>

              {{ $t('firefly.submission') }}

                <input v-model="returnAfter" name="return_after" type="checkbox">
                {{ $t('firefly.after_update_create_another') }}

                <input v-model="storeAsNew" name="store_as_new" type="checkbox">
                {{ $t('firefly.store_as_new') }}

              <button class="btn btn-success" @click="submit">{{ $t('firefly.update_transaction') }}</button>

  </form>
</template>


docs options - fix ajax single select

docs options - fix ajax single select

<template>

select

        The select option is used to identify the select element that will be used to
        create slim select. You can any value you normally would in a querySelector
        or pass the element directly.

          new SlimSelect({
            select: '.element .you #want',
            // or
            select: document.querySelector('.element .you #want')
          })

          // If you already have a created slim select you can select your div
          // and continue to use slim select off of the element using the .slim data value
          let el = document.querySelector('.element .you #want')
          el.slim.open() // Or any other options/methods

ajax

        Slim select allows you to syncronize result values from your ajax requests.

        Call callback() method with slimselect data, false or string with specific string.

        <select id="ajaxSingle">
          <option data-placeholder="true"></option>
        </select>
        <select id="ajaxMultiple" multiple></select>

          new SlimSelect({
            select: '#ajax',
            ajax: function (search, callback) {
              // Check search value. If you dont like it callback(false) or callback('Message String')
              if (search.length < 3) {
                callback('Need 3 characters')
                return
              }

              // Perform your own ajax request here
              fetch('https://jsonplaceholder.typicode.com/users')
              .then(function (response) {
                return response.json()
              })
              .then(function (json) {
                let data = []
                for (let i = 0; i < json.length; i++) {
                  data.push({text: json[i].name})
                }

                // Upon successful fetch send data to callback function.
                // Be sure to send data back in the proper format.
                // Refer to the method setData for examples of proper format.
                callback(data)
              })
              .catch(function(error) {
                // If any erros happened send false back through the callback
                callback(false)
              })
            }
          })

placeholder

        Placeholders consists of setting the placeholder option value.
        The only difference is single selects require an empty option with data-placeholder set to true.
        Default value is "Select Value".

        <select id="placeholderSingle">
          <option data-placeholder="true"></option>
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>
        <select id="placeholderMultiple" multiple>
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>

          new SlimSelect({
            select: '#placeholder',
            placeholder: 'Placeholder Text Here'
          })

          <!-- Set empty option with data-placeholder if you to have placeholder -->
          <!-- text for single select, otherwise first option will be selected -->
          <select id="placeholderSingle">
            <option data-placeholder="true"></option>
            <option value="value1">Value 1</option>
            <option value="value2">Value 2</option>
            <option value="value3">Value 3</option>
          </select>

allowDeselect

        This will allow you to deselect a value on a single select dropdown.

        Be sure to have an empty option data placeholder so slim select has an empty string value to select.

        <select id="allowDeselect">
          <option data-placeholder="true"></option>
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>

          new SlimSelect({
            select: '.element .you #want',
            allowDeselect: true
          })

          <!-- Requires emtpy data-placeholder option -->
          <select id="allowDeselect">
            <option data-placeholder="true"></option>
            <option value="value1">Value 1</option>
            <option value="value2">Value 2</option>
            <option value="value3">Value 3</option>
          </select>

addable

        Slim select has the ability to dynamically add options via the search input field

        addable is a callback which takes a function parameter.
        The return value is the string of the value you want added.

        <select id="addableSingle">
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>
        <select id="addableMultiple" multiple>
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>

          new SlimSelect({
            select: '.element .you #want',
            // Optional - In the event you want to alter/validate it as a return value
            addable: function (value) {
              // return false or null if you do not want to allow value to be submitted
              if (value === 'bad') {return false}

              // Return the value string
              return value // Optional - value alteration // ex: value.toLowerCase()

              // Optional - Return a valid data object. See methods/setData for list of valid options
              return {
                text: value,
                value: value.toLowerCase()
              }
            }
          })

css / class

        Slim select will inherit any styles and classes that were added to the original select element.

        <select id="select-class" class="select-class">
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>

          <select id="select-class" class="classItems">
            <option value="value1">Value 1</option>
            <option value="value2">Value 2</option>
            <option value="value3">Value 3</option>
          </select>

innerHTML

        Slim select has the ability to set custom html in content in the select option.

        Due to html spec we cant do this directly on the select dropdown, but we can pass them as data values to slim select

        <select id="selectInnerHTMLSingle"></select>
        <select id="selectInnerHTMLMulti" multiple></select>

Use text for selected values

        <select id="selectInnerHTMLSingleText"></select>
        <select id="selectInnerHTMLMultiText" multiple></select>

          <select id="select-innerHTML"></select>

          var select = new SlimSelect({
            select: '#select-innerHTML',
            valuesUseText: false, // Use text instead of innerHTML for selected values - default false
            data: [
              {innerHTML: '<img height="30" width="30" src="http://lorempixel.com/30/30" /> Image', text: 'Image', value: 'image'},
              {innerHTML: '<b>Bold Text</b>', text: 'Bold Text', value: 'bold text'},
              {innerHTML: '<div style="border: solid 1px #666666;">Border</div>', text: 'Border', value: 'border'},
              {innerHTML: '<i>Slim Select you are awesome</i>', text: 'Slim Select awesome'}
            ]
          })

showSearch / searchText / searchHighlight

        showSearch is a boolean value that will decide whether or not to show the search.
        Default is true.

        searchText is a string value that will show in the event there are no results.
        Default is 'No Results'.

        searchPlaceholder is a string value that will set the value of the input search placeholder text.
        Default is 'Search'.

        searchHighlight is a boolean value that will highlight search results.
        Default is false.

        <select id="showSearch">
          <option value="dog">Dog</option>
          <option value="cat">Cat</option>
          <option value="bird">Bird</option>
        </select>
        <select id="searchText">
          <option value="dog">Dog</option>
          <option value="cat">Cat</option>
          <option value="bird">Bird</option>
        </select>
        <select id="searchPlaceholder">
          <option value="dog">Dog</option>
          <option value="cat">Cat</option>
          <option value="bird">Bird</option>
        </select>
        <select id="searchHighlight">
          <option value="dog">Dog</option>
          <option value="cat">Cat</option>
          <option value="bird">Bird</option>
        </select>

          let slim = new SlimSelect({
            select: '#search',
            showSearch: false,
            searchText: 'Sorry nothing to see here',
            searchPlaceholder: 'Search for the good stuff!',
            searchHighlight: true
          })

          // You can also set search text via method
          slim.setSearchText('Searching...')

closeOnSelect

        closeOnSelect is a boolean value in which determines whether or not to
        close the content area upon selecting a value.

        <select id="closeOnSelectSingle">
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>
        <select id="closeOnSelectMultiple" multiple>
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>

          new SlimSelect({
            select: '#search',
            closeOnSelect: false
          })

showContent

        showContent is a string value that will decide where to show your content when it comes out.
        By default slim select will try to put the content where it can without going off screen.
        But you may want to always show it in one direction.

        Possible Options: 'auto', 'up' or 'down'. Default is 'auto'

        <select id="showContentUp">
          <option value="up1">Up 1</option>
          <option value="up2">Up 2</option>
          <option value="up3">Up 3</option>
        </select>
        <select id="showContentDown">
          <option value="down1">Down 1</option>
          <option value="down2">Down 2</option>
          <option value="down3">Down 3</option>
        </select>

          new SlimSelect({
            select: '#showContent',
            showContent: 'auto' // 'auto', 'up' or 'down'
          })

beforeOnChange

        beforeOnChange will trigger a callback on an option click and will allow
        you the ability to halt if the value it produces isnt to your liking.
        In order to stop propagation just return false on the callback.
        Returning nothing or true will allow it to continue as normal.

Before on change: {{beforeOnChangeSingle}}

      <select id="beforeOnChangeSingle">
        <option value="value1">Value 1</option>
        <option value="value2">Value 2</option>
        <option value="value3">Value 3</option>
      </select>

Before on change: {{beforeOnChangeMultiple}}

      <select id="beforeOnChangeMultiple" multiple>
        <option value="value1" selected>Value 1</option>
        <option value="value2">Value 2</option>
        <option value="value3">Value 3</option>
      </select>

          new SlimSelect({
            select: '#beforeOnChange',
            beforeOnChange: (info) => {
              console.log(info)
              return false // this will stop propagation
            }
          })

onChange

        onChange will trigger a callback after the value of the select dropdown has changed.

On change: {{onChangeSingle}}

      <select id="onChangeSingle">
        <option value="value1">Value 1</option>
        <option value="value2">Value 2</option>
        <option value="value3">Value 3</option>
      </select>

On change: {{onChangeMultiple}}

      <select id="onChangeMultiple" multiple>
        <option value="value1" selected>Value 1</option>
        <option value="value2">Value 2</option>
        <option value="value3">Value 3</option>
      </select>

          new SlimSelect({
            select: '#onChange',
            onChange: (info) => {
              console.log(info)
            }
          })

showOptionTooltips

        showOptionTooltips option is used to active displaying the on-hover tooltips for select options.
        The tooltip text is equal to the option text content.

      <select id="showOptionTooltips">
        <option value="value1">Value 1</option>
        <option value="value2">Value 2</option>
        <option value="value3">Value 3</option>
      </select>

          new SlimSelect({
            select: '#showOptionTooltips',
            showOptionTooltips: true
          })

</template>

<template>

select

        The select option is used to identify the select element that will be used to
        create slim select. You can any value you normally would in a querySelector
        or pass the element directly.

          new SlimSelect({
            select: '.element .you #want',
            // or
            select: document.querySelector('.element .you #want')
          })

          // If you already have a created slim select you can select your div
          // and continue to use slim select off of the element using the .slim data value
          let el = document.querySelector('.element .you #want')
          el.slim.open() // Or any other options/methods

ajax

        Slim select allows you to syncronize result values from your ajax requests.

        Call callback() method with slimselect data, false or string with specific string.

        <select id="ajaxSingle"></select>
        <select id="ajaxMultiple" multiple></select>

          new SlimSelect({
            select: '#placeholder',
            ajax: function (search, callback) {
              // Check search value. If you dont like it callback(false) or callback('Message String')
              if (search.length < 3) {
                callback('Need 3 characters')
                return
              }

              // Perform your own ajax request here
              fetch('https://jsonplaceholder.typicode.com/users')
              .then(function (response) {
                return response.json()
              })
              .then(function (json) {
                let data = []
                for (let i = 0; i < json.length; i++) {
                  data.push({text: json[i].name})
                }

                // Upon successful fetch send data to callback function.
                // Be sure to send data back in the proper format.
                // Refer to the method setData for examples of proper format.
                callback(data)
              })
              .catch(function(error) {
                // If any erros happened send false back through the callback
                callback(false)
              })
            }
          })

placeholder

        Placeholders consists of setting the placeholder option value.
        The only difference is single selects require an empty option with data-placeholder set to true.
        Default value is "Select Value".

        <select id="placeholderSingle">
          <option data-placeholder="true"></option>
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>
        <select id="placeholderMultiple" multiple>
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>

          new SlimSelect({
            select: '#placeholder',
            placeholder: 'Placeholder Text Here'
          })

          <!-- Set empty option with data-placeholder if you to have placeholder -->
          <!-- text for single select, otherwise first option will be selected -->
          <select id="placeholderSingle">
            <option data-placeholder="true"></option>
            <option value="value1">Value 1</option>
            <option value="value2">Value 2</option>
            <option value="value3">Value 3</option>
          </select>

allowDeselect

        This will allow you to deselect a value on a single select dropdown.

        Be sure to have an empty option data placeholder so slim select has an empty string value to select.

        <select id="allowDeselect">
          <option data-placeholder="true"></option>
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>

          new SlimSelect({
            select: '.element .you #want',
            allowDeselect: true
          })

          <!-- Requires emtpy data-placeholder option -->
          <select id="allowDeselect">
            <option data-placeholder="true"></option>
            <option value="value1">Value 1</option>
            <option value="value2">Value 2</option>
            <option value="value3">Value 3</option>
          </select>

addable

        Slim select has the ability to dynamically add options via the search input field

        addable is a callback which takes a function parameter.
        The return value is the string of the value you want added.

        <select id="addableSingle">
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>
        <select id="addableMultiple" multiple>
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>

          new SlimSelect({
            select: '.element .you #want',
            // Optional - In the event you want to alter/validate it as a return value
            addable: function (value) {
              // return false or null if you do not want to allow value to be submitted
              if (value === 'bad') {return false}

              // Return the value string
              return value // Optional - value alteration // ex: value.toLowerCase()

              // Optional - Return a valid data object. See methods/setData for list of valid options
              return {
                text: value,
                value: value.toLowerCase()
              }
            }
          })

css / class

        Slim select will inherit any styles and classes that were added to the original select element.

        <select id="select-class" class="select-class">
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>

          <select id="select-class" class="classItems">
            <option value="value1">Value 1</option>
            <option value="value2">Value 2</option>
            <option value="value3">Value 3</option>
          </select>

innerHTML

        Slim select has the ability to set custom html in content in the select option.

        Due to html spec we cant do this directly on the select dropdown, but we can pass them as data values to slim select

        <select id="selectInnerHTMLSingle"></select>
        <select id="selectInnerHTMLMulti" multiple></select>

Use text for selected values

        <select id="selectInnerHTMLSingleText"></select>
        <select id="selectInnerHTMLMultiText" multiple></select>

          <select id="select-innerHTML"></select>

          var select = new SlimSelect({
            select: '#select-innerHTML',
            valuesUseText: false, // Use text instead of innerHTML for selected values - default false
            data: [
              {innerHTML: '<img height="30" width="30" src="http://lorempixel.com/30/30" /> Image', text: 'Image', value: 'image'},
              {innerHTML: '<b>Bold Text</b>', text: 'Bold Text', value: 'bold text'},
              {innerHTML: '<div style="border: solid 1px #666666;">Border</div>', text: 'Border', value: 'border'},
              {innerHTML: '<i>Slim Select you are awesome</i>', text: 'Slim Select awesome'}
            ]
          })

showSearch / searchText / searchHighlight

        showSearch is a boolean value that will decide whether or not to show the search.
        Default is true.

        searchText is a string value that will show in the event there are no results.
        Default is 'No Results'.

        searchPlaceholder is a string value that will set the value of the input search placeholder text.
        Default is 'Search'.

        searchHighlight is a boolean value that will highlight search results.
        Default is false.

        <select id="showSearch">
          <option value="dog">Dog</option>
          <option value="cat">Cat</option>
          <option value="bird">Bird</option>
        </select>
        <select id="searchText">
          <option value="dog">Dog</option>
          <option value="cat">Cat</option>
          <option value="bird">Bird</option>
        </select>
        <select id="searchPlaceholder">
          <option value="dog">Dog</option>
          <option value="cat">Cat</option>
          <option value="bird">Bird</option>
        </select>
        <select id="searchHighlight">
          <option value="dog">Dog</option>
          <option value="cat">Cat</option>
          <option value="bird">Bird</option>
        </select>

          let slim = new SlimSelect({
            select: '#search',
            showSearch: false,
            searchText: 'Sorry nothing to see here',
            searchPlaceholder: 'Search for the good stuff!',
            searchHighlight: true
          })

          // You can also set search text via method
          slim.setSearchText('Searching...')

closeOnSelect

        closeOnSelect is a boolean value in which determines whether or not to
        close the content area upon selecting a value.

        <select id="closeOnSelectSingle">
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>
        <select id="closeOnSelectMultiple" multiple>
          <option value="value1">Value 1</option>
          <option value="value2">Value 2</option>
          <option value="value3">Value 3</option>
        </select>

          new SlimSelect({
            select: '#search',
            closeOnSelect: false
          })

showContent

        showContent is a string value that will decide where to show your content when it comes out.
        By default slim select will try to put the content where it can without going off screen.
        But you may want to always show it in one direction.

        Possible Options: 'auto', 'up' or 'down'. Default is 'auto'

        <select id="showContentUp">
          <option value="up1">Up 1</option>
          <option value="up2">Up 2</option>
          <option value="up3">Up 3</option>
        </select>
        <select id="showContentDown">
          <option value="down1">Down 1</option>
          <option value="down2">Down 2</option>
          <option value="down3">Down 3</option>
        </select>

          new SlimSelect({
            select: '#showContent',
            showContent: 'auto' // 'auto', 'up' or 'down'
          })

beforeOnChange

        beforeOnChange will trigger a callback on an option click and will allow
        you the ability to halt if the value it produces isnt to your liking.
        In order to stop propagation just return false on the callback.
        Returning nothing or true will allow it to continue as normal.

Before on change: {{beforeOnChangeSingle}}

      <select id="beforeOnChangeSingle">
        <option value="value1">Value 1</option>
        <option value="value2">Value 2</option>
        <option value="value3">Value 3</option>
      </select>

Before on change: {{beforeOnChangeMultiple}}

      <select id="beforeOnChangeMultiple" multiple>
        <option value="value1" selected>Value 1</option>
        <option value="value2">Value 2</option>
        <option value="value3">Value 3</option>
      </select>

          new SlimSelect({
            select: '#beforeOnChange',
            beforeOnChange: (info) => {
              console.log(info)
              return false // this will stop propagation
            }
          })

onChange

        onChange will trigger a callback after the value of the select dropdown has changed.

On change: {{onChangeSingle}}

      <select id="onChangeSingle">
        <option value="value1">Value 1</option>
        <option value="value2">Value 2</option>
        <option value="value3">Value 3</option>
      </select>

On change: {{onChangeMultiple}}

      <select id="onChangeMultiple" multiple>
        <option value="value1" selected>Value 1</option>
        <option value="value2">Value 2</option>
        <option value="value3">Value 3</option>
      </select>

          new SlimSelect({
            select: '#onChange',
            onChange: (info) => {
              console.log(info)
            }
          })

showOptionTooltips

        showOptionTooltips option is used to active displaying the on-hover tooltips for select options.
        The tooltip text is equal to the option text content.

      <select id="showOptionTooltips">
        <option value="value1">Value 1</option>
        <option value="value2">Value 2</option>
        <option value="value3">Value 3</option>
      </select>

          new SlimSelect({
            select: '#showOptionTooltips',
            showOptionTooltips: true
          })

</template>


fix 404 netlify exception

fix 404 netlify exception


:art:

:art:

<template>

	%i18n:@suspend-user%
	<input v-model="username" type="text" class="ui"/>
	<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button>

</template>


借伞成功 增加延迟提醒

借伞成功 增加延迟提醒

<template>

          共享伞解锁
          扫码借伞

          手动输入

        <input type="text" v-model="umbrella_number" placeholder="请输入伞柄上的数字">
        <input type="submit" value="立即用伞" @click="inputUnlock">

{{nickname}}

个人资料

我的钱包

资金记录

用伞记录

我要反馈

帮助中心

        ✖

             <%= site_name %>

 可用雨伞 <%= have %> 把

 可还伞位 <%= repay %> 把

            <img src="<%= photo %>"/>

</template>


Deactivate invite contacts.

Deactivate invite contacts.

<template>

                <v-icon @click="askQuestion">mdi-help-circle-outline</v-icon>

                    {{$t("common:navigation.askQuestion")}}

                <v-icon @click="createCommitment">mdi-human-handsup</v-icon>

                    {{$t("common:navigation.createCommitment")}}

            <v-btn outline color="primary" class="navigation-button-full-width"
                   @click="$router.push({name: 'login'})">
                {{$t('common:button.login')}}
            </v-btn>
            <v-btn color="secondary" class="navigation-button-full-width register-button"
                   @click="$router.push({name: 'register'})">
                {{$t('common:button.register')}}
            </v-btn>
            <v-divider></v-divider>

                <nuxt-link :to="{name: 'index'}">
                    <v-icon>mdi-home</v-icon>
                    {{$t("common:navigation.home")}}
                </nuxt-link>

                <v-icon @click="setLanguage()">mdi-web</v-icon>
                {{$t("common:navigation.language")}}

        <v-divider></v-divider>

                <nuxt-link :to="{name: 'index'}">
                    <v-icon>mdi-home</v-icon>
                    {{$t("common:navigation.home")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'vision'}">
                    <v-icon>mdi-rocket</v-icon>
                    {{$t("pages:footer.vision")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'aboutUs'}">
                    <v-icon>mdi-account-multiple</v-icon>
                    {{$t("pages:footer.aboutUs")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'contact'}">
                    <v-icon>mdi-account-box-outline</v-icon>
                    {{$t("pages:footer.contact")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'faq'}">
                    <v-icon>mdi-help</v-icon>
                    {{$t("pages:footer.faq")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'terms'}">
                    <v-icon>mdi-library</v-icon>
                    {{$t("pages:footer.terms")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'privacy'}">
                    <v-icon>mdi-security-lock</v-icon>
                    {{$t("pages:footer.privacy")}}
                </nuxt-link>

            <v-divider></v-divider>

                    <nuxt-link :to="{name: 'setting'}">
                        <v-icon>mdi-settings</v-icon>
                        {{$t("common:navigation.settings")}}
                    </nuxt-link>

                    <v-icon @click="logout">mdi-logout-variant</v-icon>
                    {{$t("common:navigation.logout")}}

        <language-dialog v-if="showChangeLanguage" @close-dialog="showChangeLanguage = false">
        </language-dialog>
        <create-commitment-dialog v-if="showCreateCommitment" @close-dialog="showCreateCommitment = false">
        </create-commitment-dialog>
        <create-question-dialog v-if="showCreateQuestion" @close-dialog="showCreateQuestion = false">
        </create-question-dialog>
        <import-contact-dialog v-if="showImportContact" @close-dialog="showImportContact = false">
        </import-contact-dialog>
        <v-snackbar top v-model="showError" color="error" :timeout="0">{{$t("common:error.unknown")}}
            <v-btn dark flat @click="showError = false">{{$t("common:button.close")}}</v-btn>
        </v-snackbar>

</template>

<template>

                <v-icon @click="askQuestion">mdi-help-circle-outline</v-icon>

                    {{$t("common:navigation.askQuestion")}}

                <v-icon @click="createCommitment">mdi-human-handsup</v-icon>

                    {{$t("common:navigation.createCommitment")}}

                <v-icon @click="inviteContacts">mdi-account-multiple-plus</v-icon>

                    {{$t("common:navigation.inviteContacts")}}

            <v-btn outline color="primary" class="navigation-button-full-width"
                   @click="$router.push({name: 'login'})">
                {{$t('common:button.login')}}
            </v-btn>
            <v-btn color="secondary" class="navigation-button-full-width register-button"
                   @click="$router.push({name: 'register'})">
                {{$t('common:button.register')}}
            </v-btn>
            <v-divider></v-divider>

                <nuxt-link :to="{name: 'index'}">
                    <v-icon>mdi-home</v-icon>
                    {{$t("common:navigation.home")}}
                </nuxt-link>

                <v-icon @click="setLanguage()">mdi-web</v-icon>
                {{$t("common:navigation.language")}}

        <v-divider></v-divider>

                <nuxt-link :to="{name: 'index'}">
                    <v-icon>mdi-home</v-icon>
                    {{$t("common:navigation.home")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'vision'}">
                    <v-icon>mdi-rocket</v-icon>
                    {{$t("pages:footer.vision")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'aboutUs'}">
                    <v-icon>mdi-account-multiple</v-icon>
                    {{$t("pages:footer.aboutUs")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'contact'}">
                    <v-icon>mdi-account-box-outline</v-icon>
                    {{$t("pages:footer.contact")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'faq'}">
                    <v-icon>mdi-help</v-icon>
                    {{$t("pages:footer.faq")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'terms'}">
                    <v-icon>mdi-library</v-icon>
                    {{$t("pages:footer.terms")}}
                </nuxt-link>

                <nuxt-link :to="{name: 'privacy'}">
                    <v-icon>mdi-security-lock</v-icon>
                    {{$t("pages:footer.privacy")}}
                </nuxt-link>

            <v-divider></v-divider>

                    <nuxt-link :to="{name: 'setting'}">
                        <v-icon>mdi-settings</v-icon>
                        {{$t("common:navigation.settings")}}
                    </nuxt-link>

                    <v-icon @click="logout">mdi-logout-variant</v-icon>
                    {{$t("common:navigation.logout")}}

        <language-dialog v-if="showChangeLanguage" @close-dialog="showChangeLanguage = false">
        </language-dialog>
        <create-commitment-dialog v-if="showCreateCommitment" @close-dialog="showCreateCommitment = false">
        </create-commitment-dialog>
        <create-question-dialog v-if="showCreateQuestion" @close-dialog="showCreateQuestion = false">
        </create-question-dialog>
        <import-contact-dialog v-if="showImportContact" @close-dialog="showImportContact = false">
        </import-contact-dialog>
        <v-snackbar top v-model="showError" color="error" :timeout="0">{{$t("common:error.unknown")}}
            <v-btn dark flat @click="showError = false">{{$t("common:button.close")}}</v-btn>
        </v-snackbar>

</template>


Changed loader style

Changed loader style

<template>
    <transition name="fade">

    </transition>
</template>


add mapState to songMetadata comp

add mapState to songMetadata comp

<template>
  <panel title="Song Metadata">
    <v-layout>
      <v-flex sx6>

          {{song.title}}

          {{song.artist}}

          {{song.genre}}

        <v-btn
          class="cyan"
          dark
          :to="{
            name: 'edit-song',
            params() {
              return {
                songId: song.id
              }
            }
          }">
          Edit
        </v-btn>
        <v-btn
          v-if="isUserLoggedIn && !bookmark"
          class="cyan"
          dark
          @click="setAsBookmark"
         >
          Set As Bookmark
        </v-btn>
        <v-btn
          v-if="isUserLoggedIn && bookmark"
          class="cyan"
          dark
          @click="unsetAsBookmark"
         >
          Unset As Bookmark
        </v-btn>
      </v-flex>

      <v-flex xs6>

        {{song.album}}
      </v-flex>
    </v-layout>
  </panel>
</template>


docs(Toc): add next layer of heading to available list

<template>
  <v-navigation-drawer
    id="documentation-toc"
    v-scroll="onScroll"
    :floating="structure === false"
    :right="!$vuetify.rtl"
    app
    clipped
    color="transparent"
  >
    <template v-if="structure !== false">

            Contents

        <template v-for="(item, i) in internalToc">

            <a
              :href="`#${item.id}`"
              class="d-block"
              @click.stop.prevent="goTo(`#${item.id}`)"
              v-html="item.text"
            />

        </template>

        <v-fade-transition appear>
          <supporters-supporter-group
            :group="supporters['Diamond']"
            small
            justify="start"
            title="Diamond Sponsors"
          />
        </v-fade-transition>

    </template>
  </v-navigation-drawer>
</template>


Fix .focus is not a function

Fix .focus is not a function

Signed-off-by: Marco Ambrosini <3397c7e210a9d1588196cfebb12b2c6b133f6934@icloud.com>

<!--
  - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
  -
  - @author Marco Ambrosini <marcoambrosini@icloud.com>
  -
  - @license GNU AGPL version 3 or any later version
  -
  - This program is free software: you can redistribute it and/or modify
  - it under the terms of the GNU Affero General Public License as
  - published by the Free Software Foundation, either version 3 of the
  - License, or (at your option) any later version.
  -
  - This program is distributed in the hope that it will be useful,
  - but WITHOUT ANY WARRANTY; without even the implied warranty of
  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  - GNU Affero General Public License for more details.
  -
  - You should have received a copy of the GNU Affero General Public License
  - along with this program. If not, see <http://www.gnu.org/licenses/>.
-->

<template>
	<form class="app-navigation-search"
		@submit.prevent="handleSubmit">
		<NcTextField ref="searchConversations"
			:value.sync="localValue"
			:place-holder="placeholderText"
			:show-trailing-button="isSearching"
			trailing-button-icon="close"
			@trailing-button-click="abortSearch"
			@keypress.enter.prevent="handleSubmit">
			<Magnify :size="16" />
		</NcTextField>
	</form>
</template>

<script>
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import Magnify from 'vue-material-design-icons/Magnify.vue'
import { EventBus } from '../../../services/EventBus.js'

export default {
	name: 'SearchBox',
	components: {
		NcTextField,
		Magnify,
	},
	props: {
		/**
		 * The placeholder for the input field
		 */
		placeholderText: {
			type: String,
			default: t('spreed', 'Search conversations or users'),
		},
		/**
		 * The value of the input field, when receiving it as a prop the localValue
		 * is updated.
		 */
		value: {
			type: String,
			required: true,
		},
		/**
		 * If true, this component displays an 'x' button to abort the search
		 */
		isSearching: {
			type: Boolean,
			default: false,
		},
	},
	data() {
		return {
			localValue: '',
		}
	},
	computed: {
		cancelSearchLabel() {
			return t('spreed', 'Cancel search')
		},
	},
	watch: {
		localValue(localValue) {
			this.$emit('update:value', localValue)
			this.$emit('input', localValue)
		},
		value(value) {
			this.localValue = value
		},
	},
	mounted() {
		this.focusInputIfRoot()
		/**
		 * Listen to routeChange global events and focus on the input
		 */
		EventBus.$on('route-change', this.focusInputIfRoot)
	},
	beforeDestroy() {
		EventBus.$off('route-change', this.focusInputIfRoot)
	},
	methods: {
		// Focus the input field of the searchbox component.
		focusInput() {
			this.$refs.searchConversations.$el.focus()
		},
		// Focuses the input if the current route is root.
		focusInputIfRoot() {
			if (this.$route.name === 'root') {
				this.focusInput()
			}
		},
		/**
		 * When the form is submitted we send this event up in order to allow for example
		 * to select the first search result and trigger a route change in the main view.
		 */
		handleSubmit() {
			this.$emit('submit')
		},
		/**
		 * Emits the abort-search event and re-focuses the input
		 */
		abortSearch() {
			this.$emit('abort-search')
			this.focusInput()
		},
	},
}
</script>

<style lang="scss" scoped>
@import '../../../assets/variables';

.app-navigation-search {
	position: sticky;
	top: 0;
	background-color: var(--color-main-background);
	z-index: 1;
	display: flex;
	justify-content: center;
	flex-grow: 1;
}

</style>

<!--
  - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
  -
  - @author Marco Ambrosini <marcoambrosini@icloud.com>
  -
  - @license GNU AGPL version 3 or any later version
  -
  - This program is free software: you can redistribute it and/or modify
  - it under the terms of the GNU Affero General Public License as
  - published by the Free Software Foundation, either version 3 of the
  - License, or (at your option) any later version.
  -
  - This program is distributed in the hope that it will be useful,
  - but WITHOUT ANY WARRANTY; without even the implied warranty of
  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  - GNU Affero General Public License for more details.
  -
  - You should have received a copy of the GNU Affero General Public License
  - along with this program. If not, see <http://www.gnu.org/licenses/>.
-->

<template>
	<form class="app-navigation-search"
		@submit.prevent="handleSubmit">
		<NcTextField ref="searchConversations"
			:value.sync="localValue"
			:place-holder="placeholderText"
			:show-trailing-button="isSearching"
			trailing-button-icon="close"
			@trailing-button-click="abortSearch"
			@keypress.enter.prevent="handleSubmit">
			<Magnify :size="16" />
		</NcTextField>
	</form>
</template>

<script>
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import Magnify from 'vue-material-design-icons/Magnify.vue'
import { EventBus } from '../../../services/EventBus.js'

export default {
	name: 'SearchBox',
	components: {
		NcTextField,
		Magnify,
	},
	props: {
		/**
		 * The placeholder for the input field
		 */
		placeholderText: {
			type: String,
			default: t('spreed', 'Search conversations or users'),
		},
		/**
		 * The value of the input field, when receiving it as a prop the localValue
		 * is updated.
		 */
		value: {
			type: String,
			required: true,
		},
		/**
		 * If true, this component displays an 'x' button to abort the search
		 */
		isSearching: {
			type: Boolean,
			default: false,
		},
	},
	data() {
		return {
			localValue: '',
		}
	},
	computed: {
		cancelSearchLabel() {
			return t('spreed', 'Cancel search')
		},
	},
	watch: {
		localValue(localValue) {
			this.$emit('update:value', localValue)
			this.$emit('input', localValue)
		},
		value(value) {
			this.localValue = value
		},
	},
	mounted() {
		this.focusInputIfRoot()
		/**
		 * Listen to routeChange global events and focus on the input
		 */
		EventBus.$on('route-change', this.focusInputIfRoot)
	},
	beforeDestroy() {
		EventBus.$off('route-change', this.focusInputIfRoot)
	},
	methods: {
		// Focus the input field of the searchbox component.
		focusInput() {
			this.$refs.searchConversations.focus()
		},
		// Focuses the input if the current route is root.
		focusInputIfRoot() {
			if (this.$route.name === 'root') {
				this.focusInput()
			}
		},
		/**
		 * When the form is submitted we send this event up in order to allow for example
		 * to select the first search result and trigger a route change in the main view.
		 */
		handleSubmit() {
			this.$emit('submit')
		},
		/**
		 * Emits the abort-search event and re-focuses the input
		 */
		abortSearch() {
			this.$emit('abort-search')
			this.focusInput()
		},
	},
}
</script>

<style lang="scss" scoped>
@import '../../../assets/variables';

.app-navigation-search {
	position: sticky;
	top: 0;
	background-color: var(--color-main-background);
	z-index: 1;
	display: flex;
	justify-content: center;
	flex-grow: 1;
}

</style>


Adding noopener noreferrer

Adding noopener noreferrer

<template>

            Object Link:

                {{cardLink.type}} - {{cardLink.id}}

                Card Title

                    Write an appropriate name for the kanban card. To update click on the "Update" button.

                Card Title
                <input v-model="cardTitle"
                        class="form-control"
                >

                Card Location

                    Select the appropriate location for this card.

                        Card Column
                        <n-select v-bind:options="listColumns"
                                  label="column"
                                  v-model:value="cardColumn"
                                  :disabled="kanbanStatus === 'Closed'"
                        ></n-select>

                        Card Level
                        <n-select v-bind:options="listLevels"
                                  label="level"
                                  v-model:value="cardLevel"
                                  :disabled="kanbanStatus === 'Closed'"
                        ></n-select>

                <button class="btn btn-secondary"
                        v-on:click="closeModal"
                >
                    Close
                </button>
                <button class="btn btn-primary save-changes"
                        v-on:click="updateCard"
                        v-if="kanbanStatus !== 'Closed'"
                >
                    Update Card
                </button>

</template>


removed highlight-field tooltip icon (for now)

<template>

	<back-bar
		:title="
			currentDefinition && currentDefinition.label ?
				currentDefinition.label : title
		"
	 />

		<el-form
			v-if="currentDefinition"
			label-position="top"
			:model="blockFields"
			:rules="rules"
			ref="block_fields"
		>
			<el-form-item
				v-for="field in currentDefinition.fields"
				:label="field.label"
				:prop="field.name"
				:error="
					typeof localErrors[field.name] === 'string' ?
						localErrors[field.name] :
						null
				"
				:key="field.name"
			>
				<template slot="label">
					{{ field.label }}

					<el-tooltip
						v-if="field.info"
						popper-class="el-tooltip__popper--narrow"
						:content="field.info"
						placement="top"
					>
						<icon
							class="el-form-item__icon-help"
							name="help-circle"
							width="15"
							height="15"
							viewBox="0 0 15 15"
						/>
					</el-tooltip>

				</template>
				<component
					:is="getField(field.type)"
					:field="field"
					:path="field.name"
					:index="currentIndex"
					:key="`${currentDefinition.name}-${currentIndex}`"
					:scrollTo="scrollTo"
					:errors="
						typeof localErrors[field.name] !== 'string' ?
							localErrors[field.name] :
							null
					"
				/>
			</el-form-item>
		</el-form>

Click a block to display its options in this sidebar.

		<el-button :plain="true" type="danger" @click="deleteThisBlock">Remove</el-button>
		<el-button @click="submitForm('block_fields')">Validate</el-button>

</template>

<template>

	<back-bar
		:title="
			currentDefinition && currentDefinition.label ?
				currentDefinition.label : title
		"
	 />

		<el-form
			v-if="currentDefinition"
			label-position="top"
			:model="blockFields"
			:rules="rules"
			ref="block_fields"
		>
			<el-form-item
				v-for="field in currentDefinition.fields"
				:label="field.label"
				:prop="field.name"
				:error="
					typeof localErrors[field.name] === 'string' ?
						localErrors[field.name] :
						null
				"
				:key="field.name"
			>
				<template slot="label">
					{{ field.label }}

					<el-tooltip
						v-if="field.info"
						popper-class="el-tooltip__popper--narrow"
						:content="field.info"
						placement="top"
					>
						<icon
							class="el-form-item__icon-help"
							name="help-circle"
							width="15"
							height="15"
							viewBox="0 0 15 15"
						/>
					</el-tooltip>

					<el-tooltip content="Highlight field" placement="top">
						<icon
							class="el-form-item__icon-view"
							name="eye"
							width="14"
							height="14"
							viewBox="0 0 14 14"
							@click="viewField(field.name)"
						/>
					</el-tooltip>
				</template>
				<component
					:is="getField(field.type)"
					:field="field"
					:path="field.name"
					:index="currentIndex"
					:key="`${currentDefinition.name}-${currentIndex}`"
					:scrollTo="scrollTo"
					:errors="
						typeof localErrors[field.name] !== 'string' ?
							localErrors[field.name] :
							null
					"
				/>
			</el-form-item>
		</el-form>

Click a block to display its options in this sidebar.

		<el-button :plain="true" type="danger" @click="deleteThisBlock">Remove</el-button>
		<el-button @click="submitForm('block_fields')">Validate</el-button>

</template>