coach-lesson-resourceMlabel has `lesson` in string

<template>

  <CoreBase
    :immersivePage="true"
    immersivePageIcon="close"
    :immersivePagePrimary='false'
    :immersivePageRoute="exitButtonRoute"
    :appBarTitle="$tr('manageResourcesAction')"
    :authorized="userIsAuthorized"
    authorizedRole="adminOrCoach"
    :pageTitle="pageTitle"
  >

    <KPageContainer>

        {{ $tr('documentTitle', { lessonName: currentLesson.title }) }}

      <KGrid>
        <KGridItem
          sizes="100, 100, 50"
          percentage
        >
          <LessonsSearchBox @searchterm="handleSearchTerm" />
        </KGridItem>

        <KGridItem
          sizes="100, 100, 50"
          percentage
          alignments="left, left, right"
        >

            {{ $tr('totalResourcesSelected', { total: workingResources.length }) }}

        </KGridItem>
      </KGrid>

      <LessonsSearchFilters
        v-if="inSearchMode"
        v-model="filters"
        class="search-filters"
        :searchTerm="searchTerm"
        :searchResults="searchResults"
      />

      <ResourceSelectionBreadcrumbs
        v-if="!inSearchMode"
        :ancestors="ancestors"
        :channelsLink="channelsLink"
        :topicsLink="topicsLink"
      />

      <ContentCardList
        v-if="!isExiting"
        :contentList="filteredContentList"
        :showSelectAll="selectAllIsVisible"
        :viewMoreButtonState="viewMoreButtonState"
        :selectAllChecked="addableContent.length === 0"
        :contentIsChecked="contentIsInLesson"
        :contentHasCheckbox="c => !contentIsDirectoryKind(c)"
        :contentCardMessage="selectionMetadata"
        :contentCardLink="contentLink"
        @changeselectall="toggleTopicInWorkingResources"
        @change_content_card="toggleSelected"
        @moreresults="handleMoreResults"
      />

    </KPageContainer>

    <KBottomAppBar>
      <KRouterLink
        :text="inSearchMode ? $tr('exitSearchButtonLabel') : coachStrings.$tr('finishAction')"
        :primary="true"
        appearance="raised-button"
        :to="exitButtonRoute"
      />
    </KBottomAppBar>

  </CoreBase>

</template>

<template>

  <CoreBase
    :immersivePage="true"
    immersivePageIcon="close"
    :immersivePagePrimary="false"
    :immersivePageRoute="exitButtonRoute"
    :appBarTitle="$tr('documentTitle', { lessonName: currentLesson.title })"
    :authorized="userIsAuthorized"
    authorizedRole="adminOrCoach"
    :pageTitle="pageTitle"
  >

    <KPageContainer>

        {{ $tr('documentTitle', { lessonName: currentLesson.title }) }}

      <KGrid>
        <KGridItem
          sizes="100, 100, 50"
          percentage
        >
          <LessonsSearchBox @searchterm="handleSearchTerm" />
        </KGridItem>

        <KGridItem
          sizes="100, 100, 50"
          percentage
          alignments="left, left, right"
        >

            {{ $tr('totalResourcesSelected', { total: workingResources.length }) }}

        </KGridItem>
      </KGrid>

      <LessonsSearchFilters
        v-if="inSearchMode"
        v-model="filters"
        class="search-filters"
        :searchTerm="searchTerm"
        :searchResults="searchResults"
      />

      <ResourceSelectionBreadcrumbs
        v-if="!inSearchMode"
        :ancestors="ancestors"
        :channelsLink="channelsLink"
        :topicsLink="topicsLink"
      />

      <ContentCardList
        v-if="!isExiting"
        :contentList="filteredContentList"
        :showSelectAll="selectAllIsVisible"
        :viewMoreButtonState="viewMoreButtonState"
        :selectAllChecked="addableContent.length === 0"
        :contentIsChecked="contentIsInLesson"
        :contentHasCheckbox="c => !contentIsDirectoryKind(c)"
        :contentCardMessage="selectionMetadata"
        :contentCardLink="contentLink"
        @changeselectall="toggleTopicInWorkingResources"
        @change_content_card="toggleSelected"
        @moreresults="handleMoreResults"
      />

    </KPageContainer>

    <KBottomAppBar>
      <KRouterLink
        :text="inSearchMode ? $tr('exitSearchButtonLabel') : coachStrings.$tr('finishAction')"
        :primary="true"
        appearance="raised-button"
        :to="exitButtonRoute"
      />
    </KBottomAppBar>

  </CoreBase>

</template>


code style

code style

<template>

Index Module

      <button @click="increment">{{ counter }}</button>

      <nuxt-link to="/about">About</nuxt-link>

Todo Module

      <nuxt-link to="/todos">Todos</nuxt-link>

Nested Modules

      <nuxt-link to="/website">Website</nuxt-link>

</template>


Fix actionsheet bug

Fix actionsheet bug

<template>

        {{ item.title }}
        {{ item.subtitle }}

        {{ item.title }}

          <wd-actionsheet-checkbox class="custome-checkbox-item" @input="getItems" :nowValue="currentCheck" :text="item.title" :index="index">
          </wd-actionsheet-checkbox>
          {{ item.title }}
          {{ item.subtitle }}

</template>


Docs Runthrough: UiProgressCircular

Docs Runthrough: UiProgressCircular

<template>

UiProgressCircular

UiProgressCircular shows a circular progress indicator (a spinner). Currently UiProgressCircular only works in browsers with SVG SMIL animation support (e.g. all modern browsers except IE and Edge).

UiProgressCircular supports five possible colors: primary, accent, multi-color (alternating primary colors), black and white. The size (width and height) and stroke can be customized.

            Examples View Source

Determinate

                <ui-progress-circular
                    :show="loading" type="determinate" color="primary" :value="progress"
                ></ui-progress-circular>

                <ui-progress-circular
                    :show="loading" type="determinate" color="accent" :value="progress"
                ></ui-progress-circular>

                <ui-progress-circular
                    :show="loading" type="determinate" color="black" :value="progress"
                ></ui-progress-circular>

                    <ui-progress-circular
                        :show="loading" type="determinate" color="white" :value="progress"
                    ></ui-progress-circular>

Indeterminate

                <ui-progress-circular :show="loading" color="multi-color"></ui-progress-circular>
                <ui-progress-circular :show="loading" color="primary"></ui-progress-circular>
                <ui-progress-circular :show="loading" color="accent"></ui-progress-circular>
                <ui-progress-circular :show="loading" color="black"></ui-progress-circular>

                    <ui-progress-circular :show="loading" color="white"></ui-progress-circular>

Custom size

                <ui-progress-circular
                    :show="loading" :size="54" type="determinate" :auto-stroke="false"
                    :value="progress"
                ></ui-progress-circular>

                <ui-progress-circular
                    :show="loading" :size="54" :auto-stroke="false"
                ></ui-progress-circular>

Custom stroke width

                <ui-progress-circular
                    :show="loading" :size="48" :stroke="8" type="determinate" :value="progress"
                ></ui-progress-circular>

                <ui-progress-circular :show="loading" :size="48" :stroke="8"></ui-progress-circular>

            <ui-button @click="loading = !loading">Toggle Loading</ui-button>

API

        <ui-tabs raised>

            <ui-tab header="Props">

                            Name
                            Type
                            Default
                            Description

                                show
                                Boolean
                                false

Determines whether or not the circular progress is shown.

Changing this value will show/hide the circular progress.

                                type
                                String
                                "indeterminate"
                                The type of circular progress. One of determinate or indeterminate.

                                color
                                String
                                "primary"

The color of the circular progress. One of primary, accent, multi-color, black or white.

multi-color is only supported on an indeterminate progress, if set on a determinate progress, the color will fall back to primary.

                                size
                                Number
                                24
                                The width and height of the circular progress in pixels (without any units).

                                stroke
                                Number
                                auto or 4

The stroke width of the circular progress in pixels (without any units).

If stroke is not provided, it is automatically calculated unless autoStroke is set to false. See below for details.

                                autoStroke
                                Boolean
                                false

Determines whether or not the stroke width should be automatically calculated if it is not provided.

The calculated stroke is the width divided by 8.

If autoStroke is set to false and stroke is not provided, it defaults to the number 4.

                                value
                                Number
                                0

The value of progress as a number between 0 and 100. Changing this value will update the determinate progress ring.

Only applicable when the type is determinate.

            </ui-tab>

        </ui-tabs>

</template>

<template>

UiProgressCircular

UiProgressCircular shows a circular progress indicator (spinner). Currently UiProgressCircular only works in browsers with SVG SMIL animation support (e.g. all modern browsers except IE and Edge).

UiProgressCircular supports five possible colors: primary, accent, multi-color (alternating primary colors), black and white. The size (width and height) and stroke can also be customized.

Examples

Determinate

                <ui-progress-circular
                    :show="loading" type="determinate" color="primary" :value="progress"
                ></ui-progress-circular>

                <ui-progress-circular
                    :show="loading" type="determinate" color="accent" :value="progress"
                ></ui-progress-circular>

                <ui-progress-circular
                    :show="loading" type="determinate" color="black" :value="progress"
                ></ui-progress-circular>

                    <ui-progress-circular
                        :show="loading" type="determinate" color="white" :value="progress"
                    ></ui-progress-circular>

Indeterminate

                <ui-progress-circular :show="loading" color="multi-color"></ui-progress-circular>

                <ui-progress-circular :show="loading" color="primary"></ui-progress-circular>

                <ui-progress-circular :show="loading" color="accent"></ui-progress-circular>

                <ui-progress-circular :show="loading" color="black"></ui-progress-circular>

                    <ui-progress-circular :show="loading" color="white"></ui-progress-circular>

Custom size

                <ui-progress-circular
                    :show="loading" :size="54" type="determinate" :auto-stroke="false"
                    :value="progress"
                ></ui-progress-circular>

                <ui-progress-circular
                    :show="loading" :size="54" :auto-stroke="false"
                ></ui-progress-circular>

Custom stroke width

                <ui-progress-circular
                    :show="loading" :size="48" :stroke="8" type="determinate" :value="progress"
                ></ui-progress-circular>

                <ui-progress-circular :show="loading" :size="48" :stroke="8"></ui-progress-circular>

            <ui-button @click="loading = !loading">Toggle Loading</ui-button>

API

        <ui-tabs raised>

            <ui-tab header="Props">

                            Name
                            Type
                            Default
                            Description

                                show
                                Boolean
                                false
                                Determines whether or not the circular progress is shown. Changing this value will show/hide the circular progress.

                                type
                                String
                                "indeterminate"
                                The type of progress. One of determinate or indeterminate.

                                color
                                String
                                "primary"

The color of the circular progress. One of primary, accent, multi-color, black or white.

multi-color is only supported on an indeterminate progress, if set on a determinate progress, the color will fall back to primary.

                                size
                                Number
                                24
                                The width and height of the circular progress in pixels (without any units).

                                stroke
                                Number
                                auto or 4
                                The stroke width of the circular progress in pixels (without any units). If stroke is not provided, it is automatically calculated unless autoStroke is set to false. See below for details.

                                autoStroke
                                Boolean
                                false

Determines whether or not the stroke width should be automatically calculated if it is not provided.

The calculated stroke is the width divided by 8.

If autoStroke is set to false and stroke is not provided, it defaults to the number 4.

                                value
                                Number
                                0

