├── README.md ├── components ├── delete │ ├── Delete.vue │ └── README.md ├── editable-textarea │ ├── EditableTextarea.js │ └── README.md ├── input-file │ ├── InputFile.vue │ └── README.md └── resizable-textarea │ ├── README.md │ └── ResizableTextarea.js └── directives └── dragdrop ├── Dragdrop.js └── README.md /README.md: -------------------------------------------------------------------------------- 1 | # 👨‍🔬 Vue Lab 2 | 3 | Vue Lab is collection of detailed Vue.js **components** and **directives** that can easily be reused into your Vue.js projects. Such Vue.js goodies are useful when pulling an existing library seems like an overkill and you just need a place to start, something to **copy/paste/edit** into your projects. 4 | 5 | ## Components 6 | 7 | * **[Delete](components/delete)**. Confirm deletion without openning a modal. 8 | * **[EditableTextarea](components/editable-textarea)**. Edit the selected text of a textarea. *(renderless)* 9 | * **[InputFile](components/input-file)**. Display a decent input file to your users. 10 | * **[ResizableTextarea](components/resizable-textarea)**. Resize a textarea based on its content. *(renderless)* 11 | 12 | ## Directives 13 | 14 | * **[Dragdrop](directives/dragdrop)**. Reorder your lists using drag&drop. 15 | 16 | ## Contributing 17 | 18 | You are more than welcome to request or suggest more Vue.js goodies by either openning an issue or implementing it yourself in a PR. When openning a PR, please provide a `readme.md` file following the same convention as the ones provided with existing goodies. A working example on CodePen and/or a blog article is also more than welcome. 19 | -------------------------------------------------------------------------------- /components/delete/Delete.vue: -------------------------------------------------------------------------------- 1 | 2 | 38 | 39 | 134 | 135 | 155 | -------------------------------------------------------------------------------- /components/delete/README.md: -------------------------------------------------------------------------------- 1 | # :wastebasket: Delete component 2 | 3 | Provides a convenient way for the user to confirm something that needs to be delete. This is useful when you don't want users to delete elements by mistake but don't want to bother them with a modal neither. 4 | 5 | ![Demo screenshot](https://user-images.githubusercontent.com/3642397/36788432-24397a0c-1c8e-11e8-9c9f-2c872c515124.png) 6 | 7 | ## Demo 8 | 9 | * [CodePen](https://codepen.io/lorisleiva/pen/LQMaNj) 10 | * [Blog article](http://lorisleiva.com/are-you-sure) 11 | 12 | ## Installation 13 | 14 | Install dependencies 15 | ``` 16 | npm install axios -D 17 | npm install font-awesome -D 18 | ``` 19 | Copy/paste the code in a new `Delete.vue` file. 20 | 21 | Add it to any component that needs it. 22 | ```js 23 | import Delete from './path/to/Delete.vue' 24 | 25 | export default { 26 | components: { Delete }, 27 | } 28 | ``` 29 | 30 | ## Usage 31 | 32 | ```html 33 | 37 | ``` 38 | 39 | ## Options 40 | 41 | | Attribute | Default | Description | 42 | | - | - | - | 43 | | `url` | *No API call* | The API endpoint used to delete the item. | 44 | | `deleteDelay` | 500 | Delay in milliseconds to wait between the moment the item has been deleted by the API and the time we raise the `@delete` event. This enables the user to see some "deleted" feedback before the component disappears. | 45 | | `confirmText` | 'Are you sure?' | The confirmation text prompted to the user. | 46 | | `successText` | 'Deleted' | The text shown when the item has been successfully deleted. | 47 | | `errorText` | 'Something went wrong!' | The text shown when an error occurs. | 48 | 49 | ## Events 50 | 51 | | Event | Arguments | Description | 52 | | - | - | - | 53 | | `@delete` | | Called when the item has been successfully deleted by the API endpoint. | 54 | -------------------------------------------------------------------------------- /components/editable-textarea/EditableTextarea.js: -------------------------------------------------------------------------------- 1 | import { startsWith, endsWith } from 'lodash' 2 | 3 | export default { 4 | methods: { 5 | getContent() { 6 | return { 7 | text: this.$el.value, 8 | start: this.$el.selectionStart, 9 | end: this.$el.selectionEnd, 10 | } 11 | }, 12 | updateContent(text, start, end) { 13 | this.$el.value = text 14 | triggerEvent(this.$el, 'input') 15 | 16 | this.$el.selectionStart = start 17 | this.$el.selectionEnd = end 18 | this.$el.focus() 19 | 20 | return text 21 | }, 22 | wrapWith(pattern, placeholder) { 23 | let { text, start, end } = this.getContent() 24 | let { before, selection, after } = cutTextWithSelection(text, start, end) 25 | let wrappedContent = selection || placeholder || '' 26 | 27 | // Exception for bold and italic 28 | let keepItalicPattern = pattern === '*' 29 | && endsWith(before, '**') && !endsWith(before, '***') 30 | && startsWith(after, '**') && !startsWith(after, '***') 31 | 32 | let removePattern = endsWith(before, pattern) 33 | && startsWith(after, pattern) 34 | && !keepItalicPattern 35 | 36 | before = removePattern ? before.slice(0, - pattern.length) : before + pattern 37 | after = removePattern ? after.slice(pattern.length) : pattern + after 38 | 39 | return this.updateContent( 40 | before + wrappedContent + after, 41 | before.length, 42 | before.length + wrappedContent.length, 43 | ) 44 | }, 45 | }, 46 | render() { 47 | return this.$slots.default[0] 48 | }, 49 | } 50 | 51 | function cutTextWithSelection(text, start, end) { 52 | return { 53 | before: text.substring(0, start), 54 | selection: text.substring(start, end), 55 | after: text.substring(end, text.length), 56 | } 57 | } 58 | 59 | function triggerEvent(el, type) { 60 | if ('createEvent' in document) { 61 | // modern browsers, IE9+ 62 | var e = document.createEvent('HTMLEvents') 63 | e.initEvent(type, false, true) 64 | el.dispatchEvent(e) 65 | } else { 66 | // IE 8 67 | var e = document.createEventObject() 68 | e.eventType = type 69 | el.fireEvent('on' + e.eventType, e) 70 | } 71 | } -------------------------------------------------------------------------------- /components/editable-textarea/README.md: -------------------------------------------------------------------------------- 1 | # :memo: EditableTextarea component 2 | 3 | Renderless controlable component that updates the content of a textarea. 4 | 5 | ![Demo screenshot](https://user-images.githubusercontent.com/3642397/40798547-972480f0-650b-11e8-9d4f-ca8e2d44ba74.png) 6 | 7 | ## Demo 8 | 9 | * [CodePen](https://codepen.io/lorisleiva/pen/LrVPKE) 10 | * [Blog article](http://lorisleiva.com/renderless-editable-textarea) 11 | 12 | ## Installation 13 | 14 | Install dependencies 15 | ```bash 16 | npm install lodash -D 17 | ``` 18 | 19 | Copy/paste the code in a new `EditableTextarea.js` file. 20 | 21 | Add it to any component that needs it. 22 | ```js 23 | import EditableTextarea from './path/to/EditableTextarea.js' 24 | 25 | export default { 26 | components: { EditableTextarea }, 27 | } 28 | ``` 29 | 30 | ## Usage 31 | 32 | ```html 33 | 34 | 35 | 36 | ``` 37 | 38 | ```js 39 | // Wrap selection between '**' 40 | this.$refs.editor.wrapWith('**') 41 | 42 | // If selection is empty, adds **placeholder** 43 | this.$refs.editor.wrapWith('**', 'placeholder') 44 | ``` -------------------------------------------------------------------------------- /components/input-file/InputFile.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 77 | 78 | 83 | -------------------------------------------------------------------------------- /components/input-file/README.md: -------------------------------------------------------------------------------- 1 | # :bookmark_tabs: InputFile component 2 | 3 | Provides a simple wrapper to the native `` for aesthetic purposes. 4 | 5 | ![Demo screenshot](https://user-images.githubusercontent.com/3642397/36842316-9f355466-1d4b-11e8-8551-037c7390b9b9.png) 6 | 7 | ## Demo 8 | 9 | * [CodePen](https://codepen.io/lorisleiva/pen/VQgdgP) 10 | 11 | ## Installation 12 | 13 | Copy/paste the code in a new `InputFile.vue` file. 14 | 15 | Add it to any component that needs it. 16 | ```js 17 | import InputFile from './path/to/InputFile.vue' 18 | 19 | export default { 20 | components: { InputFile }, 21 | } 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```html 27 | 28 | Browse... 29 | 30 | ``` 31 | 32 | ## Options 33 | 34 | | Attribute | Default | Description | 35 | | - | - | - | 36 | | `$slots.default` | 'Select...' | The text displayed on the button. | 37 | | `name` | | The name attribute given to the hidden input of type `file`. | 38 | | `multiple` | | Allows multiple file selection when provided. | 39 | -------------------------------------------------------------------------------- /components/resizable-textarea/README.md: -------------------------------------------------------------------------------- 1 | # :scroll: ResizableTextarea component 2 | 3 | Renderless component that wraps any textarea to resize it based on its content. 4 | 5 | ![Demo screenshot](https://user-images.githubusercontent.com/3642397/39928168-e55fc4fa-5534-11e8-8f13-a274910b7d6f.gif) 6 | 7 | ## Demo 8 | 9 | * [CodePen](https://codepen.io/lorisleiva/pen/XqqKKP) 10 | * [Blog article](http://lorisleiva.com/renderless-resizable-textarea) 11 | 12 | ## Installation 13 | 14 | Copy/paste the code in a new `ResizableTextarea.js` file. 15 | 16 | Add it to any component that needs it. 17 | ```js 18 | import ResizableTextarea from './path/to/ResizableTextarea.js' 19 | 20 | export default { 21 | components: { ResizableTextarea }, 22 | } 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```html 28 | 29 | 30 | 31 | ``` 32 | -------------------------------------------------------------------------------- /components/resizable-textarea/ResizableTextarea.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | resizeTextarea (event) { 4 | event.target.style.height = 'auto' 5 | event.target.style.height = (event.target.scrollHeight) + 'px' 6 | }, 7 | }, 8 | mounted () { 9 | this.$nextTick(() => { 10 | this.$el.setAttribute('style', 'height:' + (this.$el.scrollHeight) + 'px;overflow-y:hidden;') 11 | }) 12 | 13 | this.$el.addEventListener('input', this.resizeTextarea) 14 | }, 15 | beforeDestroy () { 16 | this.$el.removeEventListener('input', this.resizeTextarea) 17 | }, 18 | render () { 19 | return this.$slots.default[0] 20 | }, 21 | } -------------------------------------------------------------------------------- /directives/dragdrop/Dragdrop.js: -------------------------------------------------------------------------------- 1 | // npm install dragula -D 2 | import dragula from 'dragula'; 3 | 4 | // npm install lodash -D (Only used in the `reorder` helper method) 5 | import { set } from 'lodash'; 6 | 7 | // Map drake instances to their data structure globally in order to destroy them later. 8 | let arrays = []; 9 | let drakes = []; 10 | 11 | export default { 12 | // When the directive is first bound to the container. 13 | bind (container, binding, vnode) { 14 | 15 | // Get the `order` attribute if it exists. 16 | let orderProperty = vnode.data.attrs ? vnode.data.attrs.order : undefined; 17 | 18 | // Get the `options` attribute if it exists. 19 | let options = vnode.data.attrs ? vnode.data.attrs.options : undefined; 20 | 21 | // Get the array of items to update. 22 | let items = binding.value || []; 23 | 24 | // Keep track of the last dragging index to do some reordering. 25 | let dragIndex; 26 | 27 | // Use dragula on the container. 28 | let drake = dragula([container], options) 29 | 30 | // When we drag an item, memorize its index. 31 | .on('drag', (el, source) => { 32 | dragIndex = findDomIndex(source, el); 33 | }) 34 | 35 | // When we drop an item, reorder the array and update the order properties. 36 | .on('drop', (el, target) => { 37 | 38 | // Move the dragged and dropped item from `dragIndex` to the new index. 39 | move(items, dragIndex, findDomIndex(target, el)); 40 | 41 | // If the container has a `order` attribute, use it to reorder them. 42 | if (orderProperty) reorder(items, orderProperty); 43 | }); 44 | 45 | // Map the items with the drake instance. 46 | addDrake(items, drake); 47 | }, 48 | 49 | // When the directive is unbound from the container. 50 | unbind (container, binding, vnode) { 51 | 52 | // Retrieve the drake instance and kill it. 53 | let drake = getDrake(binding.value); 54 | if (drake) drake.destroy(); 55 | } 56 | } 57 | 58 | /** 59 | * Find the index of a DOM element within a given container. 60 | */ 61 | function findDomIndex(container, el) { 62 | return Array.prototype.indexOf.call(container.children, el); 63 | } 64 | 65 | /** 66 | * Move an array item from one index to another. 67 | * The given array is transformed, not returned. 68 | */ 69 | function move(array, fromIndex, toIndex) { 70 | array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); 71 | } 72 | 73 | /** 74 | * Reorder the items of an array from 0 to `array.length`. 75 | * The new order is stored on the given `orderProperty`. 76 | * The given array is transformed, not returned. 77 | */ 78 | function reorder(array, orderProperty) { 79 | let newOrder = 0; 80 | array.forEach(item => { set(item, orderProperty, newOrder++) }) 81 | } 82 | 83 | /** 84 | * Register a drake instance based on the reference of the given array. 85 | */ 86 | function addDrake(array, drake) { 87 | if (arrays.indexOf(array) >= 0) return; 88 | arrays.push(array); 89 | drakes.push(drake); 90 | } 91 | 92 | /** 93 | * Retrieve a drake instance based on the reference of the given array. 94 | */ 95 | function getDrake(array) { 96 | let drakeIndex = arrays.indexOf(array); 97 | if (drakeIndex >= 0) return drakes[drakeIndex]; 98 | } -------------------------------------------------------------------------------- /directives/dragdrop/README.md: -------------------------------------------------------------------------------- 1 | # :arrow_up_down: Dragdrop directive 2 | 3 | Directive that uses [Dragula](https://github.com/bevacqua/dragula) to reorder your lists using drag&drop. A drag&drop will: 4 | * Reorders the DOM elements (by Dragula) 5 | * Reorders the VueJS array that renders those elements 6 | * Assign the `order` property of all items of that array to match the new order 7 | 8 | ![Demo screenshot](https://user-images.githubusercontent.com/3642397/36781978-bcc2f378-1c77-11e8-894c-cfec19f93f8a.png) 9 | 10 | ## Demo 11 | 12 | * [CodePen](https://codepen.io/lorisleiva/pen/JpeBdr) 13 | * [Blog article](http://lorisleiva.com/drag-drop-made-easy) 14 | 15 | ## Installation 16 | 17 | Install dependencies 18 | ``` 19 | npm install dragula -D 20 | npm install lodash -D 21 | ``` 22 | Copy/paste the code in a new `Dragdrop.js` file. 23 | 24 | Add it to any component that needs it. 25 | ```js 26 | import Dragdrop from './path/to/Dragdrop.js' 27 | 28 | export default { 29 | directives: { Dragdrop }, 30 | } 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```html 36 |
37 |
41 |
42 | ``` 43 | 44 | ## Options 45 | 46 | | Attribute | Default | Description | 47 | | - | - | - | 48 | | `v-dragdrop="array"` | **required** | Tells the directive to initialize Dragula on this container and to update our `array` accordingly. | 49 | | `order` | *No order update* | Tells the directive which property of our items keeps track of its order. You can use dot notation to provide a nested property. E.g. `order="order"` will update `chapter.order` whilst `order="meta.order"` will update `chapter.meta.order`. | 50 | | `options` | `{}` | Provides Dragula options. | 51 | --------------------------------------------------------------------------------