├── .editorconfig
├── .eslintrc
├── .gitignore
├── .travis.yml
├── README.md
├── __tests__
├── setup.ts
└── vuequery.spec.ts
├── dist
├── .gitkeep
├── vuequery.js
└── vuequery.min.js
├── jest.config.js
├── package.json
├── rollup.config.js
├── src
├── helpers.ts
├── index.ts
└── vuequery.ts
├── tsconfig.json
├── types.d.ts
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | indent_size = 2
4 | indent_style = space
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 |
8 | [*.md]
9 | trim_trailing_whitespace = false
10 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "plugin:@typescript-eslint/recommended"
5 | ],
6 | "parserOptions": {
7 | "ecmaVersion": 2020,
8 | "sourceType": "module"
9 | },
10 | "rules": {
11 | "@typescript-eslint/member-delimiter-style": 0,
12 | "@typescript-eslint/ban-ts-ignore": 0,
13 | "@typescript-eslint/no-explicit-any": 0
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: [10, 12, 13, 14]
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VueQuery [](https://travis-ci.org/phanan/vuequery)
2 |
3 | > Traverse [Vue](https://vuejs.org)'s component tree with ease.
4 |
5 | > IMPORTANT: Vue's reactivity/event system is extremely powerful and flexible, and should have 99.99% of your use cases covered. In fact, having to traverse the component tree _almost always_ means you're doing Vue wrong. There are certain edge cases, however, when such is required, and this library aims to aid you there.
6 |
7 | ## Installation & Usage
8 |
9 | > This plugin is only tested for Vue 2.
10 |
11 | ### Installation
12 |
13 | #### Browser
14 | Include `vuequery.min.js` as a script:
15 |
16 | ```html
17 |
18 | ```
19 |
20 | Now `VueQuery` should be avaiable as a global function.
21 |
22 | #### Node.js
23 |
24 | First, require VueQuery as a dependency with `npm` or `yarn`:
25 |
26 | ``` bash
27 | npm install vuequery
28 | yarn add vuequery
29 | ```
30 |
31 | Then, import it:
32 |
33 | ``` js
34 | import VueQuery from 'vuequery'
35 | // You can also alias the import as $ for a jQuery-like experience
36 | import $ from 'vuequery'
37 | ```
38 |
39 | ### Usage
40 |
41 | Similar to jQuery, calling `VueQuery()` on a Vue component returns a VueQuery instance which exposes several API's to let you traverse through Vue's component tree.
42 |
43 | ``` js
44 | // assuming we're currently in a Vue component context
45 | // init a VueQuery instance on the current component
46 | const $vm = VueQuery(this)
47 |
48 | // get the original Vue component
49 | $vm.vm
50 |
51 | // get the immediate next sibling of the component
52 | $vm.next()
53 |
54 | // chaining is supported
55 | $vm.prev('foo').children()[0].find('bar')
56 | ```
57 |
58 | ## API
59 |
60 | > As VueQuery is heavily inspired by jQuery, its API signatures are very similar (albeit much less sophisticated) to that of jQuery's traversing module. In fact, the documentation here is more or less copied from [jQuery's](http://api.jquery.com/). Differences, if any, will be explicitly specified.
61 |
62 | ### `children([selector])`
63 |
64 | **Description**: Get the children of the current component, optionally filtered by a `selector`, which can be either
65 |
66 | * a string, in which case it will match the components by name. Obviously, for this to work, your components should have the `name` option. This is a good Vue practice anyway.
67 | * a Vue instance
68 | * a VueQuery instance, in which case it will match the encapsulated Vue instance
69 |
70 | **Return Values**: `Array.|[]`
71 |
72 | ### `closest(selector)`
73 |
74 | **Description**: Get the first component that matches the selector by testing the current component itself and traversing up through its ancestors in the component tree.
75 |
76 | **Return Values**: `VueQuery|null`
77 |
78 | ### `find(selector)`
79 |
80 | **Description**: Get the descendants of the current component, filtered by a selector.
81 |
82 | **Return Values**: `Array.|[]`
83 |
84 | ### `has(selector)`
85 |
86 | > Note: This API's behavior is different from its jQuery counterpart.
87 |
88 | **Description**: Check if the current component has any component that matches the selector in its descendant tree.
89 |
90 | **Return Values**: `Boolean`
91 |
92 | ### `is(selector)`
93 |
94 | **Description**: Check if the current component matches the selector.
95 |
96 | **Return Values**: `Boolean`
97 |
98 | ### `next([selector])`
99 |
100 | **Description**: Get the immediately following sibling of the current component. If a selector is provided, it retrieves the next sibling only if it matches that selector.
101 |
102 | **Return Values**: `VueQuery|null`
103 |
104 | ### `nextAll([selector])`
105 |
106 | **Description**: Get all following siblings of the current component, optionally filtered by a selector.
107 |
108 | **Return Values**: `Array.|[]`
109 |
110 | ### `nextUntil([selector][, filter])`
111 |
112 | **Description**: Get all following siblings of the current component, up to but not including the component matched by the selector passed.
113 | If the selector is not matched or is not supplied, all following siblings will be selected; in these cases it selects the same components as the `.nextAll()` method does when no selector is provided.
114 |
115 | The method optionally accepts a `filter` expression for its second argument. If this argument is supplied, the components will be filtered by testing whether they match it.
116 |
117 | **Return Values**: `Array.|[]`
118 |
119 | ### `parent([selector])`
120 |
121 | **Description**: Get the parent of the current component. If the selector is supplied, the parent component will only be returned if it matches it.
122 |
123 | **Return Values**: `VueQuery|null`
124 |
125 | ### `parents([selector])`
126 |
127 | **Description**: Get the ancestors of the current component, optionally filtered by a selector.
128 |
129 | **Return Values**: `Array.|[]`
130 |
131 | ### `parentsUntil([selector][, filter])`
132 |
133 | **Description**: Get the ancestors of the current component, up to but not including the component matched by the selector passed.
134 | If the selector is not matched or is not supplied, all ancestors will be selected; in these cases it selects the same components as the `.parents()` method does when no selector is provided.
135 |
136 | The method optionally accepts a `filter` expression for its second argument. If this argument is supplied, the components will be filtered by testing whether they match it.
137 |
138 | **Return Values**: `Array.|[]`
139 |
140 | ### `prev([selector])`
141 |
142 | **Description**: Get the immediately preceding sibling of the current component. If a selector is provided, it retrieves the previous sibling only if it matches that selector.
143 |
144 | **Return Values**: `VueQuery|null`
145 |
146 | ### `prevAll([selector])`
147 |
148 | **Description**: Get all preceding siblings of the current component, optionally filtered by a selector.
149 |
150 | **Return Values**: `Array.|[]`
151 |
152 | ### `prevUntil([selector][, filter])`
153 |
154 | **Description**: Get all preceding siblings of the current component, up to but not including the component matched by the selector passed.
155 | If the selector is not matched or is not supplied, all preceding siblings will be selected; in these cases it selects the same components as the `.nextAll()` method does when no selector is provided.
156 |
157 | The method optionally accepts a `filter` expression for its second argument. If this argument is supplied, the components will be filtered by testing whether they match it.
158 |
159 | ### `siblings([selector])`
160 |
161 | **Description**: Get the siblings of the current component, optionally filtered by a selector. The original component is not included among the siblings.
162 |
163 | **Return Values**: `Array.|[]`
164 |
165 | ## License
166 |
167 | MIT © [Phan An](http://phanan.net)
168 |
--------------------------------------------------------------------------------
/__tests__/setup.ts:
--------------------------------------------------------------------------------
1 | const globalVue = require('vue/dist/vue.js')
2 |
3 | globalVue.component('noop', {
4 | template: '',
5 | })
6 |
7 | globalVue.component('foo', {
8 | name: 'foo',
9 | template: 'This is foo
',
10 | })
11 |
12 | globalVue.component('bar', {
13 | name: 'bar',
14 | template: '
',
15 | })
16 |
17 | globalVue.component('baz', {
18 | name: 'baz',
19 | template: '
',
20 | })
21 |
22 | globalVue.component('qux', {
23 | name: 'qux',
24 | template: '
',
25 | })
26 |
--------------------------------------------------------------------------------
/__tests__/vuequery.spec.ts:
--------------------------------------------------------------------------------
1 | const globalVue = require('vue/dist/vue.js')
2 | import { VueQuery } from '../types'
3 | import $ from '../src/vuequery'
4 |
5 | describe('VueQuery', () => {
6 | let $vm: VueQuery
7 |
8 | beforeEach(() => {
9 | $vm = $(
10 | new globalVue({
11 | el: document.createElement('div'),
12 | template: '
',
13 | }).$mount()
14 | ) as VueQuery
15 | })
16 |
17 | describe('common', () => {
18 | it('doesnt init on a VueQuery instance', () => {
19 | expect($($vm)).toBe($vm)
20 | })
21 | })
22 |
23 | describe('children()', () => {
24 | it('matches existing children', () => {
25 | expect($vm.children()).toHaveLength(1) //
26 | expect($vm.children()[0].children()).toHaveLength(4) //
27 | })
28 |
29 | it('does not match non-existing children', () => {
30 | expect($vm.children('cuckoo')).toHaveLength(0)
31 | })
32 | })
33 |
34 | describe('find()', () => {
35 | it('finds direct descendants', () => {
36 | expect($vm.find('qux')).toHaveLength(1)
37 | })
38 |
39 | it('finds distant descendants', () => {
40 | expect($vm.find('bar')).toHaveLength(3)
41 | })
42 |
43 | it('doesnt find non-existing name', () => {
44 | expect($vm.find('cuckoo')).toHaveLength(0)
45 | })
46 | })
47 |
48 | describe('closest()', () => {
49 | it('finds closest to be self', () => {
50 | expect($vm.find('qux')[0].closest('qux')!.is('qux')).toBe(true)
51 | })
52 |
53 | it('finds closest to be parent', () => {
54 | expect($vm.find('foo')[0].closest('bar')!.is('bar')).toBe(true)
55 | })
56 |
57 | it('finds closest to be ancestor', () => {
58 | expect($vm.find('foo')[0].closest('qux')!.is('qux')).toBe(true)
59 | })
60 |
61 | it('doesnt find invalid closest', () => {
62 | expect($vm.find('foo')[0].closest('cuckoo')).toBeNull()
63 | })
64 | })
65 |
66 | describe('has()', () => {
67 | it('has direct descendant', () => {
68 | expect($vm.has('qux')).toBe(true)
69 | })
70 |
71 | it('has distant descendant', () => {
72 | expect($vm.has('bar')).toBe(true)
73 | })
74 |
75 | it('doesnt have invalid descendant', () => {
76 | expect($vm.has('cuckoo')).toBe(false)
77 | })
78 | })
79 |
80 | describe('is()', () => {
81 | it('is itself', () => {
82 | expect($vm.is($vm)).toBe(true)
83 | })
84 |
85 | it('is of a name', () => {
86 | expect($vm.find('qux')[0].is('qux')).toBe(true)
87 | })
88 | })
89 |
90 | describe('next()', () => {
91 | it('finds next without name', () => {
92 | expect($vm.find('baz')[0].next()!.is('noop')).toBe(true)
93 | })
94 |
95 | it('finds next with name', () => {
96 | expect($vm.find('baz')[0].next('noop')!.is('noop')).toBe(true)
97 | })
98 |
99 | it('doesnt find next with invalid name', () => {
100 | expect($vm.find('baz')[0].next('baz')).toBeNull()
101 | })
102 |
103 | it('doesnt find next at end', () => {
104 | expect($vm.find('baz')[2].next()).toBeNull()
105 | })
106 |
107 | it('doesnt find next with invalid name', () => {
108 | expect($vm.find('baz')[0].next('cuckoo')).toBeNull()
109 | })
110 | })
111 |
112 | describe('nextAll()', () => {
113 | it('finds all next without name', () => {
114 | const collected = $vm.find('baz')[0].nextAll()
115 | expect(collected).toHaveLength(3)
116 | expect(collected[0].is('noop')).toBe(true)
117 | expect(collected[1].is('baz')).toBe(true)
118 | expect(collected[2].is('baz')).toBe(true)
119 | })
120 |
121 | it('finds all next with name', () => {
122 | const collected = $vm.find('baz')[0].nextAll('baz')
123 | expect(collected).toHaveLength(2)
124 | expect(collected[0].is('baz')).toBe(true)
125 | expect(collected[1].is('baz')).toBe(true)
126 | })
127 |
128 | it('doesnt find any next with invalid name', () => {
129 | const collected = $vm.find('baz')[0].nextAll('cuckoo')
130 | expect(collected).toHaveLength(0)
131 | })
132 | })
133 |
134 | describe('nextUntil()', () => {
135 | it('finds all next until', () => {
136 | const collected = $vm.find('baz')[0].nextUntil('baz')
137 | expect(collected).toHaveLength(1)
138 | expect(collected[0].is('noop')).toBe(true)
139 | })
140 |
141 | it('finds no next until', () => {
142 | expect($vm.find('baz')[0].nextUntil('baz', 'baz')).toHaveLength(0)
143 | })
144 | })
145 |
146 | describe('parent()', () => {
147 | it('finds parent', () => {
148 | expect($vm.find('baz')[0].parent()!.is('qux'))
149 | })
150 |
151 | it('finds parent with name specified', () => {
152 | expect($vm.find('baz')[0].parent('qux')!.is('qux')).toBe(true)
153 | })
154 |
155 | it('doesnt find parent with invalid name specified', () => {
156 | expect($vm.find('baz')[0].parent('cuckoo')).toBeNull()
157 | })
158 |
159 | it('doesnt find parent of root', () => {
160 | expect($vm.parent()).toBeNull()
161 | })
162 | })
163 |
164 | describe('parents()', () => {
165 | it('finds direct parents', () => {
166 | const parents = $vm.find('foo')[0].parents('bar')
167 | expect(parents).toHaveLength(1)
168 | expect(parents[0].is('bar')).toBe(true)
169 | })
170 |
171 | it('finds distant parents', () => {
172 | const parents = $vm.find('foo')[0].parents('baz')
173 | expect(parents).toHaveLength(1)
174 | expect(parents[0].is('baz')).toBe(true)
175 | })
176 |
177 | it('find parents without name specified', () => {
178 | const parents = $vm.find('foo')[0].parents()
179 | expect(parents).toHaveLength(4)
180 | expect(parents[0].is('bar')).toBe(true)
181 | expect(parents[1].is('baz')).toBe(true)
182 | expect(parents[2].is('qux')).toBe(true)
183 | expect(parents[3].vm === parents[3].vm.$root).toBe(true)
184 | })
185 |
186 | it('doesnt find parents with invalid name specified', () => {
187 | expect($vm.find('baz')[0].parents('cuckoo')).toHaveLength(0)
188 | })
189 |
190 | it('doesnt find parents of root', () => {
191 | expect($vm.parents()).toHaveLength(0)
192 | })
193 | })
194 |
195 | describe('parentsUntil()', () => {
196 | it('finds parents until', () => {
197 | const parents = $vm.find('foo')[0].parentsUntil('qux')
198 | expect(parents).toHaveLength(2)
199 | expect(parents[0].is('bar')).toBe(true)
200 | expect(parents[1].is('baz')).toBe(true)
201 | })
202 |
203 | it('doesnt finds parents of root', () => {
204 | expect($vm.parentsUntil()).toHaveLength(0)
205 | })
206 | })
207 |
208 | describe('prev()', () => {
209 | it('finds prev without name', () => {
210 | expect($vm.find('baz')[1].prev()!.is('noop')).toBe(true)
211 | })
212 |
213 | it('finds prev with name', () => {
214 | expect($vm.find('baz')[1].prev('noop')!.is('noop')).toBe(true)
215 | })
216 |
217 | it('doesnt find prev with invalid name', () => {
218 | expect($vm.find('baz')[1].prev('baz')).toBeNull()
219 | })
220 |
221 | it('doesnt find prev at beginning', () => {
222 | expect($vm.find('baz')[0].prev()).toBeNull()
223 | })
224 |
225 | it('doesnt find prev with invalid name', () => {
226 | expect($vm.find('baz')[1].prev('cuckoo')).toBeNull()
227 | })
228 | })
229 |
230 | describe('prevAll()', () => {
231 | it('finds all prev without name', () => {
232 | const collected = $vm.find('baz')[2].prevAll()
233 | expect(collected).toHaveLength(3)
234 | expect(collected[0].is('baz')).toBe(true)
235 | expect(collected[1].is('noop')).toBe(true)
236 | expect(collected[2].is('baz')).toBe(true)
237 | })
238 |
239 | it('finds all prev with name', () => {
240 | const collected = $vm.find('baz')[2].prevAll('baz')
241 | expect(collected).toHaveLength(2)
242 | expect(collected[0].is('baz')).toBe(true)
243 | expect(collected[1].is('baz')).toBe(true)
244 | })
245 |
246 | it('doesnt find any prev with invalid name', () => {
247 | expect($vm.find('baz')[2].prevAll('cuckoo')).toHaveLength(0)
248 | })
249 | })
250 |
251 | describe('prevUntil()', () => {
252 | it('finds all prev until', () => {
253 | const collected = $vm.find('baz')[1].prevUntil('baz')
254 | expect(collected).toHaveLength(1)
255 | expect(collected[0].is('noop')).toBe(true)
256 | })
257 |
258 | it('finds no prev until', () => {
259 | expect($vm.find('baz')[1].nextUntil('baz', 'baz')).toHaveLength(0)
260 | })
261 | })
262 |
263 | describe('siblings()', () => {
264 | it('finds all siblings', () => {
265 | const me = $vm.find('baz')[1]
266 | const collected = me.siblings()
267 | expect(collected).toHaveLength(3)
268 | expect(collected).not.toContain(me)
269 | })
270 | })
271 | })
272 |
--------------------------------------------------------------------------------
/dist/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phanan/vuequery/1696b6d4b94c2e4b0cc1cf508be7b95ba05461f7/dist/.gitkeep
--------------------------------------------------------------------------------
/dist/vuequery.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function isVueQuery(selector) {
4 | return selector._isVueQuery === true;
5 | }
6 | /**
7 | * Checks if a Vue component matches against a selector.
8 | * The selector can be either:
9 | * - a string (name of the component)
10 | * - a VueComponent object
11 | * - a VueQuery object
12 | */
13 | var matches = function (vm, selector) {
14 | if (!selector) {
15 | return true;
16 | }
17 | if (selector instanceof String || typeof selector === 'string') {
18 | return vm.$options.name === selector;
19 | }
20 | if (isVueQuery(selector)) {
21 | return selector.vm === vm;
22 | }
23 | return selector === vm;
24 | };
25 | /**
26 | * Get all children of a Vue component, optionally filtered by a selector.
27 | */
28 | var rawChildren = function (vm, selector) {
29 | var collected = [];
30 | vm.$children.forEach(function (child) {
31 | if (matches(child, selector)) {
32 | collected.push(child);
33 | }
34 | });
35 | return collected;
36 | };
37 | /**
38 | * Get all siblings of a Vue component, optionally filtered by a selector.
39 | * @param {Boolean} withSelf Whether to include the current vm
40 | */
41 | var rawSiblings = function (vm, selector, withSelf) {
42 | if (withSelf === void 0) { withSelf = true; }
43 | if (isRoot(vm)) {
44 | return [];
45 | }
46 | var collected = rawChildren(vm.$parent, selector);
47 | if (!withSelf) {
48 | var index = collected.indexOf(vm);
49 | if (index > -1) {
50 | collected.splice(index, 1);
51 | }
52 | }
53 | return collected;
54 | };
55 | /**
56 | * Check if a Vue component is the root instance.
57 | */
58 | var isRoot = function (vm) {
59 | return vm.$root === vm;
60 | };
61 | /**
62 | * Get the first sibling of a Vue component filtered by a selector.
63 | */
64 | var siblingsWithSort = function (vm, selector, direction) {
65 | var siblings = rawSiblings(vm);
66 | if (!siblings.length) {
67 | return null;
68 | }
69 | var i = siblings.indexOf(vm);
70 | if (direction === "Ascending") {
71 | if (i === siblings.length - 1) {
72 | return null;
73 | }
74 | return matches(siblings[i + 1], selector) ? siblings[i + 1] : null;
75 | }
76 | if (i === 0) {
77 | return null;
78 | }
79 | return matches(siblings[i - 1], selector) ? siblings[i - 1] : null;
80 | };
81 | /**
82 | * Get all siblings of a Vue component filtered by a selector.
83 | */
84 | var siblingsAllWithSort = function (vm, selector, direction) {
85 | var siblings = rawSiblings(vm, selector);
86 | if (!siblings.length) {
87 | return [];
88 | }
89 | var i = siblings.indexOf(vm);
90 | return direction === "Ascending"
91 | ? siblings.slice(i + 1)
92 | : siblings.slice(0, i).reverse();
93 | };
94 | /**
95 | * Get all siblings of a Vue component filtered by a "filter" selector, and up to but
96 | * not including the "until" selector passed.
97 | */
98 | var siblingsUntilWithSort = function (vm, until, filter, direction) {
99 | var siblings = rawSiblings(vm);
100 | if (!siblings.length) {
101 | return [];
102 | }
103 | if (direction === "Descending") {
104 | siblings = siblings.reverse();
105 | }
106 | siblings = siblings.slice(siblings.indexOf(vm) + 1);
107 | if (!until) {
108 | return siblings;
109 | }
110 | var collected = [];
111 | for (var i = 0, j = siblings.length; i < j; ++i) {
112 | if (matches(siblings[i], until)) {
113 | break;
114 | }
115 | if (matches(siblings[i], filter)) {
116 | collected.push(siblings[i]);
117 | }
118 | }
119 | return collected;
120 | };
121 |
122 | var $ = function (vm) {
123 | if (Array.isArray(vm)) {
124 | // If the argument is an array, we VueQuery'fy the elements.
125 | return vm.map(function (c) {
126 | return $(c);
127 | });
128 | }
129 | // Avoid double encapsulation
130 | if (isVueQuery(vm)) {
131 | return vm;
132 | }
133 | // @ts-ignore
134 | if (!vm._isVue) {
135 | throw new Error('[VueQuery] Cannot instantiate on a non-Vue element');
136 | }
137 | if (!isRoot(vm) && !vm.$options.name) {
138 | throw new Error('[VueQuery] Non-root component must have a `name` option');
139 | }
140 | return {
141 | vm: vm,
142 | _isVueQuery: true,
143 | children: function (selector) {
144 | return $(rawChildren(vm, selector));
145 | },
146 | closest: function (selector) {
147 | if (matches(vm, selector)) {
148 | return $(vm);
149 | }
150 | if (!vm.$parent) {
151 | return null;
152 | }
153 | var $parent = $(vm.$parent);
154 | return $parent ? $parent.closest(selector) : null;
155 | },
156 | find: function (selector) {
157 | var _find = function (component) {
158 | var collected = [];
159 | if (!component.$children || !component.$children.length) {
160 | return [];
161 | }
162 | component.$children.forEach(function (c) {
163 | if (matches(c, selector)) {
164 | collected.push(c);
165 | }
166 | collected = collected.concat(_find(c));
167 | });
168 | return collected;
169 | };
170 | return $(_find(vm));
171 | },
172 | has: function (selector) {
173 | if (!vm.$children || !vm.$children.length) {
174 | return false;
175 | }
176 | for (var i = 0, j = vm.$children.length; i < j; ++i) {
177 | if (matches(vm.$children[i], selector)) {
178 | return true;
179 | }
180 | var $c = $(vm.$children[i]);
181 | if (!$c) {
182 | break;
183 | }
184 | return $c.has(selector);
185 | }
186 | return false;
187 | },
188 | is: function (selector) {
189 | if (Array.isArray(selector)) {
190 | return selector.some(function (item) { return matches(vm, item); });
191 | }
192 | return matches(vm, selector);
193 | },
194 | next: function (selector) {
195 | var _next = siblingsWithSort(vm, selector ? selector : null, 'Ascending');
196 | return _next ? $(_next) : null;
197 | },
198 | nextAll: function (selector) {
199 | return $(siblingsAllWithSort(vm, selector ? selector : null, 'Ascending'));
200 | },
201 | nextUntil: function (until, filter) {
202 | return $(siblingsUntilWithSort(vm, until, filter, 'Ascending'));
203 | },
204 | parent: function (selector) {
205 | if (isRoot(vm)) {
206 | return null;
207 | }
208 | return matches(vm.$parent, selector) ? $(vm.$parent) : null;
209 | },
210 | parents: function (selector) {
211 | var _parents = function (component) {
212 | if (isRoot(component)) {
213 | return [];
214 | }
215 | var collected = [];
216 | if (matches(component.$parent, selector)) {
217 | collected.push(component.$parent);
218 | }
219 | collected = collected.concat(_parents(component.$parent));
220 | return collected;
221 | };
222 | return $(_parents(vm));
223 | },
224 | parentsUntil: function (until, filter) {
225 | var _parentsUntil = function (component) {
226 | if (isRoot(component)) {
227 | return [];
228 | }
229 | var collected = [];
230 | if (until && matches(component.$parent, until)) {
231 | return collected;
232 | }
233 | if (matches(component.$parent, filter)) {
234 | collected.push(component.$parent);
235 | }
236 | return collected.concat(_parentsUntil(component.$parent));
237 | };
238 | return $(_parentsUntil(vm));
239 | },
240 | prev: function (selector) {
241 | var _prev = siblingsWithSort(vm, selector ? selector : null, 'Descending');
242 | return _prev ? $(_prev) : null;
243 | },
244 | prevAll: function (selector) {
245 | return $(siblingsAllWithSort(vm, selector ? selector : null, 'Descending'));
246 | },
247 | prevUntil: function (until, filter) {
248 | return $(siblingsUntilWithSort(vm, until, filter, 'Descending'));
249 | },
250 | siblings: function (selector) {
251 | var children = rawSiblings(vm, selector);
252 | var index = children.indexOf(vm);
253 | if (index > -1) {
254 | children.splice(index, 1);
255 | }
256 | return $(children);
257 | }
258 | };
259 | };
260 |
261 | module.exports = $;
262 |
--------------------------------------------------------------------------------
/dist/vuequery.min.js:
--------------------------------------------------------------------------------
1 | var VueQuery=function(){"use strict";function n(n){return!0===n._isVueQuery}var r=function(r,e){return!e||(e instanceof String||"string"==typeof e?r.$options.name===e:n(e)?e.vm===r:e===r)},e=function(n,e){var t=[];return n.$children.forEach((function(n){r(n,e)&&t.push(n)})),t},t=function(n,r,t){if(void 0===t&&(t=!0),u(n))return[];var i=e(n.$parent,r);if(!t){var c=i.indexOf(n);c>-1&&i.splice(c,1)}return i},u=function(n){return n.$root===n},i=function(n,e,u){var i=t(n);if(!i.length)return null;var c=i.indexOf(n);return"Ascending"===u?c===i.length-1?null:r(i[c+1],e)?i[c+1]:null:0===c?null:r(i[c-1],e)?i[c-1]:null},c=function(n,r,e){var u=t(n,r);if(!u.length)return[];var i=u.indexOf(n);return"Ascending"===e?u.slice(i+1):u.slice(0,i).reverse()},o=function(n,e,u,i){var c=t(n);if(!c.length)return[];if("Descending"===i&&(c=c.reverse()),c=c.slice(c.indexOf(n)+1),!e)return c;for(var o=[],l=0,a=c.length;l-1&&r.splice(e,1),l(r)}}};return l}();
2 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | setupFilesAfterEnv: ['/__tests__/setup.ts'],
4 | testMatch: ['**/__tests__/**/*.spec.ts']
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vuequery",
3 | "version": "2.1.1",
4 | "description": "Traverse Vue's component tree with ease",
5 | "main": "dist/vuequery.js",
6 | "scripts": {
7 | "lint": "eslint",
8 | "test": "eslint && jest",
9 | "build": "rollup -c"
10 | },
11 | "keywords": [
12 | "vue",
13 | "vue.js",
14 | "vuejs",
15 | "component",
16 | "components",
17 | "traverse",
18 | "query"
19 | ],
20 | "author": "Phan An (https://phanan.net)",
21 | "license": "MIT",
22 | "types": "./types.d.ts",
23 | "peerDependencies": {
24 | "vue": "~2.5.0"
25 | },
26 | "devDependencies": {
27 | "@rollup/plugin-typescript": "^4.1.1",
28 | "@types/jest": "^25.2.2",
29 | "@typescript-eslint/eslint-plugin": "^2.33.0",
30 | "@typescript-eslint/parser": "^2.33.0",
31 | "eslint": "^7.0.0",
32 | "jest": "^26.0.1",
33 | "rollup": "^2.10.2",
34 | "rollup-plugin-terser": "^5.3.0",
35 | "ts-jest": "^26.0.0",
36 | "typescript": "^3.9.2",
37 | "vue": "~2.5.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript'
2 | import { terser } from 'rollup-plugin-terser'
3 |
4 | export default {
5 | input: 'src/index.ts',
6 | output: [
7 | {
8 | file: 'dist/vuequery.js',
9 | format: 'cjs'
10 | },
11 | {
12 | file: 'dist/vuequery.min.js',
13 | format: 'iife',
14 | plugins: [terser()],
15 | name: 'VueQuery'
16 | }
17 | ],
18 | external: ['vue'],
19 | plugins: [typescript()]
20 | }
21 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { Selector, VueQuery, SortDirection } from '../types'
3 |
4 | export function isVueQuery(selector: Selector): selector is VueQuery {
5 | return (selector as VueQuery)._isVueQuery === true
6 | }
7 |
8 | /**
9 | * Checks if a Vue component matches against a selector.
10 | * The selector can be either:
11 | * - a string (name of the component)
12 | * - a VueComponent object
13 | * - a VueQuery object
14 | */
15 | export const matches = (vm: Vue, selector?: Selector | null): boolean => {
16 | if (!selector) {
17 | return true
18 | }
19 |
20 | if (selector instanceof String || typeof selector === 'string') {
21 | return vm.$options.name === selector
22 | }
23 |
24 | if (isVueQuery(selector)) {
25 | return selector.vm === vm
26 | }
27 |
28 | return selector === vm
29 | }
30 |
31 | /**
32 | * Get all children of a Vue component, optionally filtered by a selector.
33 | */
34 | export const rawChildren = (vm: Vue, selector?: Selector | null): Vue[] => {
35 | const collected: Vue[] = []
36 |
37 | vm.$children.forEach((child) => {
38 | if (matches(child, selector)) {
39 | collected.push(child)
40 | }
41 | })
42 |
43 | return collected
44 | }
45 |
46 | /**
47 | * Get all siblings of a Vue component, optionally filtered by a selector.
48 | * @param {Boolean} withSelf Whether to include the current vm
49 | */
50 | export const rawSiblings = (
51 | vm: Vue,
52 | selector?: Selector | null,
53 | withSelf = true
54 | ): Vue[] => {
55 | if (isRoot(vm)) {
56 | return []
57 | }
58 |
59 | const collected = rawChildren(vm.$parent, selector)
60 |
61 | if (!withSelf) {
62 | const index = collected.indexOf(vm)
63 |
64 | if (index > -1) {
65 | collected.splice(index, 1)
66 | }
67 | }
68 |
69 | return collected
70 | }
71 |
72 | /**
73 | * Check if a Vue component is the root instance.
74 | */
75 | export const isRoot = (vm: Vue): boolean => {
76 | return vm.$root === vm
77 | }
78 |
79 | /**
80 | * Get the first sibling of a Vue component filtered by a selector.
81 | */
82 | export const siblingsWithSort = (
83 | vm: Vue,
84 | selector: Selector | null,
85 | direction: SortDirection
86 | ): Vue | null => {
87 | const siblings = rawSiblings(vm)
88 |
89 | if (!siblings.length) {
90 | return null
91 | }
92 |
93 | const i = siblings.indexOf(vm)
94 |
95 | if (direction === "Ascending") {
96 | if (i === siblings.length - 1) {
97 | return null
98 | }
99 |
100 | return matches(siblings[i + 1], selector) ? siblings[i + 1] : null
101 | }
102 |
103 | if (i === 0) {
104 | return null
105 | }
106 |
107 | return matches(siblings[i - 1], selector) ? siblings[i - 1] : null
108 | }
109 |
110 | /**
111 | * Get all siblings of a Vue component filtered by a selector.
112 | */
113 | export const siblingsAllWithSort = (
114 | vm: Vue,
115 | selector: Selector | null,
116 | direction: SortDirection
117 | ): Vue[] => {
118 | const siblings = rawSiblings(vm, selector)
119 |
120 | if (!siblings.length) {
121 | return []
122 | }
123 |
124 | const i = siblings.indexOf(vm)
125 |
126 | return direction === "Ascending"
127 | ? siblings.slice(i + 1)
128 | : siblings.slice(0, i).reverse()
129 | }
130 |
131 | /**
132 | * Get all siblings of a Vue component filtered by a "filter" selector, and up to but
133 | * not including the "until" selector passed.
134 | */
135 | export const siblingsUntilWithSort = (
136 | vm: Vue,
137 | until: Selector | undefined,
138 | filter: Selector | undefined,
139 | direction: SortDirection
140 | ): Vue[] => {
141 | let siblings = rawSiblings(vm)
142 |
143 | if (!siblings.length) {
144 | return []
145 | }
146 |
147 | if (direction === "Descending") {
148 | siblings = siblings.reverse()
149 | }
150 |
151 | siblings = siblings.slice(siblings.indexOf(vm) + 1)
152 |
153 | if (!until) {
154 | return siblings
155 | }
156 |
157 | const collected = []
158 |
159 | for (let i = 0, j = siblings.length; i < j; ++i) {
160 | if (matches(siblings[i], until)) {
161 | break
162 | }
163 |
164 | if (matches(siblings[i], filter)) {
165 | collected.push(siblings[i])
166 | }
167 | }
168 |
169 | return collected
170 | }
171 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import $ from './vuequery'
2 |
3 | export default $
4 |
--------------------------------------------------------------------------------
/src/vuequery.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { VueQuery, Selector } from '../types'
3 |
4 | import {
5 | isRoot,
6 | matches,
7 | rawSiblings,
8 | rawChildren,
9 | siblingsWithSort,
10 | siblingsUntilWithSort,
11 | siblingsAllWithSort,
12 | isVueQuery
13 | } from './helpers'
14 |
15 | const $ = (vm: Vue | VueQuery | (Vue | VueQuery)[] | any): VueQuery | VueQuery[] => {
16 | if (Array.isArray(vm)) {
17 | // If the argument is an array, we VueQuery'fy the elements.
18 | return vm.map((c: Vue | VueQuery): VueQuery => {
19 | return $(c) as VueQuery
20 | })
21 | }
22 |
23 | // Avoid double encapsulation
24 | if (isVueQuery(vm)) {
25 | return vm
26 | }
27 |
28 | // @ts-ignore
29 | if (!vm._isVue) {
30 | throw new Error('[VueQuery] Cannot instantiate on a non-Vue element')
31 | }
32 |
33 | if (!isRoot(vm) && !vm.$options.name) {
34 | throw new Error('[VueQuery] Non-root component must have a `name` option')
35 | }
36 |
37 | return {
38 | vm,
39 | _isVueQuery: true,
40 |
41 | children (selector?: Selector): VueQuery[] {
42 | return $(rawChildren(vm, selector)) as VueQuery[]
43 | },
44 |
45 | closest (selector: Selector): VueQuery | null {
46 | if (matches(vm, selector)) {
47 | return $(vm) as VueQuery
48 | }
49 |
50 | if (!vm.$parent) {
51 | return null
52 | }
53 |
54 | const $parent = $(vm.$parent) as VueQuery
55 |
56 | return $parent ? $parent.closest(selector) : null
57 | },
58 |
59 | find (selector: Selector): VueQuery[] {
60 | const _find = (component: Vue): Vue[] => {
61 | let collected: Vue[] = []
62 |
63 | if (!component.$children || !component.$children.length) {
64 | return []
65 | }
66 |
67 | component.$children.forEach(c => {
68 | if (matches(c, selector)) {
69 | collected.push(c)
70 | }
71 |
72 | collected = collected.concat(_find(c))
73 | })
74 |
75 | return collected
76 | }
77 |
78 | return $(_find(vm)) as VueQuery[]
79 | },
80 |
81 | has (selector: Selector): boolean {
82 | if (!vm.$children || !vm.$children.length) {
83 | return false
84 | }
85 |
86 | for (let i = 0, j = vm.$children.length; i < j; ++i) {
87 | if (matches(vm.$children[i], selector)) {
88 | return true
89 | }
90 |
91 | const $c = $(vm.$children[i]) as VueQuery
92 |
93 | if (!$c) {
94 | break
95 | }
96 |
97 | return $c.has(selector)
98 | }
99 |
100 | return false
101 | },
102 |
103 | is (selector: Selector | (Extract)[]): boolean {
104 | if (Array.isArray(selector)) {
105 | return selector.some(item => matches(vm, item))
106 | }
107 |
108 | return matches(vm, selector)
109 | },
110 |
111 | next (selector?: Selector): VueQuery | null {
112 | const _next = siblingsWithSort(vm, selector ? selector : null, 'Ascending')
113 |
114 | return _next ? $(_next) as VueQuery : null
115 | },
116 |
117 | nextAll (selector?: Selector): VueQuery[] {
118 | return $(siblingsAllWithSort(vm, selector ? selector : null, 'Ascending')) as VueQuery[]
119 | },
120 |
121 | nextUntil (until?: Selector, filter?: Selector): VueQuery[] {
122 | return $(siblingsUntilWithSort(vm, until, filter, 'Ascending')) as VueQuery[]
123 | },
124 |
125 | parent (selector?: Selector): VueQuery | null {
126 | if (isRoot(vm)) {
127 | return null
128 | }
129 |
130 | return matches(vm.$parent, selector) ? $(vm.$parent) as VueQuery : null
131 | },
132 |
133 | parents (selector?: Selector): VueQuery[] {
134 | const _parents = (component: Vue): Vue[] => {
135 | if (isRoot(component)) {
136 | return []
137 | }
138 |
139 | let collected: Vue[] = []
140 |
141 | if (matches(component.$parent, selector)) {
142 | collected.push(component.$parent)
143 | }
144 |
145 | collected = collected.concat(_parents(component.$parent))
146 |
147 | return collected
148 | }
149 |
150 | return $(_parents(vm)) as VueQuery[]
151 | },
152 |
153 | parentsUntil (until?: Selector, filter?: Selector): VueQuery[] {
154 | const _parentsUntil = (component: Vue): Vue[] => {
155 | if (isRoot(component)) {
156 | return []
157 | }
158 |
159 | const collected: Vue[] = []
160 |
161 | if (until && matches(component.$parent, until)) {
162 | return collected
163 | }
164 |
165 | if (matches(component.$parent, filter)) {
166 | collected.push(component.$parent)
167 | }
168 |
169 | return collected.concat(_parentsUntil(component.$parent))
170 | }
171 |
172 | return $(_parentsUntil(vm)) as VueQuery[]
173 | },
174 |
175 | prev (selector?: Selector): VueQuery | null {
176 | const _prev = siblingsWithSort(vm, selector ? selector : null, 'Descending')
177 |
178 | return _prev ? $(_prev) as VueQuery : null
179 | },
180 |
181 | prevAll (selector?: Selector): VueQuery[] {
182 | return $(siblingsAllWithSort(vm, selector ? selector : null, 'Descending')) as VueQuery[]
183 | },
184 |
185 | prevUntil (until?: Selector, filter?: Selector): VueQuery[] {
186 | return $(siblingsUntilWithSort(vm, until, filter, 'Descending')) as VueQuery[]
187 | },
188 |
189 | siblings (selector?: Selector): VueQuery[] {
190 | const children = rawSiblings(vm, selector)
191 | const index = children.indexOf(vm)
192 |
193 | if (index > -1) {
194 | children.splice(index, 1)
195 | }
196 |
197 | return $(children) as VueQuery[]
198 | }
199 | }
200 | }
201 |
202 | export default $
203 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "strict": true,
5 | "module": "es2015",
6 | "moduleResolution": "node",
7 | "allowSyntheticDefaultImports": true,
8 | "noImplicitThis": false,
9 | "downlevelIteration": true,
10 | "esModuleInterop": true
11 | },
12 | "include": [
13 | "./**/*.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/types.d.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | export type Selector = string | Vue | VueQuery
4 |
5 | export type SortDirection = 'Ascending' | 'Descending'
6 |
7 | export interface VueQuery {
8 | vm: Vue
9 | _isVueQuery: true
10 |
11 | /**
12 | * Get the children of the current component, optionally filtered by a selector.
13 | */
14 | children(selector?: Selector): VueQuery[]
15 |
16 | /**
17 | * Get the first component that matches the selector by testing the current component itself
18 | * and traversing up through its ancestors in the component tree.
19 | */
20 | closest(selector: Selector): VueQuery | null
21 |
22 | /**
23 | * Get the descendants of the current component, filtered by a selector.
24 | */
25 | find(selector: Selector): VueQuery[]
26 |
27 | /**
28 | * Check if the current component has the selector in its descendant tree.
29 | */
30 | has(selector: Selector): boolean
31 |
32 | /**
33 | * Check if the current component matches the selector (which can be a string, a
34 | * Vue/VueQuery instance, or an array of those, in which case a single
35 | * match will return true.)
36 | */
37 | is(selector: Selector): boolean
38 |
39 | /**
40 | * Get the immediately following sibling of the current component.
41 | * If a selector is provided, it retrieves the next sibling only if it matches that selector.
42 | */
43 | next(selector?: Selector): VueQuery | null
44 |
45 | /**
46 | * Get all following siblings of the current component, optionally filtered by a selector.
47 | */
48 | nextAll(selector?: Selector): VueQuery[]
49 |
50 | /**
51 | * Get all following siblings of the current component, up to but not including the component
52 | * matched by the selector passed.
53 | */
54 | nextUntil(until?: Selector, filter?: Selector): VueQuery[]
55 |
56 | /**
57 | * Get the parent of the current component, optionally filtered by a selector.
58 | */
59 | parent(selector?: Selector): VueQuery | null
60 |
61 | /**
62 | * Get the ancestors of the current component, optionally filtered by a selector.
63 | */
64 | parents(selector?: Selector): VueQuery[]
65 |
66 | /**
67 | * Get the ancestors of the current component, up to but not including the component matched
68 | * by the selector.
69 | */
70 | parentsUntil(until?: Selector, filter?: Selector): VueQuery[]
71 |
72 | /**
73 | * Get the immediately preceding sibling of the current component.
74 | * If a selector is provided, it retrieves the previous sibling only if it matches that selector.
75 | */
76 | prev(selector?: Selector): VueQuery | null
77 |
78 | /**
79 | * Get all preceding siblings of the current component, optionally filtered by a selector.
80 | */
81 | prevAll(selector?: Selector): VueQuery[]
82 |
83 | /**
84 | * Get all preceding siblings of the current component, up to but not including the component
85 | * matched by the selector.
86 | */
87 | prevUntil(until?: Selector, filter?: Selector): VueQuery[]
88 |
89 | /**
90 | * Get the siblings of the current component, optionally filtered by a selector.
91 | */
92 | siblings(selector?: Selector): VueQuery[]
93 | }
94 |
95 | export default function $(vm: Vue | VueQuery | (Vue | VueQuery)[] | any): VueQuery | VueQuery[]
96 |
--------------------------------------------------------------------------------