Fix url path to display correct folder id when back or forward buttons are pressed

<template>
  <div>
    <userInputs
      v-on:changeViewMode="changeViewMode"
      v-on:execute="executeAction"
      v-on:changeFolder="changeFolder"
          :folder-path="folderPath"
          :view-mode="viewMode"
    ></userInputs>
    <documents
      v-on:changeFolder="changeFolder"
          :view-mode="viewMode"
          :docs="docs"
    ></documents>
  </div>
</template>

<script>
import fileManager from 'src/logic/filemanager';
import documents from './Documents';
import userInputs from './UserInputs';

let fileManagerVue = this;

function constructFolderPath(folderObj) {
  const folderPath = [];

  folderPath.push(folderObj);
  while (folderObj.parentFolder !== null) {
    folderPath.push(folderObj.parentFolder);
    folderObj = folderObj.parentFolder;
  }

  return folderPath.reverse();
}

export default {
  components: {
    documents,
    userInputs,
  },
  data() {
    return {
      viewMode: 'iconView',
      docs: {},
      history: null,
      folderPath: [],
      changeDocViewOnResize: function() {
        if (window.innerWidth < 480) {
          this.viewMode = 'listView';
        }
      }
    };
  },
  watch: {
  },
  methods: {
    changeFolder(folderObj) {
      this.folderPath = constructFolderPath(folderObj);
      this.updateUrlPath(folderObj);
      this.docs = folderObj;
      try {
        this.history.update(this.docs);
      } catch (error) {
        if (error.message === 'Attempting to update the same folder') {
          return;
        }
        throw error;
      }
    },
    changeViewMode(viewMode) {
      this.viewMode = viewMode;
    },
    updateUrlPath(folderObj) {
      if (folderObj.id === 0) {
        this.$router.push({ path: '' });
      } else {
        this.$router.push({ path: '', query: { folder: folderObj.id } });
      }
    },
    executeAction(action, data) {
      switch (action) {
        case 'createFolder': {
          this.docs.createFolder('Folder');
          break;
        }
        case 'createFile': {
          this.docs.createFile('File');
          break;
        }
        case 'history back': {
          this.docs = this.history.previous();
          this.folderPath = constructFolderPath(this.docs);
          this.updateUrlPath(this.docs);
          break;
        }
        case 'history forward': {
          this.docs = this.history.next();
          this.folderPath = constructFolderPath(this.docs);
          this.updateUrlPath(this.docs);
          break;
        }
        case 'download': {
          data.download();
          break;
        }
        case 'delete': {
          data.remove();
          break;
        }
        case 'search': {
          this.search(data);
          break;
        }
        default: break;
      }
    },
    search(searchStr) {
      if (searchStr === '') {
        const folderID = this.history.currFolderID;
        this.docs = fileManager.getFolderByID(folderID);
      } else {
        fileManager.searchAll(searchStr)
        .then((result) => { this.docs = result; })
        .catch((error) => {
          // Simple alert box for message
          // TODO: Use a less obstrusive alert message
        });
        // Intentionally not updating the history stack.
        // Since, there may be multiple incomplete searching,
        // updating the history stack may unnecessary populate the stack.
      }
    }
  },
  mounted() {
    fileManagerVue = this;
    const ROOT_FOLDER_ID = 0;
    this.docs = fileManager.getFolderByID(ROOT_FOLDER_ID);
    this.history = fileManager.createHistory(this.docs);

    this.changeDocViewOnResize();
    window.addEventListener('resize', this.changeDocViewOnResize.bind(fileManagerVue));
  },
  beforeDestroy() {
    fileManagerVue = this;

    window.removeEventListener('resize', this.changeDocViewOnResize.bind(fileManagerVue));
  }
};

</script>

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

</style>

<template>
  <div>
    <userInputs
      v-on:changeViewMode="changeViewMode"
      v-on:execute="executeAction"
      v-on:changeFolder="changeFolder"
          :folder-path="folderPath"
          :view-mode="viewMode"
    ></userInputs>
    <documents
      v-on:changeFolder="changeFolder"
          :view-mode="viewMode"
          :docs="docs"
    ></documents>
  </div>
</template>

<script>
import fileManager from 'src/logic/filemanager';
import documents from './Documents';
import userInputs from './UserInputs';

let fileManagerVue = this;

function constructFolderPath(folderObj) {
  const folderPath = [];

  folderPath.push(folderObj);
  while (folderObj.parentFolder !== null) {
    folderPath.push(folderObj.parentFolder);
    folderObj = folderObj.parentFolder;
  }

  return folderPath.reverse();
}

export default {
  components: {
    documents,
    userInputs,
  },
  data() {
    return {
      viewMode: 'iconView',
      docs: {},
      history: null,
      folderPath: [],
      changeDocViewOnResize: function() {
        if (window.innerWidth < 480) {
          this.viewMode = 'listView';
        }
      }
    };
  },
  watch: {
  },
  methods: {
    changeFolder(folderObj) {
      this.folderPath = constructFolderPath(folderObj);
      this.updateUrlPath(folderObj);
      this.docs = folderObj;
      try {
        this.history.update(this.docs);
      } catch (error) {
        if (error.message === 'Attempting to update the same folder') {
          return;
        }
        throw error;
      }
    },
    changeViewMode(viewMode) {
      this.viewMode = viewMode;
    },
    updateUrlPath(folderObj) {
      if (folderObj.id === 0) {
        this.$router.push({ path: '' });
      } else {
        this.$router.push({ path: '', query: { folder: folderObj.id } });
      }
    },
    executeAction(action, data) {
      switch (action) {
        case 'createFolder': {
          this.docs.createFolder('Folder');
          break;
        }
        case 'createFile': {
          this.docs.createFile('File');
          break;
        }
        case 'history back': {
          this.docs = this.history.previous();
          this.folderPath = constructFolderPath(this.docs);
          break;
        }
        case 'history forward': {
          this.docs = this.history.next();
          this.folderPath = constructFolderPath(this.docs);
          break;
        }
        case 'download': {
          data.download();
          break;
        }
        case 'delete': {
          data.remove();
          break;
        }
        case 'search': {
          this.search(data);
          break;
        }
        default: break;
      }
    },
    search(searchStr) {
      if (searchStr === '') {
        const folderID = this.history.currFolderID;
        this.docs = fileManager.getFolderByID(folderID);
      } else {
        fileManager.searchAll(searchStr)
        .then((result) => { this.docs = result; })
        .catch((error) => {
          // Simple alert box for message
          // TODO: Use a less obstrusive alert message
        });
        // Intentionally not updating the history stack.
        // Since, there may be multiple incomplete searching,
        // updating the history stack may unnecessary populate the stack.
      }
    }
  },
  mounted() {
    fileManagerVue = this;
    const ROOT_FOLDER_ID = 0;
    this.docs = fileManager.getFolderByID(ROOT_FOLDER_ID);
    this.history = fileManager.createHistory(this.docs);

    this.changeDocViewOnResize();
    window.addEventListener('resize', this.changeDocViewOnResize.bind(fileManagerVue));
  },
  beforeDestroy() {
    fileManagerVue = this;

    window.removeEventListener('resize', this.changeDocViewOnResize.bind(fileManagerVue));
  }
};

</script>

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

</style>


[SCI-10004] image formatting

[SCI-10004] image formatting

<template>
  <div class="preview-image">
    {{ heading }}
    <vue-xhr-image
      v-model="src"
      :title="heading"
      :image-url="imageUrl"
      @click="click"
    />
  </div>
</template>

<script>
import VueXhrImage from 'app/components/vue-xhr-image.vue'

export default {
    'name': 'PreviewImage',
    'components': {
        'vue-xhr-image': VueXhrImage,
    },
    'props': {
        'particleClass': {
            'type': Object,
            'required': true,
        },
        'index': {
            'type': Number,
            'required': true,
        },
        'sortBy': {
            'type': String,
            'required': true,
        },
    },
    'data': function() {
        return {
            'src': '',
        }
    },
    'computed': {
        'heading': function() {
            const sortField = this.sortBy == 'particles' ?
                this.particleClass.particlesPerClass :
                this.particleClass.estimatedResolution
            return this.particleClass.batchNumber + ' - ' +
                this.particleClass.classNumber +
                ' (' + sortField + ')'
        },
        'imageUrl': function() {
            return this.$store.state.apiUrl + '/em/classification/image/' +
                this.particleClass.particleClassificationId
        },
    },
    'watch': {
        'src': function() {
            if (this.index == 0) {
                this.click()
            }
        },
    },
    'methods': {
        'click': function() {
            this.$emit('click', {
                'src': this.src,
                'index': this.index,
            })
        },
    },
}
</script>

<style scoped>
.preview-image {
    text-align: center;
    background-color: #fff;
    padding: 5px;
    border-radius: 6px;
}
.preview-image img {
    width: 150px;
}
</style>

<template>
  <div class="preview-image">
    {{ heading }}
    <vue-xhr-image
      v-model="src"
      :title="heading"
      :image-url="imageUrl"
      @click="click"
    />
  </div>
</template>

<script>
import VueXhrImage from 'app/components/vue-xhr-image.vue'

