├── 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 |
3 |
4 |
8 |
9 |
10 |
15 |
16 |
22 |
23 |
24 |
30 |
36 |
37 |
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 | 
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 | 
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 |
2 |
3 |
12 |
13 |
14 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------