]> _ Git - pmi.git/commitdiff
WIP #2739 @9
authorStephen Cameron <stephen@cubedesigners.com>
Thu, 27 Jun 2019 20:41:34 +0000 (22:41 +0200)
committerStephen Cameron <stephen@cubedesigners.com>
Thu, 27 Jun 2019 20:41:34 +0000 (22:41 +0200)
package.json
resources/js/app.js
resources/js/components/Cart.vue [new file with mode: 0644]
resources/js/components/CartItem.vue [new file with mode: 0644]
resources/js/components/NumberInput.vue [new file with mode: 0644]
resources/js/components/Search.vue
resources/views/partials/header.blade.php
yarn.lock

index 6bfb5aa102e84042a56a2eed3c708b7637426344..e25a4cc6c474cd4be0178e8949b6f24cfba7ce2c 100644 (file)
@@ -28,6 +28,7 @@
         "stylus-loader": "^3.0.2",
         "tailwindcss": "^1.0.1",
         "vue": "^2.6.10",
+        "vue-slide-up-down": "^1.7.2",
         "vue-template-compiler": "^2.6.10"
     }
 }
index 1f9f6c53f488c1b7aec26ca42d32043977a3df83..d2f8884f18a5b7d2600c5748588a0da9e09dcc98 100644 (file)
@@ -33,7 +33,7 @@ const app = new Vue({
     el: '#app',
 
     data: {
-      showSearchBar: false
+
     },
 
     methods: {
diff --git a/resources/js/components/Cart.vue b/resources/js/components/Cart.vue
new file mode 100644 (file)
index 0000000..c3f5151
--- /dev/null
@@ -0,0 +1,27 @@
+<template>
+    <div class="cart-wrapper">
+        <cart-item v-for="item in items" :key="item.id" :item="item"></cart-item>
+    </div>
+</template>
+
+<script>
+    import CartItem from './CartItem'
+
+    export default {
+        name: "Cart",
+
+        components: {
+            CartItem
+        },
+
+        props: {
+            items: {
+                type: Array
+            }
+        }
+    }
+</script>
+
+<style scoped>
+
+</style>
diff --git a/resources/js/components/CartItem.vue b/resources/js/components/CartItem.vue
new file mode 100644 (file)
index 0000000..65c5081
--- /dev/null
@@ -0,0 +1,43 @@
+<template>
+    <div class="cart-item flex mb-1v">
+        <div class="border-gray-100 border-2 bg-center bg-contain bg-no-repeat"
+             :style="`background-image: url(${item.image}); width: 144px; height: 144px`">
+        </div>
+        <div class="pl-6 leading-relaxed flex-grow">
+            <div class="font-bold">
+                {{ item.category }}
+                <br>
+                {{ item.name }}
+            </div>
+            <div class="bg-grey-100 py-1 pl-3 my-2 flex items-center justify-between">
+                <span class="mr-2">Quantité</span>
+                <number-input :value="item.quantity" :min="1" inline center controls></number-input>
+            </div>
+            <a href="#" class="cart-delete-item text-red">
+                Supprimer
+            </a>
+        </div>
+    </div>
+</template>
+
+<script>
+    import NumberInput from './NumberInput'
+
+    export default {
+        name: "CartItem",
+
+        components: {
+            NumberInput
+        },
+
+        props: {
+            item: {
+                type: Object
+            }
+        }
+    }
+</script>
+
+<style scoped>
+
+</style>
diff --git a/resources/js/components/NumberInput.vue b/resources/js/components/NumberInput.vue
new file mode 100644 (file)
index 0000000..a34a614
--- /dev/null
@@ -0,0 +1,332 @@
+<!-- Adapted version of https://github.com/fengyuanchen/vue-number-input -->
+<template>
+    <div
+        class="number-input"
+        :class="{
+      'number-input--inline': inline,
+      'number-input--center': center,
+      'number-input--controls': controls,
+    }"
+        v-on="listeners"
+    >
+        <button
+            v-if="controls"
+            class="number-input__button number-input__button--minus hover:text-blue"
+            type="button"
+            :disabled="disabled || readonly || !decreasable"
+            @click="decrease"
+        />
+        <input
+            ref="input"
+            class="number-input__input"
+            type="number"
+            :name="name"
+            :value="currentValue"
+            :min="min"
+            :max="max"
+            :step="step"
+            :readonly="readonly || !inputtable"
+            :disabled="disabled || (!decreasable && !increasable)"
+            :placeholder="placeholder"
+            autocomplete="off"
+            @change="change"
+            @paste="paste"
+        >
+        <button
+            v-if="controls"
+            class="number-input__button number-input__button--plus hover:text-blue"
+            type="button"
+            :disabled="disabled || readonly || !increasable"
+            @click="increase"
+        />
+    </div>
+</template>
+
+<script>
+    const isNaN = Number.isNaN || window.isNaN;
+    const REGEXP_NUMBER = /^-?(?:\d+|\d+\.\d+|\.\d+)(?:[eE][-+]?\d+)?$/;
+    const REGEXP_DECIMALS = /\.\d*(?:0|9){10}\d*$/;
+    const normalizeDecimalNumber = (value, times = 100000000000) => (
+        REGEXP_DECIMALS.test(value) ? (Math.round(value * times) / times) : value
+    );
+
+    export default {
+        name: 'NumberInput',
+
+        model: {
+            event: 'change',
+        },
+
+        props: {
+            center: Boolean,
+            controls: Boolean,
+            disabled: Boolean,
+
+            inputtable: {
+                type: Boolean,
+                default: true,
+            },
+
+            inline: Boolean,
+
+            max: {
+                type: Number,
+                default: Infinity,
+            },
+
+            min: {
+                type: Number,
+                default: -Infinity,
+            },
+
+            name: {
+                type: String,
+                default: undefined,
+            },
+
+            placeholder: {
+                type: String,
+                default: undefined,
+            },
+
+            readonly: Boolean,
+
+            step: {
+                type: Number,
+                default: 1,
+            },
+
+            value: {
+                type: Number,
+                default: NaN,
+            },
+        },
+
+        data() {
+            return {
+                currentValue: NaN,
+            };
+        },
+
+        computed: {
+            /**
+             * Indicate if the value is increasable.
+             * @returns {boolean} Return `true` if it is decreasable, else `false`.
+             */
+            increasable() {
+                const num = this.currentValue;
+
+                return isNaN(num) || num < this.max;
+            },
+
+            /**
+             * Indicate if the value is decreasable.
+             * @returns {boolean} Return `true` if it is decreasable, else `false`.
+             */
+            decreasable() {
+                const num = this.currentValue;
+
+                return isNaN(num) || num > this.min;
+            },
+
+            /**
+             * Filter listeners
+             * @returns {Object} Return filtered listeners.
+             */
+            listeners() {
+                const listeners = { ...this.$listeners };
+
+                delete listeners.change;
+
+                return listeners;
+            },
+        },
+
+        watch: {
+            value: {
+                immediate: true,
+                handler(newValue, oldValue) {
+                    if (
+                        // Avoid triggering change event when created
+                        !(isNaN(newValue) && typeof oldValue === 'undefined')
+
+                        // Avoid infinite loop
+                        && newValue !== this.currentValue
+                    ) {
+                        this.setValue(newValue);
+                    }
+                },
+            },
+        },
+
+        methods: {
+            /**
+             * Change event handler.
+             * @param {string} value - The new value.
+             */
+            change(event) {
+                this.setValue(Math.min(this.max, Math.max(this.min, event.target.value)));
+            },
+
+            /**
+             * Paste event handler.
+             * @param {Event} event - Event object.
+             */
+            paste(event) {
+                const clipboardData = event.clipboardData || window.clipboardData;
+
+                if (clipboardData && !REGEXP_NUMBER.test(clipboardData.getData('text'))) {
+                    event.preventDefault();
+                }
+            },
+
+            /**
+             * Decrease the value.
+             */
+            decrease() {
+                if (this.decreasable) {
+                    let { currentValue } = this;
+
+                    if (isNaN(currentValue)) {
+                        currentValue = 0;
+                    }
+
+                    this.setValue(Math.min(this.max, Math.max(
+                        this.min,
+                        normalizeDecimalNumber(currentValue - this.step),
+                    )));
+                }
+            },
+
+            /**
+             * Increase the value.
+             */
+            increase() {
+                if (this.increasable) {
+                    let { currentValue } = this;
+
+                    if (isNaN(currentValue)) {
+                        currentValue = 0;
+                    }
+
+                    this.setValue(Math.min(this.max, Math.max(
+                        this.min,
+                        normalizeDecimalNumber(currentValue + this.step),
+                    )));
+                }
+            },
+
+            /**
+             * Set new value and dispatch change event.
+             * @param {number} value - The new value to set.
+             */
+            setValue(value) {
+                const oldValue = this.currentValue;
+                let newValue = this.rounded ? Math.round(value) : value;
+
+                if (this.min <= this.max) {
+                    newValue = Math.min(this.max, Math.max(this.min, newValue));
+                }
+
+                this.currentValue = newValue;
+
+                if (newValue === oldValue) {
+                    // Force to override the number in the input box (#13).
+                    this.$refs.input.value = newValue;
+                }
+
+                this.$emit('change', newValue, oldValue);
+            },
+        },
+    };
+</script>
+
+<style lang="stylus" scoped>
+    .number-input
+        display: block
+        max-width: 100%
+        overflow: hidden
+        position: relative
+
+        &__button
+            border: 0
+            position: absolute
+            top: 0
+            bottom: 1px
+            width: 2em
+            z-index: 1
+
+            &:focus
+                outline: none
+
+            &:disabled
+                opacity: 0.4
+                cursor: not-allowed
+
+
+            &::before,
+            &::after
+                background-color: currentColor
+                content: ""
+                left: 50%
+                position: absolute
+                top: 50%
+                transform: translate(-50%, -50%)
+                transition: background-color 0.15s
+
+            &::before
+                height: 1px
+                width: 0.75em
+
+            &::after
+                height: 0.75em
+                width: 1px
+
+            &--minus
+                left: 1px
+
+                &::after
+                    visibility: hidden
+
+            &--plus
+                right: 1px
+
+        &__input
+            background-color: transparent
+            border: none
+            display: block
+            font-size: 1em
+            line-height: 1.5
+            max-width: 100%
+            min-height: 1.5em
+            min-width: 2.5em
+            padding: 0.4375em 0.875em
+            transition: border-color 0.15s
+            width: 100%
+
+            // Hide browser number spinners
+            &::-webkit-outer-spin-button,
+            &::-webkit-inner-spin-button
+                -webkit-appearance: none
+
+
+        &--inline
+            display: inline-block
+
+            & > input
+                box-sizing: content-box
+                display: inline-block
+                width: 2.5em
+                padding: 0.4em 2.5em
+
+        &--center
+            & > input
+                text-align: center
+
+        &--controls
+            & > input
+                padding-left: 2.2em
+                padding-right: 2.2em
+
+
+</style>
index 9fc256d4318029793ca03361194a5688b9c0e8c3..91227c26fc33a90d9baf4e37aced9949c795225b 100644 (file)
@@ -3,32 +3,39 @@
 
         <!-- Trigger Link that will appear inside the nav portal -->
         <portal to="nav-search-toggle">
-            <a href="#" class="text-white hover:text-blue" @click.prevent="toggleVisibility">
+            <a href="#" class="text-white hover:text-blue" @click.prevent="toggleVisibility" :class="visible ? 'text-blue' : ''">
                 <slot name="link"></slot>
             </a>
         </portal>
 
-
-        <div class="header-search-box container py-3" v-show="visible">
-            <form action="/search/" method="get" class="flex justify-between items-center">
-                <input class="pl-2 -ml-2 py-2 font-display text-2xl flex-grow appearance-none focus:outline-none focus:bg-grey-100"
-                       type="text"
-                       name="search"
-                       autocomplete="off"
-                       tabindex="-1"
-                       ref="searchInput"
-                       :placeholder="placeholder">
-                <button class="ml-4 p-3 -mr-3 appearance-none focus:outline-none focus:bg-grey-100">
-                    <slot name="button"></slot>
-                </button>
-            </form>
-        </div>
+        <slide-toggle :active="visible" :duration="300" @open-end="focusField">
+            <div class="header-search-box container py-3">
+                <form action="/search/" method="get" class="flex justify-between items-center">
+                    <input class="pl-2 -ml-2 py-2 font-display text-2xl flex-grow appearance-none focus:outline-none focus:bg-grey-100"
+                           type="text"
+                           name="query"
+                           autocomplete="off"
+                           tabindex="-1"
+                           ref="searchInput"
+                           :placeholder="placeholder">
+                    <button class="ml-4 p-3 -mr-3 appearance-none focus:outline-none focus:bg-grey-100">
+                        <slot name="button"></slot>
+                    </button>
+                </form>
+            </div>
+        </slide-toggle>
     </div>
 </template>
 
 <script>
+    import SlideToggle from 'vue-slide-up-down'
+
     export default {
 
+        components: {
+            SlideToggle
+        },
+
         data() {
             return {
                 visible: false
                 default: 'Search...'
             }
         },
+
+        mounted () {
+            let component = this;
+
+            // Allow search field to be opened using the common '/' shortcut
+            window.addEventListener('keydown', (event) => {
+                // When these types of elements have focus, don't trigger the search box toggle
+                let toggleKeyCode = 191; // This is the '/' key
+                let whitelistedElements = ['INPUT', 'TEXTAREA'];
+                if (event.keyCode === toggleKeyCode && !whitelistedElements.includes(document.activeElement.tagName)) {
+                    component.toggleVisibility();
+                }
+            });
+
+            // Allow search field to be hidden when pressing escape from inside the field
+            this.$refs.searchInput.addEventListener('keydown', (event) => {
+                event.stopPropagation(); // We'll handle all keydown events here only
+                if (event.keyCode === 27) {
+                    component.toggleVisibility(); // Hide the search box when ESC is pressed
+                    component.$refs.searchInput.blur(); // Take focus away from the search input so it doesn't block the opening shortcut
+                }
+            });
+        },
+
         methods: {
             toggleVisibility() {
                 this.visible = !this.visible;
+            },
 
-                // When visible, auto focus cursor into search field
-                if (this.visible) {
-                    this.$nextTick(() => this.$refs.searchInput.focus())
-                }
+            focusField() {
+                this.$refs.searchInput.focus()
             }
         }
     }
index 1050d4a7021f54ee9ee473db3d87a211b6bc27d6..e88df29d6834da8fcc62b807c108acda64014e7d 100644 (file)
             </div>
             <div class="cart-header-popout-content text-navy font-body p-1v pb-0">
 
-                @for ($i = 1; $i <= 6; $i++)
-                    <div class="cart-header-popout-item flex mb-1v">
-                        <div class="border-gray-100 border-2 bg-center bg-contain bg-no-repeat"
-                             style="background-image: url({{ asset('storage/products/'. rand(1, 6) .'.png') }}); width: 144px; height: 144px">
-                        </div>
-                        <div class="pl-6 leading-relaxed flex-grow">
-                            <div class="font-bold">Capteur de force<br>Modèle 1220</div>
-                            <div class="bg-grey-100 py-1 pl-3 my-2 flex items-center justify-between">
-                                <span>Quantité</span>
-                                <div>
-                                    <span class="px-3">-</span>
-                                    <span class="text-sm">3</span>
-                                    <span class="px-3">+</span>
-                                </div>
-                            </div>
-                            <a href="#" class="cart-delete-item text-red">
-                                Supprimer
-                            </a>
-                        </div>
-                    </div>
-                @endfor
+                @php
+                    //#### Generate temporary data
+                    $cart_items = [];
+                    for ($i = 1; $i <= 6; $i++) {
+                        $cart_items[] = [
+                            'id' => $i,
+                            'quantity' => rand(1, 15),
+                            'name' => 'Modèle '. rand(1000, 1500),
+                            'category' => 'Capteur de force',
+                            'image' => '/storage/products/'. rand(1,6) .'.png',
+                        ];
+                    }
+                @endphp
+
+                <cart :items='@json($cart_items)'></cart>
 
             </div>
             <div class="cart-header-popout-footer bg-grey-100 p-1v">
@@ -69,6 +63,6 @@
         @svg('search')
     </template>
     <template v-slot:button>
-        @svg('search', 'fill-current text-navy w-6 h-6')
+        @svg('search', 'fill-current text-navy w-6 h-6 hover:text-blue')
     </template>
-</search>
\ No newline at end of file
+</search>
index ba48326fbe1784a8dad14c06071f2cb3f2dfeb31..e9b770afda2c99177286cb8003baddf2c11f9c06 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -3152,7 +3152,7 @@ glob-to-regexp@^0.3.0:
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
   integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
 
-glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
+glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.2:
   version "7.1.4"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
   integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
@@ -6154,7 +6154,7 @@ safe-regex@^1.1.0:
   dependencies:
     ret "~0.1.10"
 
-"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.2:
+"safer-buffer@>= 2.1.2 < 3":
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -6198,7 +6198,7 @@ selfsigned@^1.10.4:
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
   integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
 
-semver@^6.0.0, semver@^6.1.0, semver@^6.1.1:
+semver@^6.1.0, semver@^6.1.1:
   version "6.1.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b"
   integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==
@@ -6530,7 +6530,7 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
 
-source-map@^0.7.3:
+source-map@~0.7.2:
   version "0.7.3"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
   integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
@@ -6786,12 +6786,10 @@ stylus@acidjazz/stylus#dev:
   dependencies:
     css-parse "~2.0.0"
     debug "~3.1.0"
-    glob "^7.1.3"
+    glob "~7.1.2"
     mkdirp "~0.5.x"
-    safer-buffer "^2.1.2"
     sax "~1.2.4"
-    semver "^6.0.0"
-    source-map "^0.7.3"
+    source-map "~0.7.2"
 
 supports-color@^2.0.0:
   version "2.0.0"
@@ -7243,6 +7241,11 @@ vue-loader@^15.4.2:
     vue-hot-reload-api "^2.3.0"
     vue-style-loader "^4.1.0"
 
+vue-slide-up-down@^1.7.2:
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/vue-slide-up-down/-/vue-slide-up-down-1.7.2.tgz#88bcea3203eb054a44f4eba1bc77062d555db3d6"
+  integrity sha512-y7vpjKNfjQGdKiLTZyonNZbWjtzyEA9nGXIK8wojJvGQapHi7EsLcWAYOQHJ1dE70FrpbAyMP6OIV3xRJEwXkg==
+
 vue-style-loader@^4.1.0:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8"