The value of progress as a number between 0 and 100. Changing this value will update the determinate progress ring.

Only applicable when the type is determinate.

            </ui-tab>

        </ui-tabs>

</template>


#201 - Fix balance check in wizard

#201 - Fix balance check in wizard

<script lang="ts">
    import {Component, Vue} from "vue-property-decorator";
    import QrcodeVue from "qrcode.vue";
    import {VMoney} from "v-money";

    import Github, {GithubIssue} from "../../classes/Github";
    import {TokenInfo} from "../../classes/token-info";
    import {Contracts} from "../../classes/contracts";
    import {PaymentMethod, PaymentMethods} from "../../classes/payment-method";
    import {Web3x} from "../../classes/Web3x";
    import Utils from "../../classes/Utils";
    import {PendingFundCommand} from "../models/PendingFundCommand";
    import Alert from "../../classes/Alert";

    @Component({
        components: {
            QrcodeVue
        },
        directives: {
            money: VMoney
        }
    })
    export default class WizardComponent extends Vue {
        private _activeStep: number = 1;
        private _loading: boolean = false;
        private _network: string;

        public githubUrl: string = "";
        public githubIssue: GithubIssue = null;
        public supportedTokens: TokenInfo[] = [];
        public selectedToken: TokenInfo = null;
        public panelsHeight: number = 0;
        public stepTitlesHeight: number = 0;
        public trustWalletModalActive: boolean = false;
        public approveInfoModalActive: boolean = false;
        public qrData: string = "";

        public currentAllowance: number = 0;
        public currentFundAmount: number = 0;
        public errorMessages: { fundAmount: string } = {fundAmount: ""};

        public paymentMethod: PaymentMethod = PaymentMethods.getInstance().trustWallet;
        public fundAmount: string = "";
        public description: string = "";

        public moneyConfig = {
            decimal: ".",
            thousands: ",",
            prefix: "",
            suffix: "",
            precision: 2,
            masked: true,
            allowBlank: true
        };

        mounted() {
            this.githubUrl = (this.$refs.requestUrl as HTMLElement).dataset.value;
            if (this.githubUrl) {
                this.updateUrl(this.githubUrl);
            }
            let metaNetwork = document.head.querySelector("[name=\"ethereum:network\"]");
            this._network = metaNetwork ? metaNetwork.getAttribute("content") : "";
            this.updateDappPaymentMethod();
            this.gotoStep(1);
        }

        public getClassesPanel(step: number) {
            if (this._activeStep == step) {
                return "movingIn";
            } else if (this._activeStep > step) {
                return "movingOutBackward";
            } else if (this._activeStep < step) {
                return "movingOutForward";
            } else {
                return "";
            }
        }

        public getClassesStep(step: number) {
            if (this._activeStep == step) {
                return "-active";
            } else if (this._activeStep > step) {
                return "-completed";
            } else {
                return "";
            }
        }

        public async gotoStep(step: number, onlyCompleted: boolean = false) {
            this._loading = true;
            let valid = true;
            if (step > this._activeStep && !onlyCompleted) {
                let $el: HTMLElement = <HTMLElement> this.$refs[`panelStep${this._activeStep}`];
                let formElements: HTMLElement[] = Array.from($el.querySelectorAll("[data-form-validation]"));
                for (let fieldElement of formElements) {
                    let validations: string[] = fieldElement.dataset.formValidation.split(";");
                    valid = await Utils.validateHTMLElement(fieldElement, validations) && valid;
                }
                if (valid && step == 3 && this.paymentMethod == PaymentMethods.getInstance().dapp) {
                    valid = await this._validateFundAmountBalance(formElements.find((el: HTMLInputElement) => el.name == "fundAmount") as HTMLInputElement);
                } else {
                    this.errorMessages.fundAmount = `Please enter a valid number, e.g.: 120.00`;
                }
                this.$forceUpdate();
            }

            if (valid) {
                if (onlyCompleted && step < this._activeStep || !onlyCompleted) {
                    this._activeStep = step;
                    this.panelsHeight = (<HTMLElement>this.$refs[`panelStep${step}`]).clientHeight;
                    this.stepTitlesHeight = (<HTMLElement>this.$refs[`stepTitle${step}`]).clientHeight;
                }
            }
            this._loading = false;
        }

        private async _validateFundAmountBalance(element: HTMLInputElement): Promise<boolean> {
            let valid = true;
            if (this.paymentMethod == PaymentMethods.getInstance().dapp) {
                let balance = await Contracts.getErc20Balance(Web3x.getAccount(), this.selectedToken);
                let fundAmountInWei = Number(this.fundAmountValue * Math.pow(10, this.selectedToken.decimals));
                if (fundAmountInWei > balance) {
                    this.errorMessages.fundAmount = `Your ${this.selectedToken.symbol} balance is to low.`;
                    Utils.setElementInvalid(element);
                    valid = false;
                }
            }
            return valid;
        }

        private async updateDappPaymentMethod() {
            await this.updateDappDisabledMsg();
            if (!PaymentMethods.getInstance().dapp.disabledMsg) {
                this.paymentMethod = PaymentMethods.getInstance().dapp;
            } else {
                this.paymentMethod = PaymentMethods.getInstance().trustWallet;
            }
        }

        private async updateDappDisabledMsg() {
            let web3 = Web3x.getInstance();
            if (web3 && web3.eth && web3.eth.defaultAccount) {
                await new Promise((resolve, reject) => {
                    web3.version.getNetwork((err, res) => {
                        if (!err && res != this._network) {
                            PaymentMethods.getInstance().dapp.disabledMsg = "Not connected to main network.";
                        }
                        resolve("not connected");
                    });
                });
            } else {
                PaymentMethods.getInstance().dapp.disabledMsg = "DApp not available, no accounts available.";
            }
        }

        private async fund() {
            switch (this.paymentMethod) {
                case PaymentMethods.getInstance().dapp:
                    try {
                        if (await this.fundUsingDapp()) {
                            window.location.href = "/user/requests";
                        }
                    } catch (err) {
                        Alert.error(`Something went wrong during funding, please try again. <br/> If the problem remains, <a href="https://help.fundrequest.io">please contact the FundRequest team</a>.`);
                    }
                    break;
                case PaymentMethods.getInstance().trustWallet:
                    this.fundUsingTrustWallet();
                    break;
                default:
                    break;
            }
        }

        private get fundAmountValue(): number {
            return Number(this.fundAmount.replace(/,/g, ''));
        }

        private _handleTransactionError(err): void {
            Utils.hideLoading();
            throw new Error(err);
        }

        private async fundUsingDapp(): Promise<boolean> {
            Utils.showLoading();
            let frContractAddress = Contracts.getInstance().frContractAddress;
            let erc20 = await Contracts.getInstance().getErc20Contract(this.selectedToken.address);
            let decimals = this.selectedToken.decimals;
            this.currentAllowance = (await erc20.allowance(Web3x.getAccount(), frContractAddress)).toNumber();
            this.currentFundAmount = Number(this.fundAmountValue * Math.pow(10, decimals));
            Utils.hideLoading();

            if (!this.approveInfoModalActive && this.currentAllowance < this.currentFundAmount) {
                this.showApproveInfoModal();
                return false;
            }
            else {
                Utils.showLoading();
                if (this.currentAllowance > 0 && this.currentAllowance < this.currentFundAmount) {
                    // setting to 0
                    await erc20.approveTx(frContractAddress, 0).send({}).catch(rej => this._handleTransactionError(rej));
                    this.currentAllowance = 0;
                }
                if (this.currentAllowance === 0) {
                    // You will need to allow the FundRequest contract to access this token
                    await erc20.approveTx(frContractAddress, Utils.biggestNumber()).send({}).catch(rej => this._handleTransactionError(rej));
                }
                let response = await (await Contracts.getInstance().getFrContract()).fundTx(Web3x.getInstance().fromAscii("GITHUB"), this.githubIssue.platformId, this.selectedToken.address, this.currentFundAmount)
                    .send({}).catch(rej => this._handleTransactionError(rej)) as string;
                let pendingFundCommand = new PendingFundCommand();
                pendingFundCommand.transactionId = response;
                pendingFundCommand.amount = this.fundAmountValue.toString();
                pendingFundCommand.description = this.description;
                pendingFundCommand.fromAddress = Web3x.getAccount();
                pendingFundCommand.tokenAddress = this.selectedToken.address;
                pendingFundCommand.platform = this.githubIssue.platform;
                pendingFundCommand.platformId = this.githubIssue.platformId;
                await Utils.postJSON(`/rest/pending-fund`, pendingFundCommand);
                Utils.hideLoading();
                return true;
            }
        }

        public fundUsingTrustWallet() {
            this.showTrustWalletModal();
        }

        public async showTrustWalletModal() {
            this.qrData = (await Utils.postJSON(`/rest/requests/erc67/fund`, {
                platform: this.githubIssue.platform,
                platformId: this.githubIssue.platformId,
                amount: this.totalAmount,
                fundrequestAddress: Contracts.getInstance().frContractAddress,
                tokenAddress: Contracts.getInstance().tokenContractAddress
            })).erc67Link;

            Utils.modal.open(<HTMLElement>this.$refs.trustWalletModal, () => {
                this.hideTrustWalletModal();
            });

            this.trustWalletModalActive = true;
            this._fadeoutPage();
        }

        public showApproveInfoModal() {
            Utils.modal.open(<HTMLElement>this.$refs.approveInfoModal, () => {
                this.hideApproveInfoModal();
            });
            this.approveInfoModalActive = true;
            this._fadeoutPage();
        }

        public hideTrustWalletModal() {
            Utils.modal.close(<HTMLElement>this.$refs.trustWalletModal);
            this.trustWalletModalActive = false;
            this._fadeinPage();
        }

        public hideApproveInfoModal() {
            Utils.modal.close(<HTMLElement>this.$refs.approveInfoModal);
            this.approveInfoModalActive = false;
            this._fadeinPage();
        }

        public get totalAmount() {
            return this.fundAmountValue;
        }

        public get paymentMethods(): PaymentMethods {
            return PaymentMethods.getInstance();
        }

        public async updateUrl(url): Promise<void> {
            this.githubIssue = await Github.getGithubInfo(url);
            this.githubUrl = url;
            this.$forceUpdate();
            await this.updatePossibleTokens(this.githubIssue);
        }

        private _fadeoutPage() {
            (<HTMLElement>this.$refs.panels).style.opacity = "0.5";
            (<HTMLElement>this.$refs.panels).style.pointerEvents = "none";
            (<HTMLElement>this.$refs.faq).style.opacity = "0.5";
            (<HTMLElement>this.$refs.faq).style.pointerEvents = "none";
        }

        private _fadeinPage() {
            (<HTMLElement>this.$refs.panels).style.opacity = "";
            (<HTMLElement>this.$refs.panels).style.pointerEvents = "";
            (<HTMLElement>this.$refs.faq).style.opacity = "";
            (<HTMLElement>this.$refs.faq).style.pointerEvents = "";
        }

        private async updatePossibleTokens(res: GithubIssue): Promise<void> {
            if (res != null) {
                let tokens: TokenInfo[] = await Contracts.getPossibleTokens(res.platformId);
                if (tokens) {
                    this.supportedTokens = tokens;
                    this.selectedToken = this.supportedTokens[0];
                    this.$forceUpdate();
                }
            }
        }
    }