export default {
    'name': 'PreviewImage',
    'components': {
        'vue-xhr-image': VueXhrImage,
    },
    'props': {
        'particleClass': {
            'type': Object,
            'required': true,
        },
        'index': {
            'type': Number,
            'required': true,
        },
        'sortBy': {
            'type': String,
            'required': true,
        },
    },
    'data': function() {
        return {
            'src': '',
        }
    },
    'computed': {
        'heading': function() {
            const sortField = this.sortBy == 'particles' ?
                this.particleClass.particlesPerClass :
                this.particleClass.estimatedResolution
            return this.particleClass.batchNumber + ' - ' +
                this.particleClass.classNumber +
                ' (' + sortField + ')'
        },
        'imageUrl': function() {
            return this.$store.state.apiUrl + '/em/classification/image/' +
                this.particleClass.particleClassificationId
        },
    },
    'watch': {
        'src': function() {
            if (this.index == 0) {
                this.click()
            }
        },
    },
    'methods': {
        'click': function() {
            this.$emit('click', {
                'src': this.src,
                'index': this.index,
            })
        },
    },
}
</script>

<style scoped>
.preview-image {
    text-align: center;
    background-color: #fff;
    padding: 2px;
    border-radius: 6px;
}
.preview-image img {
    width: 150px;
}
</style>


fix transitions in coach reports

fix transitions in coach reports

<template>

    <breadcrumbs :list="userBreadcrumbs"/>

    <all-recent-tabs
      :recentViewLink="recentViewLink"
      :allViewLink="allViewLink"
      :isRecentView="isRecentView"/>

        <breadcrumbs :list="contentBreadcrumbs"/>

        <report-header
          :contentKind="pageState.content_scope_summary.kind"
          :contentTitle="pageState.content_scope_summary.title"
          :userFullName="pageState.user_scope_summary.full_name"/>

        <view-by-switch
          v-if="!isRecentView"
          :isContent="isViewByContent"
          :vlink="viewByLink"
          :disabled="isSingleUser || isSingleItem"/>

                <item-cell
                  :kind="row.kind"
                  :title="row.title"
                  :id="row.id"
                  :parent="row.parent"
                  :exerciseCount="row.exerciseCount"
                  :contentCount="row.contentCount"
                ></item-cell>

                <progress-cell :num="row.exerciseProgress" :isExercise="true"></progress-cell>

                <progress-cell :num="row.contentProgress" :isExercise="false"></progress-cell>

                <date-cell :date="row.lastActive"></date-cell>

</template>

<template>

    <breadcrumbs :list="userBreadcrumbs"/>

    <all-recent-tabs
      :recentViewLink="recentViewLink"
      :allViewLink="allViewLink"
      :isRecentView="isRecentView"/>

        <breadcrumbs :list="contentBreadcrumbs"/>

        <report-header
          :contentKind="pageState.content_scope_summary.kind"
          :contentTitle="pageState.content_scope_summary.title"
          :userFullName="pageState.user_scope_summary.full_name"/>

        <view-by-switch
          v-if="!isRecentView"
          :isContent="isViewByContent"
          :vlink="viewByLink"
          :disabled="isSingleUser || isSingleItem"/>

            <transition name="item">

                  <item-cell
                    :kind="row.kind"
                    :title="row.title"
                    :id="row.id"
                    :parent="row.parent"
                    :exerciseCount="row.exerciseCount"
                    :contentCount="row.contentCount"
                  ></item-cell>

                  <progress-cell :num="row.exerciseProgress" :isExercise="true"></progress-cell>

                  <progress-cell :num="row.contentProgress" :isExercise="false"></progress-cell>

                  <date-cell :date="row.lastActive"></date-cell>

            </transition>

</template>


Hooked up tabs.

Hooked up tabs.

<template>

Reports

    <breadcrumbs :list="userBreadcrumbs"></breadcrumbs>

      <button @click="switchToRecentView" :class="{ active: isRecentView }">Recent</button>
      <button @click="switchToAllView" :class="{ active: !isRecentView }">Topics</button>

      <breadcrumbs :list="contentBreadcrumbs"></breadcrumbs>

      <report-header
        :contentkind="pageState.content_scope_summary.kind"
        :contenttitle="pageState.content_scope_summary.title"
        :userfullname="pageState.user_scope_summary.full_name"
      ></report-header>

      <view-by-switch
        v-if="!isRecentView"
        :iscontent="isContent"
        @toggled="switchView"
      ></view-by-switch>

      <list-section></list-section>

</template>

<template>

Reports

    <breadcrumbs :list="userBreadcrumbs"></breadcrumbs>

      <button :class="{ active: isRecentView }">Recent</button>
      <button :class="{ active: !isRecentView }">Topics</button>

      <breadcrumbs :list="contentBreadcrumbs"></breadcrumbs>

      <report-header
        :contentkind="pageState.content_scope_summary.kind"
        :contenttitle="pageState.content_scope_summary.title"
        :userfullname="pageState.user_scope_summary.full_name"
      ></report-header>

      <view-by-switch
        v-if="!isRecentView"
        :iscontent="isContent"
        @toggled="switchView"
      ></view-by-switch>

      <list-section></list-section>

</template>


Fix to BaseSelect.vue label position

Fix to BaseSelect.vue label position

<template>

    <select
      :name="name"
      :class="{
        'cl-tertiary' : options.length === 0,
        'empty': !selected
      }"
      :autocomplete="autocomplete"
      @focus="$emit('focus')"
      @blur="$emit('blur')"
      @change="$emit('input', $event.target.value)"
    >
      <option disabled selected value v-if="!selected"/>
      <option
        v-for="(option, key) in options"
        :key="key"
        :value="option.value"
        v-bind="{selected: option.value === selected}"
      >
        {{ option.label }}
      </option>
    </select>
    {{ placeholder }}

    <ValidationMessages v-if="validations" :validations="validations"/>

</template>


Get videos playing for the realm chunk posts

<template>
	<component
		:is="Screen.isXs ? AppScrollScroller : 'div'"
		:horizontal="Screen.isXs"
		:thin="Screen.isXs"
		:class="{ '-scroller': Screen.isXs }"
	>

			<AppSpacer horizontal :scale-xs="4" />

			<template v-for="num of cardsPerRow" :key="num">
				<AppSpacer v-if="num > 1" horizontal :scale="4" :scale-xs="2" />

					<template v-if="items[num - 1]">
						<AppPostCard
							:post="items[num - 1].action"
							source="realmChunk"
							with-user
							video-context="gif"
							:link-to="num === items.length ? realm.routeLocation : undefined"
						>
							<template v-if="num === items.length" #overlay>
								<RouterLink
									class="-overlay"
									:to="realm.routeLocation"
									@click="
										trackGotoRealm({
											path: realm.path,
											source: 'realmChunkPost',
										})
									"
								>

										<AppTranslate>See more in this realm</AppTranslate>

								</RouterLink>
							</template>
						</AppPostCard>
					</template>

			</template>

			<AppSpacer horizontal :scale-xs="4" />

	</component>
</template>

<template>
	<component
		:is="Screen.isXs ? AppScrollScroller : 'div'"
		:horizontal="Screen.isXs"
		:thin="Screen.isXs"
		:class="{ '-scroller': Screen.isXs }"
	>

			<AppSpacer horizontal :scale-xs="4" />

			<template v-for="num of cardsPerRow" :key="num">
				<AppSpacer v-if="num > 1" horizontal :scale="4" :scale-xs="2" />

					<template v-if="items[num - 1]">
						<AppPostCard
							:post="items[num - 1].action"
							source="realmChunk"
							with-user
							:link-to="num === items.length ? realm.routeLocation : undefined"
						>
							<template v-if="num === items.length" #overlay>
								<RouterLink
									class="-overlay"
									:to="realm.routeLocation"
									@click="
										trackGotoRealm({
											path: realm.path,
											source: 'realmChunkPost',
										})
									"
								>

										<AppTranslate>See more in this realm</AppTranslate>

								</RouterLink>
							</template>
						</AppPostCard>
					</template>

			</template>

			<AppSpacer horizontal :scale-xs="4" />

	</component>
</template>


Disabled auto-refresh on post page

Disabled auto-refresh on post page

<template>
  <div>
    <list :main="id" all :data="data" :option="option" type="Post" :key="`post-${id}`" :auto-refresh="false" />
  </div>
</template>

<script>
import Post from '~components/Post'
import Compose from '~components/Compose'
import List from '~components/List'
import api from '~plugins/api'
import bus from '~assets/js/bus'

export default {
  async asyncData(ctx) {
    const { params: { id }, req } = ctx
    const _api = api(ctx)
    const option = {
      include_directed_posts: 1,
      include_bookmarked_by: 1,
      include_reposted_by: 1
    }
    const postPromise = _api.fetch({
      include_directed_posts: 1,
      include_bookmarked_by: 1,
      include_reposted_by: 1
    })

    const data = await postPromise
    data.data = data.data.reverse()
    return {
      id, option, data
    }
  },
  // validate ({ params }) {
  //   return /^\d+$/.test(params.id)
  // },
  mounted() {
    bus.$on('post', this.addAfter)
  },
  beforeDestroy() {
    bus.$off('post', this.addAfter)
  },
  components: {
    Post, Compose, List
  },
  methods: {
    addAfter (post) {
      // TODO: push post to list if post target is this page's post
      // this.data.data.push(post)
    }
  },
  head() {
    const [post] = this.data.data
      .filter(post => post.id === this.id)
    let title = post.content.text
    if(title.length > 30) {
      title = title.substr(0, 30) + '…'
    }
    const name = post.user.name
      ? `${post.user.name}(@${post.user.username})`
      : `@${post.user.username}`
    title = `${name}: ${title}`
    return {
      title
    }
  }
}
</script>

