"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"
}
}
el: '#app',
data: {
- showSearchBar: false
+
},
methods: {
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<!-- 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>
<!-- 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()
}
}
}
</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">
@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>
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==
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==
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==
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==
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"
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"