</script>

<script lang="ts">
    import {Component, Vue} from "vue-property-decorator";
    import QrcodeVue from "qrcode.vue";
    import {VMoney} from "v-money";

    import Github, {GithubIssue} from "../../classes/Github";
    import {TokenInfo} from "../../classes/token-info";
    import {Contracts} from "../../classes/contracts";
    import {PaymentMethod, PaymentMethods} from "../../classes/payment-method";
    import {Web3x} from "../../classes/Web3x";
    import Utils from "../../classes/Utils";
    import {PendingFundCommand} from "../models/PendingFundCommand";
    import Alert from "../../classes/Alert";

    @Component({
        components: {
            QrcodeVue
        },
        directives: {
            money: VMoney
        }
    })
    export default class WizardComponent extends Vue {
        private _activeStep: number = 1;
        private _loading: boolean = false;
        private _network: string;

        public githubUrl: string = "";
        public githubIssue: GithubIssue = null;
        public supportedTokens: TokenInfo[] = [];
        public selectedToken: TokenInfo = null;
        public panelsHeight: number = 0;
        public stepTitlesHeight: number = 0;
        public trustWalletModalActive: boolean = false;
        public approveInfoModalActive: boolean = false;
        public qrData: string = "";

        public currentAllowance: number = 0;
        public currentFundAmount: number = 0;
        public errorMessages: { fundAmount: string } = {fundAmount: ""};

        public paymentMethod: PaymentMethod = PaymentMethods.getInstance().trustWallet;
        public fundAmount: string = "";
        public description: string = "";

        public moneyConfig = {
            decimal: ".",
            thousands: ",",
            prefix: "",
            suffix: "",
            precision: 2,
            masked: true,
            allowBlank: true
        };

        mounted() {
            this.githubUrl = (this.$refs.requestUrl as HTMLElement).dataset.value;
            if (this.githubUrl) {
                this.updateUrl(this.githubUrl);
            }
            let metaNetwork = document.head.querySelector("[name=\"ethereum:network\"]");
            this._network = metaNetwork ? metaNetwork.getAttribute("content") : "";
            this.updateDappPaymentMethod();
            this.gotoStep(1);
        }

        public getClassesPanel(step: number) {
            if (this._activeStep == step) {
                return "movingIn";
            } else if (this._activeStep > step) {
                return "movingOutBackward";
            } else if (this._activeStep < step) {
                return "movingOutForward";
            } else {
                return "";
            }
        }

        public getClassesStep(step: number) {
            if (this._activeStep == step) {
                return "-active";
            } else if (this._activeStep > step) {
                return "-completed";
            } else {
                return "";
            }
        }

        public async gotoStep(step: number, onlyCompleted: boolean = false) {
            this._loading = true;
            let valid = true;
            if (step > this._activeStep && !onlyCompleted) {
                let $el: HTMLElement = <HTMLElement> this.$refs[`panelStep${this._activeStep}`];
                let formElements: HTMLElement[] = Array.from($el.querySelectorAll("[data-form-validation]"));
                for (let fieldElement of formElements) {
                    let validations: string[] = fieldElement.dataset.formValidation.split(";");
                    valid = await Utils.validateHTMLElement(fieldElement, validations) && valid;
                }
                if (valid && step == 3 && this.paymentMethod == PaymentMethods.getInstance().dapp) {
                    valid = await this._validateFundAmountBalance(formElements.find((el: HTMLInputElement) => el.name == "fundAmount") as HTMLInputElement);
                } else {
                    this.errorMessages.fundAmount = `Please enter a valid number, e.g.: 120.00`;
                }
                this.$forceUpdate();
            }

            if (valid) {
                if (onlyCompleted && step < this._activeStep || !onlyCompleted) {
                    this._activeStep = step;
                    this.panelsHeight = (<HTMLElement>this.$refs[`panelStep${step}`]).clientHeight;
                    this.stepTitlesHeight = (<HTMLElement>this.$refs[`stepTitle${step}`]).clientHeight;
                }
            }
            this._loading = false;
        }

        private async _validateFundAmountBalance(element: HTMLInputElement): Promise<boolean> {
            let valid = true;
            if (this.paymentMethod == PaymentMethods.getInstance().dapp) {
                let balance = await Contracts.getErc20Balance(Web3x.getAccount(), this.selectedToken);
                let fundAmountInWei = Number(this.fundAmountValue * Math.pow(10, this.selectedToken.decimals));
                if (fundAmountInWei > balance) {
                    this.errorMessages.fundAmount = `Your ${this.selectedToken.symbol} balance is to low.`;
                    Utils.setElementInvalid(element);
                    valid = false;
                }
            }
            return valid;
        }

        private async updateDappPaymentMethod() {
            await this.updateDappDisabledMsg();
            if (!PaymentMethods.getInstance().dapp.disabledMsg) {
                this.paymentMethod = PaymentMethods.getInstance().dapp;
            } else {
                this.paymentMethod = PaymentMethods.getInstance().trustWallet;
            }
        }

        private async updateDappDisabledMsg() {
            let web3 = Web3x.getInstance();
            if (web3 && web3.eth && web3.eth.defaultAccount) {
                await new Promise((resolve, reject) => {
                    web3.version.getNetwork((err, res) => {
                        if (!err && res != this._network) {
                            PaymentMethods.getInstance().dapp.disabledMsg = "Not connected to main network.";
                        }
                        resolve("not connected");
                    });
                });
            } else {
                PaymentMethods.getInstance().dapp.disabledMsg = "DApp not available, no accounts available.";
            }
        }

        private async fund() {
            switch (this.paymentMethod) {
                case PaymentMethods.getInstance().dapp:
                    try {
                        if (await this.fundUsingDapp()) {
                            window.location.href = "/user/requests";
                        }
                    } catch (err) {
                        Alert.error(`Something went wrong during funding, please try again. <br/> If the problem remains, <a href="https://help.fundrequest.io">please contact the FundRequest team</a>.`);
                    }
                    break;
                case PaymentMethods.getInstance().trustWallet:
                    this.fundUsingTrustWallet();
                    break;
                default:
                    break;
            }
        }

        private get fundAmountValue(): number {
            return Number(this.fundAmount.replace(',', ''));
        }

        private _handleTransactionError(err): void {
            Utils.hideLoading();
            throw new Error(err);
        }

        private async fundUsingDapp(): Promise<boolean> {
            Utils.showLoading();
            let frContractAddress = Contracts.getInstance().frContractAddress;
            let erc20 = await Contracts.getInstance().getErc20Contract(this.selectedToken.address);
            let decimals = this.selectedToken.decimals;
            this.currentAllowance = (await erc20.allowance(Web3x.getAccount(), frContractAddress)).toNumber();
            this.currentFundAmount = Number(this.fundAmountValue * Math.pow(10, decimals));
            Utils.hideLoading();

            if (!this.approveInfoModalActive && this.currentAllowance < this.currentFundAmount) {
                this.showApproveInfoModal();
                return false;
            }
            else {
                Utils.showLoading();
                if (this.currentAllowance > 0 && this.currentAllowance < this.currentFundAmount) {
                    // setting to 0
                    await erc20.approveTx(frContractAddress, 0).send({}).catch(rej => this._handleTransactionError(rej));
                    this.currentAllowance = 0;
                }
                if (this.currentAllowance === 0) {
                    // You will need to allow the FundRequest contract to access this token
                    await erc20.approveTx(frContractAddress, Utils.biggestNumber()).send({}).catch(rej => this._handleTransactionError(rej));
                }
                let response = await (await Contracts.getInstance().getFrContract()).fundTx(Web3x.getInstance().fromAscii("GITHUB"), this.githubIssue.platformId, this.selectedToken.address, this.currentFundAmount)
                    .send({}).catch(rej => this._handleTransactionError(rej)) as string;
                let pendingFundCommand = new PendingFundCommand();
                pendingFundCommand.transactionId = response;
                pendingFundCommand.amount = this.fundAmountValue.toString();
                pendingFundCommand.description = this.description;
                pendingFundCommand.fromAddress = Web3x.getAccount();
                pendingFundCommand.tokenAddress = this.selectedToken.address;
                pendingFundCommand.platform = this.githubIssue.platform;
                pendingFundCommand.platformId = this.githubIssue.platformId;
                await Utils.postJSON(`/rest/pending-fund`, pendingFundCommand);
                Utils.hideLoading();
                return true;
            }
        }

        public fundUsingTrustWallet() {
            this.showTrustWalletModal();
        }

        public async showTrustWalletModal() {
            this.qrData = (await Utils.postJSON(`/rest/requests/erc67/fund`, {
                platform: this.githubIssue.platform,
                platformId: this.githubIssue.platformId,
                amount: this.totalAmount,
                fundrequestAddress: Contracts.getInstance().frContractAddress,
                tokenAddress: Contracts.getInstance().tokenContractAddress
            })).erc67Link;

            Utils.modal.open(<HTMLElement>this.$refs.trustWalletModal, () => {
                this.hideTrustWalletModal();
            });

            this.trustWalletModalActive = true;
            this._fadeoutPage();
        }

        public showApproveInfoModal() {
            Utils.modal.open(<HTMLElement>this.$refs.approveInfoModal, () => {
                this.hideApproveInfoModal();
            });
            this.approveInfoModalActive = true;
            this._fadeoutPage();
        }

        public hideTrustWalletModal() {
            Utils.modal.close(<HTMLElement>this.$refs.trustWalletModal);
            this.trustWalletModalActive = false;
            this._fadeinPage();
        }

        public hideApproveInfoModal() {
            Utils.modal.close(<HTMLElement>this.$refs.approveInfoModal);
            this.approveInfoModalActive = false;
            this._fadeinPage();
        }

        public get totalAmount() {
            return this.fundAmountValue;
        }

        public get paymentMethods(): PaymentMethods {
            return PaymentMethods.getInstance();
        }

        public async updateUrl(url): Promise<void> {
            this.githubIssue = await Github.getGithubInfo(url);
            this.githubUrl = url;
            this.$forceUpdate();
            await this.updatePossibleTokens(this.githubIssue);
        }

        private _fadeoutPage() {
            (<HTMLElement>this.$refs.panels).style.opacity = "0.5";
            (<HTMLElement>this.$refs.panels).style.pointerEvents = "none";
            (<HTMLElement>this.$refs.faq).style.opacity = "0.5";
            (<HTMLElement>this.$refs.faq).style.pointerEvents = "none";
        }

        private _fadeinPage() {
            (<HTMLElement>this.$refs.panels).style.opacity = "";
            (<HTMLElement>this.$refs.panels).style.pointerEvents = "";
            (<HTMLElement>this.$refs.faq).style.opacity = "";
            (<HTMLElement>this.$refs.faq).style.pointerEvents = "";
        }

        private async updatePossibleTokens(res: GithubIssue): Promise<void> {
            if (res != null) {
                let tokens: TokenInfo[] = await Contracts.getPossibleTokens(res.platformId);
                if (tokens) {
                    this.supportedTokens = tokens;
                    this.selectedToken = this.supportedTokens[0];
                    this.$forceUpdate();
                }
            }
        }
    }
</script>


docs(search): pass search query to searchbar