<style scoped lang="scss">
@import '~assets/css/mixin';

.post {
  @include no-gutter-xs
}
</style>

<template>
  <div>
    <list :main="id" all :data="data" :option="option" type="Post" :key="`post-${id}`" />
  </div>
</template>

<script>
import Post from '~components/Post'
import Compose from '~components/Compose'
import List from '~components/List'
import api from '~plugins/api'
import bus from '~assets/js/bus'

export default {
  async asyncData(ctx) {
    const { params: { id }, req } = ctx
    const _api = api(ctx)
    const option = {
      include_directed_posts: 1,
      include_bookmarked_by: 1,
      include_reposted_by: 1
    }
    const postPromise = _api.fetch({
      include_directed_posts: 1,
      include_bookmarked_by: 1,
      include_reposted_by: 1
    })

    const data = await postPromise
    data.data = data.data.reverse()
    return {
      id, option, data
    }
  },
  // validate ({ params }) {
  //   return /^\d+$/.test(params.id)
  // },
  mounted() {
    bus.$on('post', this.addAfter)
  },
  beforeDestroy() {
    bus.$off('post', this.addAfter)
  },
  components: {
    Post, Compose, List
  },
  methods: {
    addAfter (post) {
      // TODO: push post to list if post target is this page's post
      // this.data.data.push(post)
    }
  },
  head() {
    const [post] = this.data.data
      .filter(post => post.id === this.id)
    let title = post.content.text
    if(title.length > 30) {
      title = title.substr(0, 30) + '…'
    }
    const name = post.user.name
      ? post.user.name
      : `@${post.user.username}`
    title = `${name}: ${title}`
    return {
      title
    }
  }
}
</script>

<style scoped lang="scss">
@import '~assets/css/mixin';

.post {
  @include no-gutter-xs
}
</style>


Allow esc key to close search input

Allow esc key to close search input

<template>
	<form :class="['message-search', {opened: searchOpened}]" @submit.prevent="searchMessages">
		<div class="input-wrapper">
			<input
				ref="searchInputField"
				v-model="searchInput"
				type="search"
				name="search"
				class="input"
				placeholder="Search messages…"
				@blur="closeSearch"
				@keyup.esc="closeSearch"
			/>
		</div>
		<button
			v-if="!onSearchPage"
			class="search"
			type="button"
			aria-label="Search messages in this channel"
			@mousedown.prevent="toggleSearch"
		/>
	</form>
</template>

<style>
form.message-search {
	display: flex;
}

form.message-search .input-wrapper {
	display: flex;
}

form.message-search input {
	width: 100%;
	height: auto !important;
	margin: 7px 0;
	border: 0;
	color: inherit;
	background-color: #fafafa;
	appearance: none;
}

form.message-search input::placeholder {
	color: rgba(0, 0, 0, 0.35);
}

@media (min-width: 480px) {
	form.message-search input {
		min-width: 140px;
	}

	form.message-search input:focus {
		min-width: 220px;
	}
}

form.message-search .input-wrapper {
	position: absolute;
	top: 45px;
	left: 0;
	right: 0;
	z-index: 1;
	height: 0;
	overflow: hidden;
	background: var(--window-bg-color);
}

form.message-search .input-wrapper input {
	margin: 7px;
}

form.message-search.opened .input-wrapper {
	height: 50px;
}

#chat form.message-search button {
	display: flex;
	color: #607992;
}
</style>

<script>
export default {
	name: "MessageSearchForm",
	props: {
		network: Object,
		channel: Object,
	},
	data() {
		return {
			searchOpened: false,
			searchInput: "",
		};
	},
	computed: {
		onSearchPage() {
			return this.$route.name === "SearchResults";
		},
	},
	watch: {
		"$route.query.q"() {
			this.searchInput = this.$route.query.q;
		},
	},
	mounted() {
		this.searchInput = this.$route.query.q;
		this.searchOpened = this.onSearchPage;

		if (!this.searchInput && this.searchOpened) {
			this.$refs.searchInputField.focus();
		}
	},
	methods: {
		closeSearch() {
			if (!this.onSearchPage) {
				this.searchOpened = false;
			}
		},
		toggleSearch() {
			if (this.searchOpened) {
				this.$refs.searchInputField.blur();
				return;
			}

			this.searchOpened = true;
			this.$refs.searchInputField.focus();
		},
		searchMessages(event) {
			event.preventDefault();

			if (!this.searchInput) {
				return;
			}

			this.$router
				.push({
					name: "SearchResults",
					params: {
						id: this.channel.id,
					},
					query: {
						q: this.searchInput,
					},
				})
				.catch((err) => {
					if (err.name === "NavigationDuplicated") {
						// Search for the same query again
						this.$root.$emit("re-search");
					}
				});
		},
	},
};
</script>

<template>
	<form :class="['message-search', {opened: searchOpened}]" @submit.prevent="searchMessages">
		<div class="input-wrapper">
			<input
				ref="searchInputField"
				v-model="searchInput"
				type="search"
				name="search"
				class="input"
				placeholder="Search messages…"
				@blur="closeSearch"
			/>
		</div>
		<button
			v-if="!onSearchPage"
			class="search"
			type="button"
			aria-label="Search messages in this channel"
			@mousedown.prevent="toggleSearch"
		/>
	</form>
</template>

<style>
form.message-search {
	display: flex;
}

form.message-search .input-wrapper {
	display: flex;
}

form.message-search input {
	width: 100%;
	height: auto !important;
	margin: 7px 0;
	border: 0;
	color: inherit;
	background-color: #fafafa;
	appearance: none;
}

form.message-search input::placeholder {
	color: rgba(0, 0, 0, 0.35);
}

@media (min-width: 480px) {
	form.message-search input {
		min-width: 140px;
	}

	form.message-search input:focus {
		min-width: 220px;
	}
}

form.message-search .input-wrapper {
	position: absolute;
	top: 45px;
	left: 0;
	right: 0;
	z-index: 1;
	height: 0;
	overflow: hidden;
	background: var(--window-bg-color);
}

form.message-search .input-wrapper input {
	margin: 7px;
}

form.message-search.opened .input-wrapper {
	height: 50px;
}

#chat form.message-search button {
	display: flex;
	color: #607992;
}
</style>

<script>
export default {
	name: "MessageSearchForm",
	props: {
		network: Object,
		channel: Object,
	},
	data() {
		return {
			searchOpened: false,
			searchInput: "",
		};
	},
	computed: {
		onSearchPage() {
			return this.$route.name === "SearchResults";
		},
	},
	watch: {
		"$route.query.q"() {
			this.searchInput = this.$route.query.q;
		},
	},
	mounted() {
		this.searchInput = this.$route.query.q;
		this.searchOpened = this.onSearchPage;

		if (!this.searchInput && this.searchOpened) {
			this.$refs.searchInputField.focus();
		}
	},
	methods: {
		closeSearch() {
			if (!this.onSearchPage) {
				this.searchOpened = false;
			}
		},
		toggleSearch() {
			if (this.searchOpened) {
				this.$refs.searchInputField.blur();
				return;
			}

			this.searchOpened = true;
			this.$refs.searchInputField.focus();
		},
		searchMessages(event) {
			event.preventDefault();

			if (!this.searchInput) {
				return;
			}

			this.$router
				.push({
					name: "SearchResults",
					params: {
						id: this.channel.id,
					},
					query: {
						q: this.searchInput,
					},
				})
				.catch((err) => {
					if (err.name === "NavigationDuplicated") {
						// Search for the same query again
						this.$root.$emit("re-search");
					}
				});
		},
	},
};
</script>


remove leftover

remove leftover