<template>
  <!-- eslint-disable vue/attribute-hyphenation  -->
  <v-menu
    v-model="menuModel"
    max-height="75vh"
    offset-y
    readonly
    @input="resetSearch"
  >
    <template #activator="{ attrs }">
      <v-text-field
        ref="searchInput"
        v-model="searchString"
        v-bind="attrs"
        :background-color="(!theme.isDark && !isFocused) ? 'grey lighten-3' : undefined"
        :class="isSearching ? 'rounded-b-0' : ' rounded-lg'"
        :flat="!isFocused && !isSearching"
        :placeholder="placeholder"
        autocomplete="off"
        class="mx-2 mx-md-4"
        dense
        hide-details
        solo
        style="max-width: 450px;"
        @focus="onFocus"
      >
        <template #prepend-inner>
          <v-icon
            :color="!isFocused ? 'grey' : undefined"
            class="ml-1 mr-2"
          >
            $mdiMagnify
          </v-icon>
        </template>
      </v-text-field>
    </template>

    <v-card>
      <ais-instant-search
        :search-client="searchClient"
        :search-function="searchFunction"
        index-name="vuetifyjs"
      >
        <ais-configure
          :facetFilters="[`language:${locale}`]"
          :query="searchString"
        />

        <ais-hits v-slot="{ items }">
          <search-results :groups="transformItems(items)" />
        </ais-hits>
      </ais-instant-search>

      <ais-powered-by />
    </v-card>
  </v-menu>
</template>

<script>
  // Utilities
  import { get } from 'vuex-pathify'
  import { groupItems, sortItems } from 'vuetify/lib/util/helpers'
  import algoliasearch from 'algoliasearch'
  import SearchResults from './SearchResults'

  // Globals
  import { IN_BROWSER } from '@/util/globals'

  // This behavior should be easier to do with solo fields
  // TODO: Review this for v3
  export default {
    name: 'DefaultSearch',

    components: { SearchResults },

    inject: ['theme'],

    data: () => ({
      isFocused: false,
      menuModel: false,
      searchClient: algoliasearch(
        'BH4D9OD16A', // docsearch app ID
        '259d4615e283a1bbaa3313b4eff7881c' // vuetify API key
      ),
      searchString: '',
    }),

    computed: {
      locale: get('route/params@locale'),
      search: get('route/query@search'),
      isSearching () {
        return this.searchString && this.searchString.length > 0
      },
      placeholder () {
        let placeholder = this.$t('search.placeholder')

        if (!this.$vuetify.breakpoint.mobile) {
          placeholder += ' ' + this.$t('search.key-hint')
        }

        return placeholder
      },
    },

    watch: {
      isSearching (val) {
        val
          ? this.menuModel = true
          : this.resetSearch()
      },
    },

    mounted () {
      if (!IN_BROWSER) return
      if (this.search) {
        this.$nextTick(() => {
          this.searchString = this.search
          this.$refs.searchInput.focus()
        })
      }

      document.addEventListener('keydown', this.onDocumentKeydown)
    },

    beforeDestroy () {
      if (IN_BROWSER) return

      document.removeEventListener('keydown', this.onDocumentKeydown)
    },

    methods: {
      async onFocus () {
        clearTimeout(this.timeout)

        this.isFocused = true
      },
      searchFunction (helper) {
        helper.state.query && helper.search()
      },
      transformItems (items) {
        const sorted = sortItems([...items], ['hierarchy.lvl0', 'hierarchy.lvl1'], [false, false], this.locale)
          .map(item => {
            const url = new URL(item.url)

            return {
              ...item,
              url: url.href.split(url.origin).pop(),
            }
          })
        const groups = groupItems(sorted, ['hierarchy.lvl0'])

        groups.forEach(group => {
          group.items = groupItems(group.items, ['hierarchy.lvl1'])
        })

        return groups
      },
      resetSearch () {
        clearTimeout(this.timeout)

        this.$nextTick(() => {
          this.searchString = ''
          this.timeout = setTimeout(() => this.isFocused = false)
          this.menuModel = false
        })
      },
      onDocumentKeydown (e) {
        if (
          e.key === '/' &&
          e.target !== this.$refs.searchInput.$refs.input
        ) {
          e.preventDefault()

          this.$refs.searchInput.focus()
        }
      },
    },
  }
</script>

<style lang="sass" scoped>
  .v-menu__content
    width: 0

    &, & > *
      display: flex
      flex-direction: column
      overflow: hidden

  .ais-InstantSearch
    flex: 1
    min-height: 0
    overflow-y: auto

  .ais-PoweredBy
    display: flex
    justify-content: flex-end
    padding: 8px 8px 0
</style>

<template>
  <!-- eslint-disable vue/attribute-hyphenation  -->
  <v-menu
    v-model="menuModel"
    max-height="75vh"
    offset-y
    readonly
    @input="resetSearch"
  >
    <template #activator="{ attrs }">
      <v-text-field
        ref="searchInput"
        v-model="searchString"
        v-bind="attrs"
        :background-color="(!theme.isDark && !isFocused) ? 'grey lighten-3' : undefined"
        :class="isSearching ? 'rounded-b-0' : ' rounded-lg'"
        :flat="!isFocused && !isSearching"
        :placeholder="placeholder"
        autocomplete="off"
        class="mx-2 mx-md-4"
        dense
        hide-details
        solo
        style="max-width: 450px;"
        @focus="onFocus"
      >
        <template #prepend-inner>
          <v-icon
            :color="!isFocused ? 'grey' : undefined"
            class="ml-1 mr-2"
          >
            $mdiMagnify
          </v-icon>
        </template>
      </v-text-field>
    </template>

    <v-card>
      <ais-instant-search
        :search-client="searchClient"
        :search-function="searchFunction"
        index-name="vuetifyjs"
      >
        <ais-configure
          :facetFilters="[`language:${locale}`]"
          :query="searchString"
        />

        <ais-hits v-slot="{ items }">
          <search-results :groups="transformItems(items)" />
        </ais-hits>
      </ais-instant-search>

      <ais-powered-by />
    </v-card>
  </v-menu>
</template>

<script>
  // Utilities
  import { get } from 'vuex-pathify'
  import { groupItems, sortItems } from 'vuetify/lib/util/helpers'
  import algoliasearch from 'algoliasearch'
  import SearchResults from './SearchResults'

  // Globals
  import { IN_BROWSER } from '@/util/globals'

  // This behavior should be easier to do with solo fields
  // TODO: Review this for v3
  export default {
    name: 'DefaultSearch',

    components: { SearchResults },

    inject: ['theme'],

    data: () => ({
      isFocused: false,
      menuModel: false,
      searchClient: algoliasearch(
        'BH4D9OD16A', // docsearch app ID
        '259d4615e283a1bbaa3313b4eff7881c' // vuetify API key
      ),
      searchString: '',
    }),

    computed: {
      locale: get('route/params@locale'),
      search: get('route/query@search'),
      isSearching () {
        return this.searchString && this.searchString.length > 0
      },
      placeholder () {
        let placeholder = this.$t('search.placeholder')

        if (!this.$vuetify.breakpoint.mobile) {
          placeholder += ' ' + this.$t('search.key-hint')
        }

        return placeholder
      },
    },

    watch: {
      isSearching (val) {
        val
          ? this.menuModel = true
          : this.resetSearch()
      },
    },

    mounted () {
      if (!IN_BROWSER) return

      document.addEventListener('keydown', this.onDocumentKeydown)
    },

    beforeDestroy () {
      if (IN_BROWSER) return

      document.removeEventListener('keydown', this.onDocumentKeydown)
    },

    methods: {
      async onFocus () {
        clearTimeout(this.timeout)

        this.isFocused = true
      },
      searchFunction (helper) {
        helper.state.query && helper.search()
      },
      transformItems (items) {
        const sorted = sortItems([...items], ['hierarchy.lvl0', 'hierarchy.lvl1'], [false, false], this.locale)
          .map(item => {
            const url = new URL(item.url)

            return {
              ...item,
              url: url.href.split(url.origin).pop(),
            }
          })
        const groups = groupItems(sorted, ['hierarchy.lvl0'])

        groups.forEach(group => {
          group.items = groupItems(group.items, ['hierarchy.lvl1'])
        })

        return groups
      },
      resetSearch () {
        clearTimeout(this.timeout)

        this.$nextTick(() => {
          this.searchString = ''
          this.timeout = setTimeout(() => this.isFocused = false)
          this.menuModel = false
        })
      },
      onDocumentKeydown (e) {
        if (
          e.key === '/' &&
          e.target !== this.$refs.searchInput.$refs.input
        ) {
          e.preventDefault()

          this.$refs.searchInput.focus()
        }
      },
    },
  }
</script>

<style lang="sass" scoped>
  .v-menu__content
    width: 0

    &, & > *
      display: flex
      flex-direction: column
      overflow: hidden

  .ais-InstantSearch
    flex: 1
    min-height: 0
    overflow-y: auto

  .ais-PoweredBy
    display: flex
    justify-content: flex-end
    padding: 8px 8px 0
</style>


优化菜单,自动触发菜单事件

优化菜单,自动触发菜单事件


Prevent startDrag event when swipeable is false

<template>
  <div
    @touchstart="startDrag"
    @mousedown="startDrag"
    @touchmove="onDrag"
    @mousemove="onDrag"
    @mouseleave="endDrag"
    @touchend="endDrag"
    class="mint-tab-container">
    <div
      ref="wrap"
      class="mint-tab-container-wrap">
      <slot></slot>
    </div>
  </div>
</template>

<style lang="css">
  @component-namespace mint {
    @component tab-container {
      overflow: hidden;
      position: relative;

      @descendent wrap {
        display: flex;
      }

      .swipe-transition {
        transition: transform 150ms ease-in-out;
      }
    }
  }
</style>

<script>
import { once } from 'mint-ui/src/utils/dom';
import arrayFindIndex from 'array-find-index';

/**
 * mt-tab-container
 * @desc 面板，搭配 tab-container-item 使用
 * @module components/tab-container
 *
 * @param {number|string} [value] - 当前激活的 tabId
 *
 * @example
 * <mt-tab-container v-model="selected">
 *   <mt-tab-container-item id="1"> 内容A </mt-tab-container-item>
 *   <mt-tab-container-item id="2"> 内容B </mt-tab-container-item>
 *   <mt-tab-container-item id="3"> 内容C </mt-tab-container-item>
 * </mt-tab-container>
 */
export default {
  name: 'mt-tab-container',

  props: {
    value: {},
    swipeable: Boolean
  },

  data() {
    return {
      start: { x: 0, y: 0 },
      swiping: false,
      activeItems: [],
      pageWidth: 0,
      currentActive: this.value
    };
  },

  watch: {
    value(val) {
      this.currentActive = val;
    },

    currentActive(val, oldValue) {
      this.$emit('input', val);
      if (!this.swipeable) return;
      const lastIndex = arrayFindIndex(this.$children,
        item => item.id === oldValue);
      this.swipeLeaveTransition(lastIndex);
    }
  },

  mounted() {
    if (!this.swipeable) return;

    this.wrap = this.$refs.wrap;
    this.pageWidth = this.wrap.clientWidth;
    this.limitWidth = this.pageWidth / 4;
  },

  methods: {
    swipeLeaveTransition(lastIndex = 0) {
      if (typeof this.index !== 'number') {
        this.index = arrayFindIndex(this.$children,
          item => item.id === this.currentActive);
        this.swipeMove(-lastIndex * this.pageWidth);
      }

      setTimeout(() => {
        this.wrap.classList.add('swipe-transition');
        this.swipeMove(-this.index * this.pageWidth);

        once(this.wrap, 'webkitTransitionEnd', _ => {
          this.wrap.classList.remove('swipe-transition');
          this.wrap.style.webkitTransform = '';
          this.swiping = false;
          this.index = null;
        });
      }, 0);
    },

    swipeMove(offset) {
      this.wrap.style.webkitTransform = `translate3d(${offset}px, 0, 0)`;
      this.swiping = true;
    },

    startDrag(evt) {
      if (!this.swipeable) return;
      evt = evt.changedTouches ? evt.changedTouches[0] : evt;
      this.dragging = true;
      this.start.x = evt.pageX;
      this.start.y = evt.pageY;
    },

    onDrag(evt) {
      if (!this.dragging) return;
      let swiping;
      const e = evt.changedTouches ? evt.changedTouches[0] : evt;
      const offsetTop = e.pageY - this.start.y;
      const offsetLeft = e.pageX - this.start.x;
      const y = Math.abs(offsetTop);
      const x = Math.abs(offsetLeft);

      swiping = !(x < 5 || (x >= 5 && y >= x * 1.73));
      if (!swiping) return;
      evt.preventDefault();

      const len = this.$children.length - 1;
      const index = arrayFindIndex(this.$children,
        item => item.id === this.currentActive);
      const currentPageOffset = index * this.pageWidth;
      const offset = offsetLeft - currentPageOffset;
      const absOffset = Math.abs(offset);

      if (absOffset > len * this.pageWidth ||
          (offset > 0 && offset < this.pageWidth)) {
        this.swiping = false;
        return;
      }

      this.offsetLeft = offsetLeft;
      this.index = index;
      this.swipeMove(offset);
    },

    endDrag() {
      if (!this.swiping) return;

      const direction = this.offsetLeft > 0 ? -1 : 1;
      const isChange = Math.abs(this.offsetLeft) > this.limitWidth;

      if (isChange) {
        this.index += direction;
        const child = this.$children[this.index];
        if (child) {
          this.currentActive = child.id;
          return;
        }
      }

      this.swipeLeaveTransition();
    }
  }
};
</script>

<template>
  <div
    @touchstart="startDrag"
    @mousedown="startDrag"
    @touchmove="onDrag"
    @mousemove="onDrag"
    @mouseleave="endDrag"
    @touchend="endDrag"
    class="mint-tab-container">
    <div
      ref="wrap"
      class="mint-tab-container-wrap">
      <slot></slot>
    </div>
  </div>
</template>

<style lang="css">
  @component-namespace mint {
    @component tab-container {
      overflow: hidden;
      position: relative;

      @descendent wrap {
        display: flex;
      }

      .swipe-transition {
        transition: transform 150ms ease-in-out;
      }
    }
  }
</style>

<script>
import { once } from 'mint-ui/src/utils/dom';
import arrayFindIndex from 'array-find-index';

/**
 * mt-tab-container
 * @desc 面板，搭配 tab-container-item 使用
 * @module components/tab-container
 *
 * @param {number|string} [value] - 当前激活的 tabId
 *
 * @example
 * <mt-tab-container v-model="selected">
 *   <mt-tab-container-item id="1"> 内容A </mt-tab-container-item>
 *   <mt-tab-container-item id="2"> 内容B </mt-tab-container-item>
 *   <mt-tab-container-item id="3"> 内容C </mt-tab-container-item>
 * </mt-tab-container>
 */
export default {
  name: 'mt-tab-container',

  props: {
    value: {},
    swipeable: Boolean
  },

  data() {
    return {
      start: { x: 0, y: 0 },
      swiping: false,
      activeItems: [],
      pageWidth: 0,
      currentActive: this.value
    };
  },

  watch: {
    value(val) {
      this.currentActive = val;
    },

    currentActive(val, oldValue) {
      this.$emit('input', val);
      if (!this.swipeable) return;
      const lastIndex = arrayFindIndex(this.$children,
        item => item.id === oldValue);
      this.swipeLeaveTransition(lastIndex);
    }
  },

  mounted() {
    if (!this.swipeable) return;

    this.wrap = this.$refs.wrap;
    this.pageWidth = this.wrap.clientWidth;
    this.limitWidth = this.pageWidth / 4;
  },

  methods: {
    swipeLeaveTransition(lastIndex = 0) {
      if (typeof this.index !== 'number') {
        this.index = arrayFindIndex(this.$children,
          item => item.id === this.currentActive);
        this.swipeMove(-lastIndex * this.pageWidth);
      }

      setTimeout(() => {
        this.wrap.classList.add('swipe-transition');
        this.swipeMove(-this.index * this.pageWidth);

        once(this.wrap, 'webkitTransitionEnd', _ => {
          this.wrap.classList.remove('swipe-transition');
          this.wrap.style.webkitTransform = '';
          this.swiping = false;
          this.index = null;
        });
      }, 0);
    },

    swipeMove(offset) {
      this.wrap.style.webkitTransform = `translate3d(${offset}px, 0, 0)`;
      this.swiping = true;
    },

    startDrag(evt) {
      evt = evt.changedTouches ? evt.changedTouches[0] : evt;
      this.dragging = true;
      this.start.x = evt.pageX;
      this.start.y = evt.pageY;
    },

    onDrag(evt) {
      if (!this.dragging) return;
      let swiping;
      const e = evt.changedTouches ? evt.changedTouches[0] : evt;
      const offsetTop = e.pageY - this.start.y;
      const offsetLeft = e.pageX - this.start.x;
      const y = Math.abs(offsetTop);
      const x = Math.abs(offsetLeft);

      swiping = !(x < 5 || (x >= 5 && y >= x * 1.73));
      if (!swiping) return;
      evt.preventDefault();

      const len = this.$children.length - 1;
      const index = arrayFindIndex(this.$children,
        item => item.id === this.currentActive);
      const currentPageOffset = index * this.pageWidth;
      const offset = offsetLeft - currentPageOffset;
      const absOffset = Math.abs(offset);

      if (absOffset > len * this.pageWidth ||
          (offset > 0 && offset < this.pageWidth)) {
        this.swiping = false;
        return;
      }

      this.offsetLeft = offsetLeft;
      this.index = index;
      this.swipeMove(offset);
    },

    endDrag() {
      if (!this.swiping) return;

      const direction = this.offsetLeft > 0 ? -1 : 1;
      const isChange = Math.abs(this.offsetLeft) > this.limitWidth;

      if (isChange) {
        this.index += direction;
        const child = this.$children[this.index];
        if (child) {
          this.currentActive = child.id;
          return;
        }
      }

      this.swipeLeaveTransition();
    }
  }
};
</script>


Use eCamp icon

Use eCamp icon

<template>
  <v-toolbar-items>
    <v-btn text class="px-2"
           min-width="0" rounded
           exact :to="{ name: 'home'}">
      <v-toolbar-title>
        <v-icon>$vuetify.icons.ecamp</v-icon>eCamp
      </v-toolbar-title>
    </v-btn>
    <slot />
  </v-toolbar-items>
</template>

<template>
  <v-toolbar-items>
    <v-btn text class="px-2"
           min-width="0" rounded
           exact :to="{ name: 'home'}">
      <v-toolbar-title>
        🏕eCamp
      </v-toolbar-title>
    </v-btn>
    <slot />
  </v-toolbar-items>
</template>


fix detail bug

fix detail bug

<template>

  <orbitimages :currentid="currentid"></orbitimages>

    <template v-for="item in food_list | filterBy searchText in 'food_name'">

{{current_food.food_name}}

{{current_food.food_name}}

 {{current_food.food_price}}

剩余{{current_food.food_stock}}份

            描述：{{current_food.food_description}}

加入购物车

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

                {{comment.customer}}

{{comment.content}}

{{item.food_name}}

 {{item.food_price}}

加入购物车

    </template>

去下单

成功加入购物车

继续挑选食物

完成啦~去下单

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

    <form class="reveal" id="addorder" method="post" action="./" data-reveal>

        <template v-for="information in delivery_information_list">

                <input type="radio" name="delivery_info" :value="information.id" v-if="$index == 0" checked="checked"></input>
                <input type="radio" name="delivery_info" :value="information.id" v-else></input>

收货人：{{information.receiver}}

收货地址：{{information.address}}

收货人联系电话：{{information.phone}}

        </template>

          收货人：
            <input type="text" id="add_receiver" name="receiver" required="required" placeholder="Enter the receiver"></input>

          收货地址：
            <input type="text" id="add_address" name="address" required="required" placeholder="Enter the address"></input>

          收货人联系电话：
            <input type="tel" id="add_phone" name="phone" required="required" placeholder="Enter the phone"></input>

确认添加

            食物名称
            数量/份
            单价/元
            总价/元

          <template v-for="food in shopping_cart">

              {{food.food_name}}
              {{food.count}}
               {{food.price}}
              {{food.count}}x{{food.price}}={{food.count*food.price}}

          </template>

            合计:
             {{total}}

        <input class="button expanded" type="submit" v-on:click="postOrder($event)" value="提交订单" data-close></input>

      <button class="close-button" data-close aria-label="Close modal" type="button" v-on:click="emptyInput()">
        ×
      </button>
    </form>

</template>

<template>

  <orbitimages :currentid="currentid"></orbitimages>

    <template v-for="item in food_list | filterBy searchText in 'food_name'">

{{current_food.food_name}}

{{current_food.food_name}}

 {{current_food.food_price}}

剩余{{current_food.food_stock}}份

            描述：{{current_food.food_description}}

加入购物车

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

                {{comment.customer}}

                {{comment.created_time}}

{{comment.content}}

{{item.food_name}}

 {{item.food_price}}

加入购物车

    </template>

去下单

成功加入购物车

继续挑选食物

完成啦~去下单

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

    <form class="reveal" id="addorder" method="post" action="./" data-reveal>

        <template v-for="information in delivery_information_list">

                <input type="radio" name="delivery_info" :value="information.id" v-if="$index == 0" checked="checked"></input>
                <input type="radio" name="delivery_info" :value="information.id" v-else></input>

收货人：{{information.receiver}}

收货地址：{{information.address}}

收货人联系电话：{{information.phone}}

        </template>

          收货人：
            <input type="text" id="add_receiver" name="receiver" required="required" placeholder="Enter the receiver"></input>

          收货地址：
            <input type="text" id="add_address" name="address" required="required" placeholder="Enter the address"></input>

          收货人联系电话：
            <input type="tel" id="add_phone" name="phone" required="required" placeholder="Enter the phone"></input>

确认添加

            食物名称
            数量/份
            单价/元
            总价/元

          <template v-for="food in shopping_cart">

              {{food.food_name}}
              {{food.count}}
               {{food.price}}
              {{food.count}}x{{food.price}}={{food.count*food.price}}

          </template>

            合计:
             {{total}}

        <input class="button expanded" type="submit" v-on:click="postOrder($event)" value="提交订单" data-close></input>

      <button class="close-button" data-close aria-label="Close modal" type="button" v-on:click="emptyInput()">
        ×
      </button>
    </form>

</template>