/*
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

<style>
</style>

<script>
import { Pie } from 'vue-chartjs'
import { groupBy, map, sumBy } from 'lodash'

export default {
  extends: Pie,
  props: ['chartData'],
  watch: {
    chartData (newChartData) {
      this.renderPieChart(newChartData)
    }
  },
  mounted () {
    this.renderPieChart(this.chartData)
  },

  methods: {
    renderPieChart (newChartData) {
      if (Object.keys(newChartData).length) {
        const groupedLabels = groupBy(newChartData, (o) => o.purpose__description)
        const groupedSum = map(groupedLabels, (val, key) => {
          let sum = {}
          sum[key] = sumBy(val, o => o.total_qty)
          return sum
        })
        const chartLabel = groupedSum.map(o => Object.keys(o)[0])
        const chartData = groupedSum.map(o => Object.values(o)[0])
        this.renderChart({
          labels: chartLabel,
          datasets: [
            {
              backgroundColor: '#f87979',
              data: chartData
            }
          ]
        }, {
          legend: {
            display: false
          },
          tooltips: {
            enabled: true
          },
          responsive: true,
          maintainAspectRatio: true
        })
      }
    }
  }
}
</script>

/*
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/

<style>
</style>

<script>
import { Pie } from 'vue-chartjs'
import { groupBy, map, sumBy } from 'lodash'

export default {
  extends: Pie,
  props: ['chartData'],
  watch: {
    chartData (newChartData) {
      this.renderPieChart(newChartData)
    }
  },
  mounted () {
    this.renderPieChart(this.chartData)
  },

  methods: {
    renderPieChart (newChartData) {
      if (Object.keys(newChartData).length) {
        const groupedLabels = groupBy(newChartData, (o) => o.purpose__description)
        const groupedSum = map(groupedLabels, (val, key) => {
          let sum = {}
          sum[key] = sumBy(val, o => o.total_qty)
          return sum
        })
        const chartLabel = groupedSum.map(o => Object.keys(o)[0])
        const chartData = groupedSum.map(o => Object.values(o)[0])
        this.renderChart({
          labels: chartLabel,
          datasets: [
            {
              label: 'GitHub Commits',
              backgroundColor: '#f87979',
              data: chartData
            }
          ]
        }, {
          legend: {
            display: false
          },
          tooltips: {
            enabled: true
          },
          responsive: true,
          maintainAspectRatio: true
        })
      }
    }
  }
}
</script>


Add an option for the blur feature on the interstitial view

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

<template>
	<Modal v-if="modal"
		class="talk-modal"
		size="large"
		@close="closeModal">

				{{ t('spreed', 'Camera and microphone check') }}

				<video v-show="showVideo"
					ref="video"
					class="preview__video"
					disable-picture-in-picture="true"
					tabindex="-1" />

					<VideoBackground
						:display-name="displayName"
						:user="userId" />
					<Avatar v-if="userId"
						:size="128"
						:disable-menu="true"
						:disable-tooltip="true"
						:show-user-status="false"
						:user="userId"
						:display-name="displayName" />

						{{ firstLetterOfGuestName }}

				<button
					class="device-toggle"
					:disabled="!audioPreviewAvailable"
					@click="toggleAudio">

						<Microphone
							v-if="audioOn"
							title=""
							decorative
							:size="20" />
						<MicrophoneOff
							v-else
							title=""
							decorative
							:size="20" />

				</button>
				<VolumeIndicator
					class="indicator"
					:audio-preview-available="audioPreviewAvailable"
					:current-volume="currentVolume"
					:volume-threshold="volumeThreshold"
					:disabled="!audioOn" />

				<button
					class="device-toggle"
					:disabled="!videoPreviewAvailable"
					@click="toggleVideo">

						<Video
							v-if="videoOn"
							title=""
							decorative
							:size="20" />
						<VideoOff
							v-else
							title=""
							decorative
							:size="20" />

				</button>

				<button
					class="device-toggle"
					:disabled="!blurPreviewAvailable"
					@click="toggleBlur">

						<Blur
							v-if="blurOn"
							slot="icon"
							:size="20"
							decorative
							title="" />
						<BlurOff
							v-else
							slot="icon"
							:size="20"
							decorative
							title="" />

				</button>

				<button v-if="!showDeviceSelection"
					class="select-devices"
					@click="showDeviceSelection = true">

						<Cog
							title=""
							decorative
							:size="20" />

					 {{ t('spreed', 'Choose devices') }}
				</button>
				<template v-if="showDeviceSelection">
					<MediaDevicesSelector kind="audioinput"
						:devices="devices"
						:device-id="audioInputId"
						@update:deviceId="audioInputId = $event" />
					<MediaDevicesSelector kind="videoinput"
						:devices="devices"
						:device-id="videoInputId"
						@update:deviceId="videoInputId = $event" />
				</template>

			<CheckboxRadioSwitch :checked.sync="showDeviceChecker"
				class="checkbox">
				{{ t('spreed', 'Always show this dialog before joining a call in this conversation.') }}
			</CheckboxRadioSwitch>

			<CallButton
				class="call-button"
				:force-join-call="true" />

	</Modal>
</template>

<template>
	<Modal v-if="modal"
		class="talk-modal"
		size="large"
		@close="closeModal">

				{{ t('spreed', 'Camera and microphone check') }}

				<video v-show="showVideo"
					ref="video"
					class="preview__video"
					disable-picture-in-picture="true"
					tabindex="-1" />

					<VideoBackground
						:display-name="displayName"
						:user="userId" />
					<Avatar v-if="userId"
						:size="128"
						:disable-menu="true"
						:disable-tooltip="true"
						:show-user-status="false"
						:user="userId"
						:display-name="displayName" />

						{{ firstLetterOfGuestName }}

				<button
					class="device-toggle"
					:disabled="!audioPreviewAvailable"
					@click="toggleAudio">

						<Microphone
							v-if="audioOn"
							title=""
							decorative
							:size="20" />
						<MicrophoneOff
							v-if="!audioOn"
							title=""
							decorative
							:size="20" />

				</button>
				<VolumeIndicator
					class="indicator"
					:audio-preview-available="audioPreviewAvailable"
					:current-volume="currentVolume"
					:volume-threshold="volumeThreshold"
					:disabled="!audioOn" />

				<button
					class="device-toggle"
					:disabled="!videoPreviewAvailable"
					@click="toggleVideo">

						<Video
							v-if="videoOn"
							title=""
							decorative
							:size="20" />
						<VideoOff
							v-if="!videoOn"
							title=""
							decorative
							:size="20" />

				</button>

				<button v-if="!showDeviceSelection"
					class="select-devices"
					@click="showDeviceSelection = true">

						<Cog
							title=""
							decorative
							:size="20" />

					 {{ t('spreed', 'Choose devices') }}
				</button>
				<template v-if="showDeviceSelection">
					<MediaDevicesSelector kind="audioinput"
						:devices="devices"
						:device-id="audioInputId"
						@update:deviceId="audioInputId = $event" />
					<MediaDevicesSelector kind="videoinput"
						:devices="devices"
						:device-id="videoInputId"
						@update:deviceId="videoInputId = $event" />
				</template>

			<CheckboxRadioSwitch :checked.sync="showDeviceChecker"
				class="checkbox">
				{{ t('spreed', 'Always show this dialog before joining a call in this conversation.') }}
			</CheckboxRadioSwitch>

			<CallButton
				class="call-button"
				:force-join-call="true" />

	</Modal>
</template>


fix bug for remove file

fix bug for remove file

<template>
    <meta-upload
        :file-list="model"
        :multiple="false"
        :listType="listType"
        :onSuccess="handleSuccess"
        :onRemove="handleRemove"
        :disabled="disabled"
        v-bind="$attrs"
    >
        <template slot="default">
            <slot name="default"></slot>
        </template>
        <template slot="tip">
            <slot name="tip"></slot>
        </template>

        <template slot="uploadList" slot-scope="props">
            <upload-list
                :files="props.files"
                :remove="props.remove"
                :disabled="disabled"
                :listType="listType"
                :handlePreview="handlePreview"
            ></upload-list>
        </template>
    </meta-upload>
</template>

<script>
import metaUpload from "./meta-upload"
import uploadList from './meta-upload/upload-list';

import _props_value_array_mixin from "./_props_value_array_mixin"
import _props_disabled_mixin from "./_props_disabled_mixin"

function noop(){}
function identity(value){
    return value;
}

// 这个组件要处理和上传组件数据格式的映射

export default{
    inheritAttrs:true,
    components:{
        metaUpload,
        uploadList,
    },
    mixins:[
        _props_value_array_mixin,
        _props_disabled_mixin
    ],
    props:{
        listType:{
            type:String,
            default:'text'   // text,picture,picture-card
        },
        onSuccess:{
            type:Function,
            default:noop,
        },
        onRemove:{
            type:Function,
            default:noop,
        },
        getInfoFromResponse:{
            type:Function,
            default:identity,
        },
        handlePreview:{
            type:Function,
            default:noop,
        },
        getName:{
            type:Function,
            default:identity,
        },
        getUrl:{
            type:Function,
            default:identity,
        },
    },
    computed:{
        model:{
            get(){
                return this.value.map((item)=>{
                    let name = this.getName(item);
                    let url = this.getUrl(item);
                    let target = JSON.parse(JSON.stringify(item));
                    target['name'] = name;
                    target['url'] = url;
                    return target
                });

            },
            set(value){
                this.$emit('input',value);
            }
        }
    },
    methods:{
        handleSuccess(response,file,fileList){
            let [...newArr] = this.value;
            newArr.push(this.getInfoFromResponse(response));
            this.model = newArr;

            this.onSuccess && this.onSuccess(response,file,fileList);
        },
        handleRemove(file){
            let url = file.url;
            let index = this.value.findIndex((item)=>{
                return this.getUrl(item) === url
            });

            let [...newArr] = this.value;
            newArr.splice(index,1);
            this.model = newArr;
        }
    },
}
</script>

<template>
    <meta-upload
        :file-list="model"
        :multiple="false"
        :listType="listType"
        :onSuccess="handleSuccess"
        :onRemove="handleRemove"
        :disabled="disabled"
        v-bind="$attrs"
    >
        <template slot="default">
            <slot name="default"></slot>
        </template>
        <template slot="tip">
            <slot name="tip"></slot>
        </template>

        <template slot="uploadList" slot-scope="props">
            <upload-list
                :files="props.files"
                :remove="props.remove"
                :disabled="disabled"
                :listType="listType"
                :handlePreview="handlePreview"
            ></upload-list>
        </template>
    </meta-upload>
</template>

<script>
import metaUpload from "./meta-upload"
import uploadList from './meta-upload/upload-list';

import _props_value_array_mixin from "./_props_value_array_mixin"
import _props_disabled_mixin from "./_props_disabled_mixin"

function noop(){}
function identity(value){
    return value;
}

// 这个组件要处理和上传组件数据格式的映射

export default{
    inheritAttrs:true,
    components:{
        metaUpload,
        uploadList,
    },
    mixins:[
        _props_value_array_mixin,
        _props_disabled_mixin
    ],
    props:{
        listType:{
            type:String,
            default:'text'   // text,picture,picture-card
        },
        onSuccess:{
            type:Function,
            default:noop,
        },
        onRemove:{
            type:Function,
            default:noop,
        },
        getInfoFromResponse:{
            type:Function,
            default:identity,
        },
        handlePreview:{
            type:Function,
            default:noop,
        },
        getName:{
            type:Function,
            default:identity,
        },
        getUrl:{
            type:Function,
            default:identity,
        },
    },
    computed:{
        model:{
            get(){
                return this.value.map((item)=>{
                    let name = this.getName(item);
                    let url = this.getName(item);
                    let target = JSON.parse(JSON.stringify(item));
                    target['name'] = name;
                    target['url'] = url;
                    return target
                });

            },
            set(value){
                this.$emit('input',value);
            }
        }
    },
    methods:{
        handleSuccess(response,file,fileList){
            let [...newArr] = this.value;
            newArr.push(this.getInfoFromResponse(response));
            this.model = newArr;

            this.onSuccess && this.onSuccess(response,file,fileList);
        },
        handleRemove(file){
            let valuefield = this.valuefield;
            let url = file.url;
            let index = this.value.findIndex((item)=>{
                return item[valuefield] === url
            });

            let [...newArr] = this.value;
            newArr.splice(index,1);
            this.model = newArr;
        }
    },
}
</script>


Eth icon added

Eth icon added

<template>

{{ title }}

      <button
        class="title-button prevent-user-select"
        @click="copyToClipboard($refs.addressInput)"
      >
        {{ $t('common.copy') }}
      </button>

        <input
          ref="addressInput"
          v-model="selectedAddress"
          type="text"
          placeholder="Please enter the address"
          @focus="dropdownOpen = false"
        />

          <blockie :address="selectedAddress" width="30px" height="30px" />

              <blockie :address="addr.address" width="30px" height="30px" />

Ethereum

                {{ addr.address }}

              {{ $t('interface.myAddr') }}

</template>

<template>

{{ title }}

      <button class="title-button prevent-user-select">
        {{ $t('common.copy') }}
      </button>

        <input
          ref="addressInput"
          v-model="selectedAddress"
          type="text"
          placeholder="Please enter the address"
          @focus="dropdownOpen = false"
        />

          <blockie :address="selectedAddress" width="30px" height="30px" />

              <blockie :address="addr.address" width="30px" height="30px" />

Ethereum

                {{ addr.address }}

              {{ $t('interface.myAddr') }}

</template>


added links to podcast pppp

added links to podcast pppp

<template>

        Prvý Pokusný Podcast Progressbaru - PPPP - 🔗🔗🔗 links

        🎥 Youtube Playlist

        🎧 Spotify

        🐬 RSS

        📧 Newsletter

</template>


#322 添加域时,只显示在线的集群

#322 添加域时,只显示在线的集群

<template>
    <el-dialog title="添加新域" width="35%" :visible.sync="isVisible" :before-close="closeDialog" v-loading="loading" element-loading-text="请稍等···">
        <el-form :model="namespaceInfo" :rules="rules" ref="namespaceInfo" label-width="100px">
            <el-form-item label="zk集群" prop="zkClusterKey">
                <el-col :span="20">
                    <el-select size="small" v-model="namespaceInfo.zkClusterKey" style="width: 100%">
                        <el-option v-for="item in onLineClusters" :label="item.zkAlias" :value="item.zkClusterKey" :key="item.zkClusterKey"></el-option>
                    </el-select>
                </el-col>
            </el-form-item>
            <el-form-item label="域名" prop="namespace">
                <el-col :span="20">
                    <el-input v-model="namespaceInfo.namespace"></el-input>
                </el-col>
            </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
            <el-button @click="closeDialog()">取消</el-button>
            <el-button type="primary" @click="handleSubmit()">确定</el-button>
        </div>
    </el-dialog>
</template>

<script>
export default {
  props: ['namespaceInfo', 'zkClusterKeys'],
  data() {
    return {
      isVisible: true,
      loading: false,
      rules: {
        zkClusterKey: [{ required: true, message: '请选择zk集群', trigger: 'change' }],
        namespace: [{ required: true, message: '请输入域名', trigger: 'blur' }],
      },
    };
  },
  methods: {
    handleSubmit() {
      this.$refs.namespaceInfo.validate((valid) => {
        if (valid) {
          this.namespaceInfoRequest('/console/namespaces');
        }
      });
    },
    namespaceInfoRequest(url) {
      this.$http.post(url, this.namespaceInfo).then(() => {
        this.$emit('namespace-info-success');
      })
      .catch(() => { this.$http.buildErrorHandler(`${url}请求失败！`); });
    },
    closeDialog() {
      this.$emit('close-dialog');
    },
  },
  computed: {
    onLineClusters() {
      const onlineArr = [];
      this.zkClusterKeys.forEach((element) => {
        if (!element.offline) {
          onlineArr.push(element);
        }
      });
      return onlineArr;
    },
  },
};
</script>

<template>
    <el-dialog title="添加新域" width="35%" :visible.sync="isVisible" :before-close="closeDialog" v-loading="loading" element-loading-text="请稍等···">
        <el-form :model="namespaceInfo" :rules="rules" ref="namespaceInfo" label-width="100px">
            <el-form-item label="zk集群" prop="zkClusterKey">
                <el-col :span="20">
                    <el-select size="small" v-model="namespaceInfo.zkClusterKey" style="width: 100%">
                        <el-option v-for="item in zkClusterKeys" :label="item.zkAlias" :value="item.zkClusterKey" :key="item.zkClusterKey"></el-option>
                    </el-select>
                </el-col>
            </el-form-item>
            <el-form-item label="域名" prop="namespace">
                <el-col :span="20">
                    <el-input v-model="namespaceInfo.namespace"></el-input>
                </el-col>
            </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
            <el-button @click="closeDialog()">取消</el-button>
            <el-button type="primary" @click="handleSubmit()">确定</el-button>
        </div>
    </el-dialog>
</template>

<script>
export default {
  props: ['namespaceInfo', 'zkClusterKeys'],
  data() {
    return {
      isVisible: true,
      loading: false,
      rules: {
        zkClusterKey: [{ required: true, message: '请选择zk集群', trigger: 'change' }],
        namespace: [{ required: true, message: '请输入域名', trigger: 'blur' }],
      },
    };
  },
  methods: {
    handleSubmit() {
      this.$refs.namespaceInfo.validate((valid) => {
        if (valid) {
          this.namespaceInfoRequest('/console/namespaces');
        }
      });
    },
    namespaceInfoRequest(url) {
      this.$http.post(url, this.namespaceInfo).then(() => {
        this.$emit('namespace-info-success');
      })
      .catch(() => { this.$http.buildErrorHandler(`${url}请求失败！`); });
    },
    closeDialog() {
      this.$emit('close-dialog');
    },
  },
};
</script>


Fixed item card pagination alignment

Fixed item card pagination alignment

<template>
	<v-list-item v-if="pageCount > 1" class="c-ItemCardPagination">
		<!-- Custom pagination because vuetify doesn't provide no-count pagination -->
		<v-btn :disabled="!hasPreviousPage" text icon @click.stop="$emit('prev-page')">
			<v-icon>mdi-chevron-left</v-icon>
		</v-btn>
		<v-btn :disabled="!hasNextPage" text icon @click.stop="$emit('next-page')">
			<v-icon>mdi-chevron-right</v-icon>
		</v-btn>
	</v-list-item>
</template>

<script>
export default {
	name: 'ItemCardPagination',

	props: {
		pageCount: {
			type: Number,
			required: true
		},

		hasPreviousPage: {
			type: Boolean,
			required: true
		},

		hasNextPage: {
			type: Boolean,
			required: true
		}
	}
}
</script>

<style lang="less">
// Align the pagination buttons
.c-ItemCardPagination {
	justify-content: space-around;

	&.v-list-item::after {
		display: none;
	}
}
</style>

<template>
	<v-list-item v-if="pageCount > 1" class="c-ItemCardPagination">
		<!-- Custom pagination because vuetify doesn't provide no-count pagination -->
		<v-btn :disabled="!hasPreviousPage" text icon @click.stop="$emit('prev-page')">
			<v-icon>mdi-chevron-left</v-icon>
		</v-btn>
		<v-btn :disabled="!hasNextPage" text icon @click.stop="$emit('next-page')">
			<v-icon>mdi-chevron-right</v-icon>
		</v-btn>
	</v-list-item>
</template>

<script>
export default {
	name: 'ItemCardPagination',

	props: {
		pageCount: {
			type: Number,
			required: true
		},

		hasPreviousPage: {
			type: Boolean,
			required: true
		},

		hasNextPage: {
			type: Boolean,
			required: true
		}
	}
}
</script>

<style lang="less">
// Align the pagination buttons
.c-ItemCardPagination {
	justify-content: space-around;
}
</style>


Fixes #4666 -- Make station dropdown vertically scroll at certain height.

<template>

                        {{ getStationName(row.station_id.$model) }}

                        <icon icon="remove"></icon>
                        <translate key="lang_btn_remove">Remove</translate>

                            <template #label>
                                <translate key="lang_form_station_permission">Station Permissions</translate>
                            </template>
                            <template #description>
                                <translate key="lang_form_station_permission_desc">Users with this role will have these permissions for this single station.</translate>
                            </template>
                            <template #default="props">

                            </template>

                <template #button-content>
                    <translate key="lang_btn_add_station">Add Station</translate>
                </template>

                    {{ stationName }}

</template>


完成标签导航的滚动到可视区域效果

完成标签导航的滚动到可视区域效果

<template>

      <Dropdown transfer @on-click="handleTagsOption" style="margin-top:7px;">
        <Button size="small" type="text">
          <Icon :size="18" type="ios-close-circle-outline" />
        </Button>
        <DropdownMenu slot="list">
          <DropdownItem name="close-all">关闭所有</DropdownItem>
          <DropdownItem name="close-others">关闭其他</DropdownItem>
        </DropdownMenu>
      </Dropdown>

      <Button type="text" @click="handleScroll(240)">
        <Icon :size="18" type="ios-arrow-back" />
      </Button>

      <Button type="text" @click="handleScroll(-240)">
        <Icon :size="18" type="ios-arrow-forward" />
      </Button>

        <transition-group name="taglist-moving-animation">
          <Tag
            type="dot"
            v-for="item in list"
            ref="tagsPageOpened"
            :key="`tag-nav-${item.name}`"
            :name="item.name"
            @on-close="handleClose"
            @click.native="handleClick(item)"
            :closable="item.name !== 'home'"
            :color="item.name === value.name ? 'primary' : 'default'"
          >{{ showTitleInside(item) }}</Tag>
        </transition-group>

</template>


Using core-base auth message in facility_management

<template>

  <core-base
    :appBarTitle="appBarTitle"
    :immersivePage="isImmersive"
    immersivePageIcon="arrow_back"
    :immersivePageRoute="appBarBackLink"
    :immersivePagePrimary="true"
    :authorized="isAdmin || isSuperuser"
    :authorizationErrorDetails="$tr('adminOrSuperuser')"
  >

    <div class="facility-management">
      <!-- QUESTION should we explicitly define this in every page? -->
      <top-nav v-if="!isEnrollmentPage" />
      <component :is="currentPage" />
    </div>

  </core-base>

</template>

<script>

  import { mapState, mapGetters } from 'vuex';
  import { TopLevelPageNames } from 'kolibri.coreVue.vuex.constants';
  import authMessage from 'kolibri.coreVue.components.authMessage';
  import coreBase from 'kolibri.coreVue.components.coreBase';
  import { PageNames } from '../constants';
  import classEditPage from './class-edit-page';
  import coachClassAssignmentPage from './coach-class-assignment-page';
  import learnerClassEnrollmentPage from './learner-class-enrollment-page';
  import dataPage from './data-page';
  import facilitiesConfigPage from './facilities-config-page';
  import manageClassPage from './manage-class-page';
  import topNav from './top-nav';
  import userPage from './user-page';

  const classEnrollmentPages = [PageNames.CLASS_ENROLL_LEARNER, PageNames.CLASS_ASSIGN_COACH];

  const pageNameComponentMap = {
    [PageNames.CLASS_EDIT_MGMT_PAGE]: classEditPage,
    [PageNames.CLASS_MGMT_PAGE]: manageClassPage,
    [PageNames.CLASS_ENROLL_LEARNER]: learnerClassEnrollmentPage,
    [PageNames.CLASS_ASSIGN_COACH]: coachClassAssignmentPage,
    [PageNames.DATA_EXPORT_PAGE]: dataPage,
    [PageNames.FACILITY_CONFIG_PAGE]: facilitiesConfigPage,
    [PageNames.USER_MGMT_PAGE]: userPage,
  };

  export default {
    $trs: {
      facilityTitle: 'Facility',
      adminOrSuperuser: 'You must be signed in as an admin or superuser to view this page',
      // here because going to use immersive-page
      detailPageReturnPrompt: 'Class details',
    },
    name: 'managementRoot',
    components: {
      authMessage,
      coreBase,
      topNav,
    },
    computed: {
      ...mapGetters(['isAdmin', 'isSuperuser']),
      ...mapState(['pageName']),
      ...mapState({
        isEnrollmentPage: state => classEnrollmentPages.includes(state.pageName),
      }),
      topLevelPageName: () => TopLevelPageNames.MANAGE,
      currentPage() {
        return pageNameComponentMap[this.pageName] || null;
      },
      appBarTitle() {
        if (this.isEnrollmentPage) {
          return this.$tr('detailPageReturnPrompt');
        }
        return this.$tr('facilityTitle');
      },
      appBarBackLink() {
        if (this.isEnrollmentPage) {
          return {
            name: PageNames.CLASS_EDIT_MGMT_PAGE,
          };
        }
        return null;
      },
      isImmersive() {
        return this.isEnrollmentPage;
      },
    },
  };

</script>

<style lang="scss" scoped></style>

<template>

  <core-base
    :appBarTitle="appBarTitle"
    :immersivePage="isImmersive"
    immersivePageIcon="arrow_back"
    :immersivePageRoute="appBarBackLink"
    :immersivePagePrimary="true"
  >

        <top-nav v-if="!isEnrollmentPage" />
        <component :is="currentPage" />

    <auth-message v-else :details="$tr('adminOrSuperuser')" />

  </core-base>

</template>


fix(docs): Fix duplicate keys in events table (#1786)

<template>

{{ tag }}

        view source

 0">

Properties

        <template
          slot="default"
          slot-scope="field">
          {{ field.value }}
        </template>

 0">

Slots

 0">

Events

        <template
          slot="args"
          slot-scope="field">
          <div
            v-for="arg in field.value"
            :key="`event-${field.item.event}-${arg.arg ? arg.arg : 'none'}`">
            {{ arg.arg }}

            asdasd

        </template>

</template>

<template>

{{ tag }}

        view source

 0">

Properties

        <template
          slot="default"
          slot-scope="field">
          {{ field.value }}
        </template>

 0">

Slots

 0">

Events

        <template
          slot="args"
          slot-scope="field">

            {{ arg.arg }}

        </template>

</template>


fix(php): fix attack time

fix(php): fix attack time

<template>

报警详情

          <button type="button" class="close" data-dismiss="modal" aria-label="Close"></button>

              漏洞详情

              请求信息

              资产信息

              修复建议

                报警时间

{{ moment(data.event_time).format('YYYY-MM-DD HH:mm:ss') }}

                报警消息

                [{{ attack_type2name(data.attack_type) }}] {{ data.plugin_message }}

              <attack_params ref="attack_params"></attack_params>

                应用堆栈

{{ data.stack_trace }}

                请求编号

{{ data.request_id }}

                请求 URL

POST {{ data.url }}

                请求来源

                {{ data.attack_source }}
                {{ data.attack_location.location_zh_cn }}

                请求 Referer

{{ data.referer }}

                请求 UA

                {{ data.user_agent }}

                请求 BODY

{{ data.body }}

                主机名称

{{ data.server_hostname }}

                服务器 IP

{{ nic.name }}: {{ nic.ip }}

                应用版本

{{ data.server_type | capitalize }}/{{ data.server_version }}

              暂无

          <button class="btn btn-primary" data-dismiss="modal">关闭</button>

</template>

<template>

报警详情

          <button type="button" class="close" data-dismiss="modal" aria-label="Close"></button>

              漏洞详情

              请求信息

              资产信息

              修复建议

                报警时间

{{ moment(data.event_time).format('YYYY-MM-DD hh:mm:ss') }}

                报警消息

                [{{ attack_type2name(data.attack_type) }}] {{ data.plugin_message }}

              <attack_params ref="attack_params"></attack_params>

                应用堆栈

{{ data.stack_trace }}

                请求编号

{{ data.request_id }}

                请求 URL

POST {{ data.url }}

                请求来源

                {{ data.attack_source }}
                {{ data.attack_location.location_zh_cn }}

                请求 Referer

{{ data.referer }}

                请求 UA

                {{ data.user_agent }}

                请求 BODY

{{ data.body }}

                主机名称

{{ data.server_hostname }}

                服务器 IP

{{ nic.name }}: {{ nic.ip }}

                应用版本

{{ data.server_type | capitalize }}/{{ data.server_version }}

              暂无

          <button class="btn btn-primary" data-dismiss="modal">关闭</button>

</template>


Улучшение интерфейса тренажера японских азбук

<template>

  <transition appear name="fade" mode="out-in">

{{ $t('PICKER_TEXT') }}

          <template v-for="(cells, i) in elements">
            <template v-for="cell in cells">

{{ cell[syllabaryIndex] ? cell[syllabaryIndex] : ' ' }}

{{ cell[answerIndex] ? cell[answerIndex] : ' ' }}

            </template>
          </template>
          <template v-for="i in elements[0].length">

                <input
                  class="cursor-pointer"
                  type="checkbox"
                  :value="i - 1"
                  v-model="checkedColumns"
                  @change="pickElements">

          </template>

          <button class="btn btn-primary mb-2 mr-2" :disabled="this.picked.length < 2" @click="practice">{{ $t('PRACTICE') }}</button>
          <button class="btn btn-default mb-2 mr-2" @click="checkAll">{{ $t('CHECK_ALL') }}</button>
          <button class="btn btn-default mb-2 mr-2" @click="uncheckAll">{{ $t('UNCHECK_ALL') }}</button>
          <transition name="fade-fast" mode="out-in">
            <button class="btn btn-default mb-2" @click="switchSyllabary" :key="syllabaryLabel">{{ syllabaryLabel }}</button>
          </transition>

{{ $t('PRACTICE_TEXT') }}

{{ question }}

{{ answer }}

            <input
              class="form-control text-center"
              autocapitalize="none"
              placeholder="kana"
              :autofocus="focus"
              :value="input"
              @input="checkInput($event.target.value)"
              @keydown.space.prevent="revealAnswer">

<button class="btn btn-default" @click="pick">{{ $t('BACK_TO_PICKER') }}</button>

 0">{{ $t('ANSWERED', { answered }) }}

  </transition>

</template>

<template>

  <transition appear name="fade" mode="out-in">

{{ $t('PICKER_TEXT') }}

          <template v-for="(cells, i) in elements">
            <template v-for="cell in cells">

{{ cell[syllabaryIndex] ? cell[syllabaryIndex] : ' ' }}

{{ cell[answerIndex] ? cell[answerIndex] : ' ' }}

            </template>
          </template>
          <template v-for="i in elements[0].length">

                <input
                  class="cursor-pointer"
                  type="checkbox"
                  :value="i - 1"
                  v-model="checkedColumns"
                  @change="pickElements">

          </template>

          <button class="btn btn-primary" :disabled="this.picked.length < 2" @click="practice">{{ $t('PRACTICE') }}</button>
          <button class="btn btn-default" @click="checkAll">{{ $t('CHECK_ALL') }}</button>
          <button class="btn btn-default" @click="uncheckAll">{{ $t('UNCHECK_ALL') }}</button>
          <transition name="fade-fast" mode="out-in">
            <button class="btn btn-default" @click="switchSyllabary" :key="syllabaryLabel">{{ syllabaryLabel }}</button>
          </transition>

{{ $t('PRACTICE_TEXT') }}

{{ question }}

{{ answer }}

            <input
              class="form-control text-center"
              autocapitalize="none"
              placeholder="kana"
              :autofocus="focus"
              :value="input"
              @input="checkInput($event.target.value)"
              @keydown.space.prevent="revealAnswer">

<button class="btn btn-default" @click="pick">{{ $t('BACK_TO_PICKER') }}</button>

 0">{{ $t('ANSWERED', { answered }) }}

  </transition>

</template>


Move playback action up to td for better accessibility

<template>

        {{ song.title }}
        {{ song.album.artist.name }}
        {{ song.album.name }}
        {{ song.fmtLength }}

</template>


reorg affiliations, add former stud

reorg affiliations, add former stud

<template>

          YOU ARE CURRENTLY OVERRIDING AS ANOTHER USER. Overriding is read-only and
          no actions will be saved.
           Back to MyUW Support tool

        <uw-search />

                {{ netid }}

                Email
                error

                Email

                Search

                Sign Out

        <b-container
          fluid="xl"
          class="px-3 myuw-brand-logo"
          :style="`background-image: url(${staticUrl}images/w-logo-white.png);`"
        >

            <template v-if="$mq != 'desktop'">
              <template v-if="pageTitle == 'Home'"> MyUW </template>
              <template v-else>
                MyUW

                  <template v-if="pageTitle.includes('Preview')"> Preview Quarter </template>
                  <template v-else-if="pageTitle.includes('Textbooks')"> Textbooks </template>
                  <template v-else>
                    {{ pageTitle }}
                  </template>

              </template>
            </template>
            <template v-else> MyUW </template>

    <uw-messages v-if="$mq === 'desktop'" />

                  Home

                  Husky Experience

                  Academics

                  Teaching

                  Accounts

                  Notices

                  Profile

                   Navigation separator

                  Calendar

                  UW Resources

              <uw-welcome v-if="$mq === 'desktop'" />

              <uw-messages />

              {{ pageTitle }}

              <slot v-if="$mq === 'mobile'" name="mobile" />
              <slot v-else name="desktop" />

              Contact

              MyUW Help

             Sign Out

              Terms

              Privacy

          © {{ new Date().getFullYear() }} University of Washington

        Watch a video tour of
        MyUW for Instructors,
        for staff, or
        for students.
Visit the MyUW help guide for more information.

      <template #modal-footer="{ hide }">
         Close
      </template>

</template>


remove footer headlines

remove footer headlines

<template>
  <v-footer height="auto">
    <v-layout row wrap pa-4>
      <v-flex xs4>
        <v-switch id="theme_select" v-model="dark" class="mt-0 mb-2 pt-0" label="Dark theme" hide-details/>
        Disconnect and reset
      </v-flex>

      <v-flex xs4 text-xs-center>

Elasticvue {{version}}

        ©{{ new Date().getFullYear()}} - Carsten König

      </v-flex>

      <v-flex xs4>
        <v-flex text-xs-right>
          Homepage

          Github
        </v-flex>
      </v-flex>
    </v-layout>
  </v-footer>
</template>

<template>
  <v-footer height="auto">
    <v-layout row wrap pa-4>
      <v-flex xs4>

Settings

        <v-switch id="theme_select" v-model="dark" class="my-2" label="Dark theme" hide-details/>
        Disconnect and reset
      </v-flex>

      <v-flex xs4 text-xs-center>

Elasticvue {{version}}

        ©{{ new Date().getFullYear()}} - Carsten König

      </v-flex>

      <v-flex xs4>
        <v-flex text-xs-right>

Links

          Homepage

          Github
        </v-flex>
      </v-flex>
    </v-layout>
  </v-footer>
</template>


Add archived/unarchived snackbar

Add archived/unarchived snackbar

<template>

            <spinner class="spinner" v-if="messages.length == 0"></spinner>

            <message v-for="message in messages" :key="message.device_id" :message-data="message" :thread-color="getColor(message)" :text-color="text_color(message)"></message>

        <sendbar ref="sendbar" :thread-id="threadId" :on-send="sendMessage" :loading="$store.state.media_sending" ></sendbar>

</template>


fix button.vue

fix button.vue

<script>
  export default {
    render: function (c) {
      var buttonEl, tagEl, self = this;

      buttonEl = c('button', {
        staticClass: 'btn',
        class: self.classObject,
        on: {
          click: self.onClick
        }
      }, [self.$slots.default]);

      if (self.tag) {
        tagEl = c(self.tag, { class: this.tagClass ? this.tagClass : '' }, [buttonEl]);
        return tagEl;
      }
      return buttonEl;
    },
    props: {
      tag: String,
      tagClass: String,
      primary: Boolean,
      success: Boolean,
      info: Boolean,
      danger: Boolean,
      warning: Boolean,
      link: Boolean,
      default: Boolean
    },
    computed: {
      classObject () {
        var self = this, co = {};
        ['primary', 'success', 'info', 'danger', 'warning', 'link', 'default'].forEach(function (prop, index) {
          if (self[prop]) co['btn-' + prop] = true;
        });
        return co;
      }
    },
    methods: {
      onClick (event) {
        this.$emit('click', event);
      }
    }
  }
</script>

<script>
  export default {
    render: function (c) {
      var buttonEl, tagEl, self = this;

      buttonEl = c('button', {
        staticClass: 'btn',
        class: self.classObject,
        on: {
          click: self.onClick
        }
      }, [self.$slots.default]);

      if (self.tag) {
        tagEl = c(self.tag, { class: this.tagClass ? this.tagClass : '' }, [buttonEl]);
        return tagEl;
      }
      return buttonEl;
    },
    props: {
      tag: String,
      tagClass: String,
      primary: Boolean,
      success: Boolean,
      info: Boolean,
      danger: Boolean,
      warning: Boolean
    },
    computed: {
      classObject () {
        var self = this, co = {};
        ['primary', 'success', 'info', 'danger', 'warning'].forEach(function (prop, index) {
          if (self[prop]) co['btn-' + prop] = true;
        });
        return co;
      }
    },
    methods: {
      onClick (event) {
        this.$emit('click', event);
      }
    }
  }
</script>


Feat: 获取播放时间和总时间，并格式化处理

Feat: 获取播放时间和总时间，并格式化处理

<template>

    <transition name="normal" @enter="enter" @after-enter="afterEnter" @leave="leave" @after-leave="afterLeave">

{{currentSong.name}}

{{currentSong.singer}}

播放歌词

            {{formatTime(currentTime)}}

            {{formatTime(currentSong.duration)}}

    </transition>
    <transition name="mini">

{{currentSong.name}}

{{currentSong.singer}}

    </transition>
    <audio :src="currentSong.url" ref="audio" @canplay="ready" @error="error" @timeupdate="updateTime"></audio>

</template>

<template>

    <transition name="normal" @enter="enter" @after-enter="afterEnter" @leave="leave" @after-leave="afterLeave">

{{currentSong.name}}

{{currentSong.singer}}

播放歌词

    </transition>
    <transition name="mini">

{{currentSong.name}}

{{currentSong.singer}}

    </transition>
    <audio :src="currentSong.url" ref="audio" @canplay="ready" @error="error"></audio>

</template>


chores

chores

<script>
	import { routes } from './wormhole'

	export default  {
		name: 'portalTarget',
		props: {
			name: { type: String, required: true },
			tag: { type: String, default: 'div' },
			attributes: { type: Object },
		},
		data() {
			return {
				routes,
			}
		},
		beforeDestroy() {
			this.$el.innerHTML = ''
		},

		computed: {
			passengers() {

				return this.routes[this.name] || null
			},
		},

		render(h) {
			const children = this.passengers || []

			if (children.length === 1 && !this.attributes) {
				return children[0]
			}
			else {
				return h(this.tag, {
					class: { 'vue-portal-target': true },
					attrs: this.attributes,
				}, children)
			}
		}
	}
</script>

<script>
	import { routes } from './wormhole'

	export default  {
		name: 'portalTarget',
		props: {
			name: { type: String, required: true },
			tag: { type: String, default: 'div' },
			attributes: { type: Object },
		},
		data() {
			return {
				routes
			}
		},
		beforeDestroy() {
			this.$el.innerHTML = ''
		},

		computed: {
			passengers() {

				return this.routes[this.name] || null
			}
		},

		render(h) {
			const children = this.passengers || []

			if (children.length === 1 && !this.attributes) {
				return children[0]
			}
			else {
				return h(this.tag, {
					class: { 'vue-portal-target': true },
					attrs: this.attributes,
				}, children)
			}
		}
	}
</script>


patch

patch

<template>
    <div :class="[prefixClz, fixed ? `${prefixClz}-fixed` : '']">
        <div :class="[`${prefixClz}-header`]" v-if="_hasSlot('header')">
            <slot name="header"></slot>
        </div>
        <div :class="[`${prefixClz}-body`, bodyClz]">
            <template v-if="fixed">
                <stick :offsetTop="offsetTop" @on-change="_leftFixToggle" :class="[`${prefixClz}-left`]" v-if="_hasSlot('left')">
                    <slot name="left"></slot>
                </stick>
                <div :class="[`${prefixClz}-main`]" v-if="_hasSlot('default')">
                    <slot></slot>
                </div>
                <div :class="[`${prefixClz}-right`]" v-if="_hasSlot('right')">
                    <slot name="right"></slot>
                </div>
            </template>
            <template v-else>
                <div :class="[`${prefixClz}-left`]" v-if="_hasSlot('left')">
                    <slot name="left"></slot>
                </div>
                <div :class="[`${prefixClz}-main`]" v-if="_hasSlot('default')">
                    <slot></slot>
                </div>
                <div :class="[`${prefixClz}-right`]" v-if="_hasSlot('right')">
                    <slot name="right"></slot>
                </div>
            </template>
        </div>
        <div :class="[`${prefixClz}-footer`]" v-if="_hasSlot('footer')">
            <slot name="footer"></slot>
        </div>
    </div>
</template>
<script>
  import { getScroll, getOffset } from '../../../utils'
  import Stick from '../../stick'
  const prefixClz = 'vvue-layout-type-holy-grail'
  export default {
    name: 'HolyGrail',
    components: { Stick },
    props: {
      fixed: {
        type: Boolean,
        default: false
      },
      offsetTop: {
        type: Number,
        default: 0
      },
      bodyClz: {
        type: String,
        default: ''
      }
    },
    data: () => ({
      prefixClz: prefixClz,
      leftFixed: false,
    }),
    methods: {
      _hasSlot(name) {
        return this.$slots[name].length > 0
      },
      _handleScroll() {
        const header = document.querySelector(`.${prefixClz}-header`)
        const footer = document.querySelector(`.${prefixClz}-footer`)
        const leftStick = document.querySelector(`.${prefixClz}-left`)
        const absEle = document.querySelector(`.${prefixClz}-left > div`)
        const leftSlot = document.querySelector(`.${prefixClz}-left > div`).firstChild
        const scrollTop = getScroll(window, true);
        const windowHeight = window.innerHeight

        const headerBelowTop = header.offsetHeight - scrollTop
        const footerBelowBottom = getOffset(footer).top - (windowHeight + scrollTop)

        if (headerBelowTop > 0 && footerBelowBottom > 0) { // only header
          if (this.leftFixed) {
            leftStick.style.height = `calc(100vh - ${this.offsetTop}px)`
          } else {
            leftStick.style.height = `calc(100vh - ${headerBelowTop}px)`
          }
        }
        if (headerBelowTop < 0 && footerBelowBottom > 0) { // neither header nor footer
          leftStick.style.height = `calc(100vh - ${this.offsetTop}px)`
        }
        if (headerBelowTop < 0 && footerBelowBottom < 0) { // only footer
          leftStick.style.height = `calc(100vh - ${this.offsetTop}px + ${footerBelowBottom}px)`
        }
        if (headerBelowTop > 0 && footerBelowBottom < 0) { // both header and footer
          if (this.leftFixed) {
            leftStick.style.height = `calc(100vh + ${footerBelowBottom}px) - ${this.offsetTop}px`
          } else {
            leftStick.style.height = `calc(100vh + ${footerBelowBottom}px) - ${headerBelowTop}px`
          }
        }
        absEle.style.height = leftStick.style.height // absEle 会脱离文档流 所以不能使用100%设置高度
        leftSlot.style.minHeight = '100%'
        leftStick.style.width = leftSlot.offsetWidth + 'px'
      },
      _leftFixToggle(fixed) {
        this.leftFixed = fixed
      },
      toggleLeft(type) {
        const leftStick = document.querySelector(`.${prefixClz}-left`)
        if (type === 'hidden') {
          leftStick.style.position = 'absolute'
          leftStick.style.left = -leftStick.offsetWidth + 'px'
        } else {
          leftStick.style.position = 'inherit'
          leftStick.style.left = 0
        }
      }
    },
    mounted() {
      if (this.fixed) {
        this._handleScroll()
        window.addEventListener('scroll', this._handleScroll, false)
      }
    }
  }
</script>

<template>
    <div :class="[prefixClz, fixed ? `${prefixClz}-fixed` : '']">
        <div :class="[`${prefixClz}-header`]" v-if="_hasSlot('header')">
            <slot name="header"></slot>
        </div>
        <div :class="[`${prefixClz}-body`, bodyClz]">
            <template v-if="fixed">
                <stick :offsetTop="offsetTop" @on-change="_leftFixToggle" :class="[`${prefixClz}-left`]" v-if="_hasSlot('left')">
                    <slot name="left"></slot>
                </stick>
                <div :class="[`${prefixClz}-main`]" v-if="_hasSlot('default')">
                    <slot></slot>
                </div>
                <div :class="[`${prefixClz}-right`]" v-if="_hasSlot('right')">
                    <slot name="right"></slot>
                </div>
            </template>
            <template v-else>
                <div :class="[`${prefixClz}-left`]" v-if="_hasSlot('left')">
                    <slot name="left"></slot>
                </div>
                <div :class="[`${prefixClz}-main`]" v-if="_hasSlot('default')">
                    <slot></slot>
                </div>
                <div :class="[`${prefixClz}-right`]" v-if="_hasSlot('right')">
                    <slot name="right"></slot>
                </div>
            </template>
        </div>
        <div :class="[`${prefixClz}-footer`]" v-if="_hasSlot('footer')">
            <slot name="footer"></slot>
        </div>
    </div>
</template>
<script>
  import { getScroll, getOffset } from '../../../utils'
  import Stick from '../../stick'
  const prefixClz = 'vvue-layout-type-holy-grail'
  export default {
    name: 'HolyGrail',
    components: { Stick },
    props: {
      fixed: {
        type: Boolean,
        default: false
      },
      offsetTop: {
        type: Number,
        default: 0
      },
      bodyClz: {
        type: String,
        default: ''
      }
    },
    data: () => ({
      prefixClz: prefixClz,
      leftFixed: false,
    }),
    methods: {
      _hasSlot(name) {
        return this.$slots[name].length > 0
      },
      _handleScroll() {
        const header = document.querySelector(`.${prefixClz}-header`)
        const footer = document.querySelector(`.${prefixClz}-footer`)
        const leftStick = document.querySelector(`.${prefixClz}-left`)
        const absEle = document.querySelector(`.${prefixClz}-left > div`)
        const leftSlot = document.querySelector(`.${prefixClz}-left > div`).firstChild
        const scrollTop = getScroll(window, true);
        const windowHeight = window.innerHeight

        const headerBelowTop = header.offsetHeight - scrollTop
        const footerBelowBottom = getOffset(footer).top - (windowHeight + scrollTop)

        if (headerBelowTop > 0 && footerBelowBottom > 0) { // only header
          if (this.leftFixed) {
            leftStick.style.height = `calc(100vh - ${this.offsetTop}px)`
          } else {
            leftStick.style.height = `calc(100vh - ${headerBelowTop}px)`
          }
        }
        if (headerBelowTop < 0 && footerBelowBottom > 0) { // neither header nor footer
          leftStick.style.height = `calc(100vh - ${this.offsetTop}px)`
        }
        if (headerBelowTop < 0 && footerBelowBottom < 0) { // only footer
          leftStick.style.height = `calc(100vh - ${this.offsetTop}px + ${footerBelowBottom}px)`
        }
        if (headerBelowTop > 0 && footerBelowBottom < 0) { // both header and footer
          if (this.leftFixed) {
            leftStick.style.height = `calc(100vh + ${footerBelowBottom}px) - ${this.offsetTop}px`
          } else {
            leftStick.style.height = `calc(100vh + ${footerBelowBottom}px) - ${headerBelowTop}px`
          }
        }
        absEle.style.height = leftStick.style.height // absEle 会脱离文档流 所以不能使用100%设置高度
        leftSlot.style.height = '100%'
        leftStick.style.width = leftSlot.offsetWidth + 'px'
      },
      _leftFixToggle(fixed) {
        this.leftFixed = fixed
      },
      toggleLeft(type) {
        const leftStick = document.querySelector(`.${prefixClz}-left`)
        if (type === 'hidden') {
          leftStick.style.position = 'absolute'
          leftStick.style.left = -leftStick.offsetWidth + 'px'
        } else {
          leftStick.style.position = 'inherit'
          leftStick.style.left = 0
        }
      }
    },
    mounted() {
      if (this.fixed) {
        this._handleScroll()
        window.addEventListener('scroll', this._handleScroll, false)
      }
    }
  }
</script>