[#218] remove unused variable

[#218] remove unused variable

<template>

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

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

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

      <el-row>

          <el-col v-if="answer.type=='question' || answer.type=='email'" :sm="24">
            <el-input v-if="answer.type=='question'" autosize v-model="userQuestion" placeholder="ENTER YOUR QUESTION" autoComplete="off"></el-input>
            <el-input v-if="answer.type=='email'" autosize v-model="userEmail" placeholder="ENTER YOUR EMAIL"></el-input>
          </el-col>
          <el-col :sm="colSize">
            <el-button type="warning" v-on:click="validateAnswer(answer)">{{answer.text}}</el-button>
          </el-col>

      </el-row>
      <el-row>
        <transition name="fade">

Please make sure your input is valid and not empty, thanks!

        </transition>
      </el-row>

</template>


Fix zooming overlap

Fix zooming overlap

<template>

Loading...

Reservoirs

Percent Full for Month Ending ({{dateListing}}), or Most Recently Available Month

        <svg width="450" height="45" transform="translate(70,0)">
          <g>
            <rect x="30" y="15" width="10" height="10" style="fill: rgb(26, 150, 65);"></rect>
            <text x="45" y="25" height="30" width="20">75%+</text>
          </g>
          <g>
            <rect x="100" y="15" width="10" height="10" style="fill: rgb(252, 232, 131);"></rect>
            <text x="115" y="25" height="30" width="20">50%+</text>
          </g>
          <g>
            <rect x="165" y="15" width="10" height="10" style="fill: rgb(215, 25, 28);"></rect>
            <text x="180" y="25" height="30" width="40">Less than 50% </text>
          </g>
        </svg>

Reservoir Capacity (acre feet)

      <circle-legend-chart
        :dataValues="stations"
        :field="legend_field"
        :whichType="whichType"></circle-legend-chart>
      <svg id="map" class="is-map" :width="width" :height="height" transform="translate(20,0)">
        <g>
          <template v-for="(d, index) in stations">
            <circle :id="whichType + d.state + index"
                    :cx="projection([d.lng, d.lat])[0]"
                    :cy="projection([d.lng, d.lat])[1]"
                    :fill="d.color"
                    :r="scale(d.capacity)"
                    @click="newRes(d)"
                    @mouseover="showItem(d, tipDiv, $event)"
                    @mouseout="hideItem(d, tipDiv, $event)"
                    @touchstart="showItem(d, tipDiv, $event)"
                    @touchend="hideItem(d, tipDiv, $event)"></circle>
          </template>
        </g>
      </svg>

    <line-chart v-show="done" :whichState="whichState" :reservoirName="reservoirName"
                :selectedData="selectedData" :hasKey="hasKey"></line-chart>

</template>

<template>

Loading...

Reservoirs

Percent Full for Month Ending ({{dateListing}}), or Most Recently Available Month

        <svg width="450" height="45" transform="translate(70,0)">
          <g>
            <rect x="30" y="15" width="10" height="10" style="fill: rgb(26, 150, 65);"></rect>
            <text x="45" y="25" height="30" width="20">75%+</text>
          </g>
          <g>
            <rect x="100" y="15" width="10" height="10" style="fill: rgb(252, 232, 131);"></rect>
            <text x="115" y="25" height="30" width="20">50%+</text>
          </g>
          <g>
            <rect x="165" y="15" width="10" height="10" style="fill: rgb(215, 25, 28);"></rect>
            <text x="180" y="25" height="30" width="40">Less than 50% </text>
          </g>
        </svg>

Reservoir Capacity (acre feet)

      <circle-legend-chart
        :dataValues="stations"
        :field="legend_field"
        :whichType="whichType"></circle-legend-chart>
      <svg id="map" class="is-map" width="600" height="500">
        <template v-for="(d, index) in stations">
          <circle :id="whichType + d.state + index"
                  :cx="projection([d.lng, d.lat])[0]"
                  :cy="projection([d.lng, d.lat])[1]"
                  :fill="d.color"
                  :r="scale(d.capacity)"
                  @click="newRes(d)"
                  @mouseover="showItem(d, tipDiv, $event)"
                  @mouseout="hideItem(d, tipDiv, $event)"
                  @touchstart="showItem(d, tipDiv, $event)"
                  @touchend="hideItem(d, tipDiv, $event)"></circle>
        </template>
      </svg>

    <line-chart v-show="done" :whichState="whichState" :reservoirName="reservoirName"
                :selectedData="selectedData" :hasKey="hasKey"></line-chart>

</template>


hide "Share configurations across endpoints"

this issue needs to be reworked post-release

BUG: ZAPP-708

<template>

    <q-toolbar>

Product Manufacturer

        <q-select
          class="q-mr-sm"
          use-input
          hide-selected
          fill-input
          input-debounce="0"
          :options="mfgOptions"
          :option-label="(item) => getMfgOptionLabel(item)"
          @new-value="createValue"
          @input="
            handleOptionChange(DbEnum.sessionOption.manufacturerCodes, $event)
          "
          v-model="selectedManufacturerCode"
          @filter="filterMfgCode"
          outlined
          dense
          data-cy="manufacturer-name-or-code"
        />

Default Response Policy

      <q-select
        class="q-mr-sm"
        :options="defaultResponsePolicyOptions"
        v-model="selectedDefaultResponsePolicy"
        :option-label="(item) => (item === null ? 'NULL' : item.optionLabel)"
        @input="
          handleEnumeratedOptionChange(
            DbEnum.sessionOption.defaultResponsePolicy,
            $event
          )
        "
        outlined
        dense
        data-cy="default-response-policy"
      />

        <q-toggle
          class="q-mr-sm"
          :value="commandDiscoverySetting == 1 ? true : false"
          label="Enable Command Discovery"
          dense
          left-label
          @input="handleOptionChange('commandDiscovery', $event)"
        >
          <q-tooltip> Enable Command Discovery for your project </q-tooltip>
        </q-toggle>

      <q-btn
        class="q-mr-sm"
        align="center"
        text-color="primary"
        flat
        :ripple="false"
        :unelevated="false"
        :outline="none"
        to="/customZcl"
      >

ZCL Extensions...

      </q-btn>
      <q-space />
    </q-toolbar>

</template>

<template>

    <q-toolbar>

Product Manufacturer

        <q-select
          class="q-mr-sm"
          use-input
          hide-selected
          fill-input
          input-debounce="0"
          :options="mfgOptions"
          :option-label="(item) => getMfgOptionLabel(item)"
          @new-value="createValue"
          @input="
            handleOptionChange(DbEnum.sessionOption.manufacturerCodes, $event)
          "
          v-model="selectedManufacturerCode"
          @filter="filterMfgCode"
          outlined
          dense
          data-cy="manufacturer-name-or-code"
        />

Default Response Policy

      <q-select
        class="q-mr-sm"
        :options="defaultResponsePolicyOptions"
        v-model="selectedDefaultResponsePolicy"
        :option-label="(item) => (item === null ? 'NULL' : item.optionLabel)"
        @input="
          handleEnumeratedOptionChange(
            DbEnum.sessionOption.defaultResponsePolicy,
            $event
          )
        "
        outlined
        dense
        data-cy="default-response-policy"
      />

        <q-toggle
          class="q-mr-sm"
          :value="commandDiscoverySetting == 1 ? true : false"
          label="Enable Command Discovery"
          dense
          left-label
          @input="handleOptionChange('commandDiscovery', $event)"
        >
          <q-tooltip> Enable Command Discovery for your project </q-tooltip>
        </q-toggle>

        <q-toggle
          class="q-mr-sm"
          :value="shareConfigsAcrossEndpointsSetting == 1 ? true : false"
          label="Share configurations across endpoints"
          dense
          left-label
          @input="handleOptionChange('shareConfigsAcrossEndpoints', $event)"
        >
          <q-tooltip>
            Replicate identical endpoint configuration across
            endpoints</q-tooltip
          >
        </q-toggle>

      <q-btn
        class="q-mr-sm"
        align="center"
        text-color="primary"
        flat
        :ripple="false"
        :unelevated="false"
        :outline="none"
        to="/customZcl"
      >

ZCL Extensions...

      </q-btn>
      <q-space />
    </q-toolbar>

</template>


fix: apply styling to popovers admin page links (#614)

<template>

        <va-card :title="$t('popovers.popoverStyle')">
          <va-select
            class="mb-4"
            label="color scheme"
            v-model="popover.color"
            :options="colors"
          />
          <va-select
            class="mb-4"
            label="icon (font-awesome)"
            v-model="popover.icon"
            :options="icons"
            key-by="icon"
          />
          <va-input
            class="mb-4"
            v-model="popover.title"
            label="Title"
          />
          <va-input
            class="mb-4"
            v-model="popover.message"
            label="Message"
          />
          <va-popover
            :icon="popover.icon.icon"
            :color="popover.color"
            :title="popover.title"
            :message="popover.message"
            placement="right"
            open
          >
            <va-button>
              {{ $t('notificationsPage.popovers.showPopover') }}
            </va-button>
          </va-popover>
        </va-card>

        <va-card :title="$t('popovers.popoverPlacement')">

            Any text can be used for
            <va-popover
              placement="top"
              message="Top tooltip text"
            >

                {{ $t('notificationsPage.popovers.topTooltip') }}

            </va-popover>
            showcase. Just anything you can possibly imagine to test
            <va-popover
              placement="right"
              message="Right tooltip text"
            >

                {{ $t('notificationsPage.popovers.rightTooltip') }}

            </va-popover>
            .
            But it can appear on the
            <va-popover
              placement="left"
              message="Left tooltip text"
            >

                {{$t('notificationsPage.popovers.leftTooltip')}}

            </va-popover>
            .
            Or just
            <va-popover
              placement="bottom"
              message="Bottom tooltip text"
            >

                {{ $t('notificationsPage.popovers.bottomTooltip') }}

            </va-popover>
            the item.

        </va-card>

</template>


optimization

optimization

<template>

      <vuestic-widget :headerText="$t('spinners.title')" class="spinners__widget">

A

                  <vuestic-slider v-model="config.size" :options="sliderSize"/>

A

                  <vuestic-slider v-model="speed" :options="sliderDuration"/>

              <vuestic-pallet-custom :palette="paletteArray" v-model="color" class="spinners__color-picker"/>

                <component
                  :animation-duration="config.duration"
                  :is="item"
                  :color="color"
                  :size="config.size">
                </component>

              {{item | displayName}}

            {{'spinners.poweredBy' | translate}}
            Epic Spinners

      </vuestic-widget>

</template>


Fix user-list-item conflict naming 'tickets' (#147)

<template>

          {{ event.name }}

          {{ event.dates[0].startTime | cdDateFormatter }}

{{ $t('Next in series:') }}

          {{ nextStartTime |  cdDateFormatter }}

          {{ formattedStartTime }} - {{ formattedEndTime }}

        {{ $t('See Details and Book') }}

{{ $t('Tickets') }}

          <cd-attendee :application="application" :session="sessions[application.sessionId]" :ticket="ticketsById[application.ticketId]" :user="users[application.userId]"></cd-attendee>

        <button tag="button"
          @click="cancel()"
          class="btn btn-lg cd-user-ticket-list-item__view-cancel-button">
          {{ $t('Cancel ticket', applications.length) }}</button>

{{ $t('No ticket booked') }}

        <router-link
          :to="bookLink"
          tag="button" class="btn btn-lg btn-primary cd-user-ticket-list-item__view">
          {{ $t('Book') }}</router-link>

      {{ $t('This is a recurring event') }}

        {{ $t('{recurringFrequencyInfo} at {formattedStartTime} - {formattedEndTime}, from {formattedFirstDate} to {formattedLastDate}',
        {recurringFrequencyInfo, formattedStartTime, formattedEndTime, formattedFirstDate, formattedLastDate}) }}

</template>

<template>

          {{ event.name }}

          {{ event.dates[0].startTime | cdDateFormatter }}

{{ $t('Next in series:') }}

          {{ nextStartTime |  cdDateFormatter }}

          {{ formattedStartTime }} - {{ formattedEndTime }}

        {{ $t('See Details and Book') }}

{{ $t('Tickets') }}

          <cd-attendee :application="application" :session="sessions[application.sessionId]" :ticket="tickets[application.ticketId]" :user="users[application.userId]"></cd-attendee>

        <button tag="button"
          @click="cancel()"
          class="btn btn-lg cd-user-ticket-list-item__view-cancel-button">
          {{ $t('Cancel ticket', applications.length) }}</button>

{{ $t('No ticket booked') }}

        <router-link
          :to="bookLink"
          tag="button" class="btn btn-lg btn-primary cd-user-ticket-list-item__view">
          {{ $t('Book') }}</router-link>

      {{ $t('This is a recurring event') }}

        {{ $t('{recurringFrequencyInfo} at {formattedStartTime} - {formattedEndTime}, from {formattedFirstDate} to {formattedLastDate}',
        {recurringFrequencyInfo, formattedStartTime, formattedEndTime, formattedFirstDate, formattedLastDate}) }}

</template>


update list

update list

<template>
  <page-content page-title="Demo1">

      <md-toolbar class="page-toolbar md-transparent">
        <md-button class="md-icon-button md-raised md-dense">
          <md-icon>add</md-icon>
        </md-button>
        <md-button class="md-icon-button md-raised md-dense">
          <md-icon>add</md-icon>
        </md-button>
        <md-button class="md-icon-button md-raised md-dense">
          <md-icon>add</md-icon>
        </md-button>
      </md-toolbar>
      <md-tabs :md-dynamic-height="false" class="md-transparent" ref="page_tabs" v-if="slotNames.length > 1 || !$slots.default">
        <md-tab class="api-tab" md-label="Properties">

            <form novalidate @submit.stop.prevent="submit">
              <md-input-container>
                Initial value
                <md-input v-model="initialValue"></md-input>
              </md-input-container>

              <md-input-container>
                With label
                <md-input placeholder="My nice placeholder"></md-input>
              </md-input-container>

              <md-input-container md-inline>
                Inline field
                <md-input></md-input>
              </md-input-container>

              <md-input-container>
                Number
                <md-input type="number"></md-input>
              </md-input-container>

              <md-input-container>
                Textarea
                <md-textarea></md-textarea>
              </md-input-container>

              <md-input-container>
                Disabled
                <md-input disabled></md-input>
              </md-input-container>
            </form>

        </md-tab>

        <md-tab class="api-tab" md-label="Classes">

  <md-list class="custom-list md-triple-line">
    <md-list-item>
      <md-avatar>

      </md-avatar>

        Ali Connors
        Brunch this weekend?

I'll be in your neighborhood doing errands...

      <md-button class="md-icon-button md-list-action">
        <md-icon class="md-primary">star</md-icon>
      </md-button>

      <md-divider class="md-inset"></md-divider>
    </md-list-item>

    <md-list-item>
      <md-avatar>

      </md-avatar>

        me, Scott, Jennifer
        Summer BBQ

Wish I could come, but I'm out of town ...

      <md-button class="md-icon-button md-list-action">
        <md-icon>star_border</md-icon>
      </md-button>

      <md-divider class="md-inset"></md-divider>
    </md-list-item>

    <md-list-item>
      <md-avatar>

      </md-avatar>

        Sandra Adams
        Oui oui

Do you have Paris recommendations ...

      <md-button class="md-icon-button md-list-action">
        <md-icon>star_border</md-icon>
      </md-button>

      <md-divider class="md-inset"></md-divider>
    </md-list-item>

    <md-list-item>
      <md-avatar>

      </md-avatar>

        Trevor Hansen
        Order confirmation

Thank you for your recent order from ...

      <md-button class="md-icon-button md-list-action">
        <md-icon>star_border</md-icon>
      </md-button>

      <md-divider class="md-inset"></md-divider>
    </md-list-item>
  </md-list>

        </md-tab>

        <md-tab class="api-tab" md-label="Events" >

hjkghjk

        </md-tab>

        <md-tab class="api-tab" md-label="Methods" >

asdfsadf

        </md-tab>
      </md-tabs>

        <md-card class="md-m-b">
          <md-card-header>
            <md-avatar>

            </md-avatar>

Title goes here

Subtitle here

          </md-card-header>

          <md-card-media>

          </md-card-media>

          <md-card-content>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Optio itaque ea nostrum.
          </md-card-content>

          <md-card-actions>
            <md-button>Action</md-button>
            <md-button>Action</md-button>
          </md-card-actions>
        </md-card>
        <md-whiteframe md-elevation="2" class="md-m-b">
        <md-tabs >
          <md-tab id="movies" md-label="Groups">

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt dolorum quas amet cum vitae, omnis! Illum quas voluptatem, expedita iste, dicta ipsum ea veniam dolore in, quod saepe reiciendis nihil.

          </md-tab>

          <md-tab id="music" md-label="Folders">

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt dolorum quas amet cum vitae, omnis! Illum quas voluptatem, expedita iste, dicta ipsum ea veniam dolore in, quod saepe reiciendis nihil.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt dolorum quas amet cum vitae, omnis! Illum quas voluptatem, expedita iste, dicta ipsum ea veniam dolore in, quod saepe reiciendis nihil.

          </md-tab>

          <md-tab id="books" md-label="Chats">

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt dolorum quas.

          </md-tab>

        </md-tabs>
        </md-whiteframe>

  </page-content>
</template>

<template>
  <page-content page-title="Demo1">

      <md-toolbar class="page-toolbar md-transparent">
        <md-button class="md-icon-button md-raised md-dense">
          <md-icon>add</md-icon>
        </md-button>
        <md-button class="md-icon-button md-raised md-dense">
          <md-icon>add</md-icon>
        </md-button>
        <md-button class="md-icon-button md-raised md-dense">
          <md-icon>add</md-icon>
        </md-button>
      </md-toolbar>
      <md-tabs :md-dynamic-height="false" class="md-transparent" ref="page_tabs" v-if="slotNames.length > 1 || !$slots.default">
        <md-tab class="api-tab" md-label="Properties">

            <form novalidate @submit.stop.prevent="submit">
              <md-input-container>
                Initial value
                <md-input v-model="initialValue"></md-input>
              </md-input-container>

              <md-input-container>
                With label
                <md-input placeholder="My nice placeholder"></md-input>
              </md-input-container>

              <md-input-container md-inline>
                Inline field
                <md-input></md-input>
              </md-input-container>

              <md-input-container>
                Number
                <md-input type="number"></md-input>
              </md-input-container>

              <md-input-container>
                Textarea
                <md-textarea></md-textarea>
              </md-input-container>

              <md-input-container>
                Disabled
                <md-input disabled></md-input>
              </md-input-container>
            </form>

        </md-tab>

        <md-tab class="api-tab" md-label="Classes">

tttttt

        </md-tab>

        <md-tab class="api-tab" md-label="Events" >

hjkghjk

        </md-tab>

        <md-tab class="api-tab" md-label="Methods" >

asdfsadf

        </md-tab>
      </md-tabs>

        <md-card class="md-m-b">
          <md-card-header>
            <md-avatar>

            </md-avatar>

Title goes here

Subtitle here

          </md-card-header>

          <md-card-media>

          </md-card-media>

          <md-card-content>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Optio itaque ea nostrum.
          </md-card-content>

          <md-card-actions>
            <md-button>Action</md-button>
            <md-button>Action</md-button>
          </md-card-actions>
        </md-card>
        <md-whiteframe md-elevation="2" class="md-m-b">
        <md-tabs >
          <md-tab id="movies" md-label="Groups">

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt dolorum quas amet cum vitae, omnis! Illum quas voluptatem, expedita iste, dicta ipsum ea veniam dolore in, quod saepe reiciendis nihil.

          </md-tab>

          <md-tab id="music" md-label="Folders">

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt dolorum quas amet cum vitae, omnis! Illum quas voluptatem, expedita iste, dicta ipsum ea veniam dolore in, quod saepe reiciendis nihil.

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt dolorum quas amet cum vitae, omnis! Illum quas voluptatem, expedita iste, dicta ipsum ea veniam dolore in, quod saepe reiciendis nihil.

          </md-tab>

          <md-tab id="books" md-label="Chats">

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt dolorum quas.

          </md-tab>

        </md-tabs>
        </md-whiteframe>

  </page-content>
</template>


fix[Icons]: copy bug

fix[Icons]: copy bug

<template>

      Add and use

        <el-tooltip placement="top">

            {{generateIconCode(item)}}

            <svg-icon class-name="disabled" :icon-class="item" />
            {{item}}

        </el-tooltip>

</template>

<template>

      Add and use

        <el-tooltip placement="top">

            {{generateIconCode(item)}}

            <svg-icon :icon-class="item" />
            {{item}}

        </el-tooltip>

</template>


Rework navigation so that if it scrolls, the header area stays fixed in place.

<template>
  <nav
    :class="[
      $options.className,
      { 'is-active': navigationActive }
    ]">
    <div :class="`${$options.className}__container`">
      <header :class="`${$options.className}__header`">
        <ButtonIcon
          :label="title"
          heading="3"
          @click.native="toggleNavigation">
          <ImageLazy
            src="/icons/icon-hamburger.png"
            width="60"
            height="60"
            :alt="navigationActive ? 'Close navigation' : 'Open navigation'" />
        </ButtonIcon>
      </header>

      <div :class="`${$options.className}__content`">
        <div :class="`${$options.className}__inner`">
          <ul :class="`${$options.className}__list`">
            <li
              v-for="(item, index) in items"
              :key="index"
              v-jump-to="{
                id: slugify(item.id),
                callback
              }"
              :class="`${$options.className}__item`">
              <h4 :class="`${$options.className}__title`">
                {{ item.title }}
              </h4>
              <p :class="`${$options.className}__description`">
                {{ item.description }}
              </p>
            </li>
          </ul>

          <ThemeSwitcher
            :class="`${$options.className}__switcher`" />
        </div>
      </div>
    </div>
  </nav>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'
import { slugify } from '@/utils/helpers'
import { jumpTo } from '@/utils/directives'
import ButtonIcon from '@/components/ButtonIcon/ButtonIcon'
import ImageLazy from '@/components/ImageLazy/ImageLazy'
import ThemeSwitcher from '@/components/ThemeSwitcher/ThemeSwitcher'

export default {
  name: 'AppNavigation',
  className: 'AppNavigation',
  components: {
    ButtonIcon,
    ImageLazy,
    ThemeSwitcher
  },
  directives: {
    jumpTo
  },
  props: {
    title: {
      type: String,
      default: ''
    },
    items: {
      type: Array,
      default: () => ([])
    }
  },
  computed: {
    ...mapGetters({
      navigationActive: 'navigation/active'
    })
  },
  methods: {
    ...mapActions({
      setNavigationActive: 'navigation/setActive'
    }),
    slugify,
    toggleNavigation() {
      this.setNavigationActive(!this.navigationActive)
    },
    callback() {
      this.setNavigationActive(false)
    }
  }
}
</script>

<style lang="scss" scoped>
.AppNavigation {
  $root: &;

  position: fixed;
  top: calc(var(--spacing-unit) / 2);
  left: calc(var(--spacing-unit) / 2);
  z-index: depth("navigation");
  background-color: transparent;
  pointer-events: none;

  &.is-active {
    pointer-events: auto;
  }

  &__container {
    @include shadow-box();

    display: flex;
    flex-direction: column;
    max-width: calc(100vw - var(--spacing-unit));
    max-height: calc((var(--vh, 1vh) * 100) - var(--spacing-unit));
    padding: 0;
    margin: 0;
    opacity: 0;
    background-color: rgba(color("light"), 0);
    transition: opacity $trans-speed $trans-ease;

    @supports (clip-path: circle(0% at center)) {
      // WTF SCSS? Interpreting calc math with SCSS and butchering it.
      // Need to pass as a whole string.
      $clip-rule: "(var(--button-size) / 2) + (var(--spacing-unit) / 2) * 1";

      opacity: 1;
      clip-path:
        circle(
          calc(var(--button-size) / 2) at
          calc(#{unquote($clip-rule)})
          calc(#{unquote($clip-rule)})
        );
      transition:
        background-color $trans-speed ($trans-speed * 2) $trans-ease,
        clip-path ($trans-speed * 2) $trans-ease;
    }

    #{$root}.is-active & {
      opacity: 1;
      background-color: color("light");
      transition-delay: 0s;

      @supports (clip-path: circle(0% at center)) {
        clip-path: circle(100% at center);
      }
    }
  }

  &__header {
    padding: calc(var(--spacing-unit) / 2);
    pointer-events: auto;

    @include media("sm") {
      padding: calc(var(--spacing-unit) / 2);
    }
  }

  &__content {
    flex: 1;
    width: 100%;
    overflow-y: scroll;
    -webkit-overflow-scrolling: touch;
  }

  &__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 100%;
    padding: calc(var(--spacing-unit) / 2) var(--spacing-unit) var(--spacing-unit);

    @include media("sm") {
      padding: 0 calc(var(--spacing-unit) / 2) calc(var(--spacing-unit) / 2);
    }
  }

  &__list,
  &__switcher {
    width: 100%;
    margin: 0;
    opacity: 0;
    transform: translate3d(0, 5px, 0);
    transition:
      opacity $trans-speed $trans-ease,
      transform $trans-speed $trans-ease;

    #{$root}.is-active & {
      opacity: 1;
      transform: translate3d(0, 0, 0);
    }
  }

  &__list {
    #{$root}.is-active & {
      transition-delay: $trans-speed;
    }
  }

  &__switcher {
    #{$root}.is-active & {
      transition-delay: ($trans-speed * 2);
    }
  }

  &__item {
    width: 100%;
  }

  &__title,
  &__description {
    pointer-events: none;
    user-select: none;
  }

  &__title {
    color: color("dark");
  }

  &__description {
    @include type-style("6");

    margin-bottom: 20px;
    color: color("slate");

    @include media("sm") {
      @include type-style("5");

      margin-bottom: spacing("sm");
    }
  }
}
</style>

<template>
  <nav
    :class="[
      $options.className,
      { 'is-active': navigationActive }
    ]">
    <div :class="`${$options.className}__container`">
      <ButtonIcon
        :label="title"
        heading="3"
        :class="`${$options.className}__toggle`"
        @click.native="toggleNavigation">
        <ImageLazy
          src="/icons/icon-hamburger.png"
          width="60"
          height="60"
          :alt="navigationActive ? 'Close navigation' : 'Open navigation'" />
      </ButtonIcon>

      <ul :class="`${$options.className}__list`">
        <li
          v-for="(item, index) in items"
          :key="index"
          v-jump-to="{
            id: slugify(item.id),
            callback
          }"
          :class="`${$options.className}__item`">
          <h4 :class="`${$options.className}__title`">
            {{ item.title }}
          </h4>
          <p :class="`${$options.className}__description`">
            {{ item.description }}
          </p>
        </li>
      </ul>

      <ThemeSwitcher />
    </div>
  </nav>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'
import { slugify } from '@/utils/helpers'
import { jumpTo } from '@/utils/directives'
import ButtonIcon from '@/components/ButtonIcon/ButtonIcon'
import ImageLazy from '@/components/ImageLazy/ImageLazy'
import ThemeSwitcher from '@/components/ThemeSwitcher/ThemeSwitcher'

export default {
  name: 'AppNavigation',
  className: 'AppNavigation',
  components: {
    ButtonIcon,
    ImageLazy,
    ThemeSwitcher
  },
  directives: {
    jumpTo
  },
  props: {
    title: {
      type: String,
      default: ''
    },
    items: {
      type: Array,
      default: () => ([])
    }
  },
  computed: {
    ...mapGetters({
      navigationActive: 'navigation/active'
    })
  },
  methods: {
    ...mapActions({
      setNavigationActive: 'navigation/setActive'
    }),
    slugify,
    toggleNavigation() {
      this.setNavigationActive(!this.navigationActive)
    },
    callback() {
      this.setNavigationActive(false)
    }
  }
}
</script>

<style lang="scss" scoped>
.AppNavigation {
  $root: &;

  position: fixed;
  top: calc(var(--spacing-unit) / 2);
  left: calc(var(--spacing-unit) / 2);
  z-index: depth("navigation");
  background-color: transparent;
  pointer-events: none;

  &.is-active {
    pointer-events: auto;
  }

  &__toggle {
    margin-bottom: var(--spacing-unit);
    margin-left: calc(-1 * (var(--spacing-unit) / 2));
    pointer-events: auto;

    @include media("sm") {
      margin-bottom: calc(var(--spacing-unit) / 2);
      margin-left: 0;
    }
  }

  &__container {
    @include shadow-box();

    max-width: calc(100vw - var(--spacing-unit));
    max-height: calc((var(--vh, 1vh) * 100) - var(--spacing-unit));
    overflow-y: auto;
    padding: calc(var(--spacing-unit) / 2) var(--spacing-unit);
    margin: 0;
    opacity: 0;
    background-color: rgba(color("light"), 0);
    transition: opacity $trans-speed $trans-ease;

    @include media("sm") {
      padding: calc(var(--spacing-unit) / 2);
    }

    @supports (clip-path: circle(0% at center)) {
      // WTF SCSS? Interpreting calc math with SCSS and butchering it.
      // Need to pass as a whole string.
      $clip-rule: "(var(--button-size) / 2) + (var(--spacing-unit) / 2) * 1";

      opacity: 1;
      clip-path:
        circle(
          calc(var(--button-size) / 2) at
          calc(#{unquote($clip-rule)})
          calc(#{unquote($clip-rule)})
        );
      transition:
        background-color $trans-speed ($trans-speed * 2) $trans-ease,
        clip-path ($trans-speed * 2) $trans-ease;
    }

    #{$root}.is-active & {
      opacity: 1;
      background-color: color("light");
      transition-delay: 0s;

      @supports (clip-path: circle(0% at center)) {
        clip-path: circle(100% at center);
      }
    }
  }

  &__list {
    width: 100%;
    margin: 0;
    opacity: 0;
    transform: translate3d(0, 10px, 0);
    transition:
      opacity $trans-speed $trans-ease,
      transform $trans-speed $trans-ease;

    #{$root}.is-active & {
      opacity: 1;
      transform: translate3d(0, 0, 0);
      transition-delay: $trans-speed;
    }
  }

  &__item {
    width: 100%;
  }

  &__title,
  &__description {
    pointer-events: none;
    user-select: none;
  }

  &__title {
    color: color("dark");
  }

  &__description {
    @include type-style("6");

    margin-bottom: 20px;
    color: color("slate");

    @include media("sm") {
      @include type-style("5");

      margin-bottom: spacing("sm");
    }
  }
}
</style>


Patch 2 (#50)

Patch 2 (#50)

* Update README.md

* Fix issue with spinner never stopping (#26)

I was experiencing [the same issue as reported here](https://github.com/quasarframework/quasar/issues/888), and learned that the slot="message" attribute needs to be moved upwards to the highest div parent of the spinner, for it to work as expected. The slot="message" attribute should only be part of the q-spinner tag if it is not enclosed in a parent div.

Would be nice to have this also reflected here in the play demo/documentation.

* update 800 → 600ms (#46)

* Update input.vue (#47)

Deprecated property "min-rows".

* Update card.vue (#49)

* fix broken image link

Wrong link to statics

<template>
  <q-page padding class="row justify-center">

Chat with avatar

To mix messages with avatar and without avatar in the same thread, use a placeholder avatar image.

      <q-chat-message
        v-for="(msg, index) in messages"
        :key="`reg-${index}`"
        :label="msg.label"
        :sent="msg.sent"
        :text-color="msg.textColor"
        :bg-color="msg.bgColor"
        :name="msg.name"
        :avatar="msg.avatar"
        :text="msg.text"
        :stamp="msg.stamp"
      />
      <q-chat-message
        name="Vladimir"
        avatar="statics/boy-avatar.png"
      >
        <q-spinner-dots size="2rem" />
      </q-chat-message>

Chat using avatar slot

      <q-chat-message
        name="Vladimir"
        :text="['Use your own spacing or class q-message-avatar']"
      >
        <q-icon name="face" size="4em" slot="avatar"/>
      </q-chat-message>

Chat without avatar

      <q-chat-message
        v-for="(msg, index) in messages"
        :key="`avatar-${index}`"
        :label="msg.label"
        :sent="msg.sent"
        :text-color="msg.textColor"
        :bg-color="msg.bgColor"
        :name="msg.name"
        :text="msg.text"
        :stamp="msg.stamp"
      />
      <q-chat-message
        name="Vladimir"
      >
        <q-spinner-dots size="2rem" />
      </q-chat-message>

  </q-page>
</template>

<template>
  <q-page padding class="row justify-center">

Chat with avatar

To mix messages with avatar and without avatar in the same thread, use a placeholder avatar image.

      <q-chat-message
        v-for="(msg, index) in messages"
        :key="`reg-${index}`"
        :label="msg.label"
        :sent="msg.sent"
        :text-color="msg.textColor"
        :bg-color="msg.bgColor"
        :name="msg.name"
        :avatar="msg.avatar"
        :text="msg.text"
        :stamp="msg.stamp"
      />
      <q-chat-message
        name="Vladimir"
        avatar="/statics/boy-avatar.png"
      >
        <q-spinner-dots size="2rem" />
      </q-chat-message>

Chat using avatar slot

      <q-chat-message
        name="Vladimir"
        :text="['Use your own spacing or class q-message-avatar']"
      >
        <q-icon name="face" size="4em" slot="avatar"/>
      </q-chat-message>

Chat without avatar

      <q-chat-message
        v-for="(msg, index) in messages"
        :key="`avatar-${index}`"
        :label="msg.label"
        :sent="msg.sent"
        :text-color="msg.textColor"
        :bg-color="msg.bgColor"
        :name="msg.name"
        :text="msg.text"
        :stamp="msg.stamp"
      />
      <q-chat-message
        name="Vladimir"
      >
        <q-spinner-dots size="2rem" />
      </q-chat-message>

  </q-page>
</template>