Skip to content

Commit ad1c42a

Browse files
adding sort method
1 parent 5d81f33 commit ad1c42a

File tree

9 files changed

+184
-60
lines changed

9 files changed

+184
-60
lines changed

README.md

+24-2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ React Dynamic Tabs with full API
6060
- [off](#off)
6161
- [getData](#getData)
6262
- [getPreviousData](#getPreviousData)
63+
- [sort](#sort)
6364
- [tabData](#tabData)
6465
- [Lazy Loading](#lazy-loading)
6566
- [Styling](#styling)
@@ -167,9 +168,9 @@ export default () => {
167168

168169
**NOTE :**
169170

170-
- Tabs can't be manipulated safely before the first render, use ready() to access the instance object, ready accepts a function as its parameter and calls it when tabs are mounted.
171+
- Tabs can't be manipulated safely before the first render, use `ready()` to access the instance object, `ready` accepts a function as its parameter and calls it when tabs are mounted.
171172

172-
- ready function and instance Object will not be changed after re-rendering multiple times.
173+
- `ready` function identity is stable and won’t change on re-renders.
173174

174175
## Options
175176

@@ -976,6 +977,27 @@ const {selectedTabID, openTabIDs} = instance.getPreviousData();
976977

977978
- getCopyPerviousData function is an older version of getPreviousData function and it is enabled by default so that existing users do not have to change their code. You are free to use both conventions.
978979

980+
### sort
981+
982+
Useful for sorting tabs manually.
983+
984+
Triggers `onInit` event.
985+
986+
Return value : Promise
987+
988+
Parameters:
989+
990+
- `Array of all tabs IDs`
991+
992+
**Example**
993+
994+
```js
995+
const {openTabIDs} = instance.getData();
996+
instance.sort(openTabIDs.reverse()).then(({currentData, instance}) => {
997+
console.log('sorting tabs has finished');
998+
});
999+
```
1000+
9791001
## tabData
9801002

9811003
<table>

src/index.test.js

+36
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,42 @@ describe('refresh method : ', () => {
439439
});
440440
});
441441
});
442+
describe('sort method : ', () => {
443+
test('it should work correctly', () => {
444+
expect.assertions(4);
445+
renderApp();
446+
const buttons = document.querySelectorAll('button.rc-dyn-tabs-title');
447+
expect(buttons[0].id).toBe('rc-dyn-tabs-l-1');
448+
expect(buttons[1].id).toBe('rc-dyn-tabs-l-2');
449+
return act(() => {
450+
return instance.sort(['2', '1']).then(() => {
451+
const buttons = document.querySelectorAll('button.rc-dyn-tabs-title');
452+
expect(buttons[0].id).toBe('rc-dyn-tabs-l-2');
453+
expect(buttons[1].id).toBe('rc-dyn-tabs-l-1');
454+
});
455+
});
456+
});
457+
test('it should fire onInit event', () => {
458+
renderApp();
459+
act(() => {
460+
instance.sort([]);
461+
});
462+
expect(op.onInit.mock.calls.length === 2).toBe(true);
463+
expect(op.onChange.mock.calls.length === 0).toBe(true);
464+
});
465+
test('returned Promise from sort method is resolved with {currentData,instance} parameter after onInit event', () => {
466+
expect.assertions(4);
467+
renderApp();
468+
return act(() => {
469+
return instance.sort([]).then((result) => {
470+
expect(result.currentData).toEqual(instance.getData());
471+
expect(result.instance).toBe(instance);
472+
expect(op.onInit.mock.calls.length === 2).toBe(true);
473+
expect(op.onChange.mock.calls.length === 0).toBe(true);
474+
});
475+
});
476+
});
477+
});
442478
describe('ready function : ', () => {
443479
// test('ready function and instance parameters always refer to same reference after re-rendering multiple times', () => {
444480
// });

src/utils/api/api.factory.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Helper from '../helper.js';
2-
const {throwMissingParam: missingParamEr} = Helper;
2+
const {throwMissingParam: missingParamEr, isArray, thorwInvalidParam} = Helper;
33
export const apiConstructor = function (getDeps, param = {options: {}}) {
44
const {optionsManager, helper, activedTabsHistory} = getDeps.call(this, param.options);
55
helper.setNoneEnumProps(this, {optionsManager, helper, activedTabsHistory, userProxy: {}});
@@ -152,6 +152,14 @@ const _apiProps = {
152152
this._open(newTabObj.id);
153153
return result;
154154
},
155+
sort: function (tabIDs = missingParamEr('sort')) {
156+
if (!isArray(tabIDs)) {
157+
thorwInvalidParam('sort');
158+
}
159+
const result = this._getFlushEffectsPromise();
160+
this._sort(tabIDs);
161+
return result;
162+
},
155163
__close: function (id) {
156164
const result = this._getFlushEffectsPromise();
157165
this._close(id);

src/utils/api/api.factory.test.js

+38
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,44 @@ describe('Api.prototype.select : ', () => {
253253
}
254254
});
255255
});
256+
describe('Api.prototype.sort : ', () => {
257+
test('should be called with an array of all tabs IDs', () => {
258+
expect.assertions(2);
259+
const realSort = obj._sort;
260+
obj._sort = () => Promise.resolve(null);
261+
const {openTabIDs} = obj.getData();
262+
try {
263+
obj.sort();
264+
} catch (er) {
265+
expect(1 === 1).toBe(true);
266+
}
267+
try {
268+
obj.sort({openTabIDs});
269+
} catch (er) {
270+
expect(1 === 1).toBe(true);
271+
}
272+
try {
273+
const sortedTabIDs = [...openTabIDs];
274+
sortedTabIDs.push('29');
275+
obj.sort(sortedTabIDs);
276+
} catch (er) {
277+
expect(1 === 1).toBe(true);
278+
}
279+
try {
280+
obj.sort([...openTabIDs].reverse());
281+
} catch (er) {
282+
expect(1 === 1).toBe(true);
283+
}
284+
obj._sort = realSort;
285+
});
286+
test('it should return a thenable object', () => {
287+
const realSort = obj._sort;
288+
obj._sort = () => {};
289+
const result = obj.sort([]);
290+
expect(typeof result.then).toBe('function');
291+
obj._sort = realSort;
292+
});
293+
});
256294
describe('Api.prototype.eventHandlerFactory : ', () => {
257295
test('eventHandlerFactory method calls select function with switching=true if beforeSelect callback returned true', () => {
258296
expect.assertions(7);

src/utils/api/api.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe('user api : ', () => {
2020
'select',
2121
'open',
2222
'close',
23+
'sort',
2324
'refresh',
2425
];
2526
expect(Object.keys(obj.userProxy).length === userApi.length).toBe(true);

src/utils/api/baseApi.js

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ BaseApi.prototype._refresh = function () {
2929
this._dispatch({type: actions.refresh});
3030
this.__flushEffects();
3131
};
32+
BaseApi.prototype._sort = function (tabId) {
33+
this._dispatch({type: actions.sort, tabId});
34+
this.__flushEffects();
35+
};
3236
BaseApi.prototype.__flushEffects = function () {
3337
this._setFlushState({});
3438
};

src/utils/helper.js

+61-56
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,65 @@
1-
const helper = {};
2-
helper.checkArrIndex = (index, arrLength) => index >= 0 && index < arrLength;
3-
helper.getInstance = function (Fn) {
4-
new (Function.prototype.bind.apply(Fn, arguments))();
5-
};
6-
helper.resolve = (result) => Promise.resolve(result);
7-
helper.getCopyState = function (state) {
8-
return {
9-
selectedTabID: state.selectedTabID,
10-
openTabIDs: (state.openTabIDs || []).slice(),
11-
};
12-
};
13-
helper.assingAll = function (targetObj, ...sourcObjs) {
14-
// copy all enumerable and not enumerable properties into the target
15-
sourcObjs.map((sourcObj) => {
16-
const enum_only = Object.keys(sourcObj);
17-
Object.getOwnPropertyNames(sourcObj).map((prop) => {
18-
if (enum_only.indexOf(prop) >= 0) targetObj[prop] = sourcObj[prop];
19-
else
20-
Object.defineProperty(targetObj, prop, {
21-
value: sourcObj[prop],
22-
writable: true,
23-
});
24-
});
25-
});
26-
return targetObj;
27-
};
28-
helper.setNoneEnumProps = function (obj, props) {
29-
const noneEnum = {};
30-
Object.keys(props).map((prop) => {
31-
noneEnum[prop] = {
32-
writable: true,
33-
value: props[prop],
1+
const helper = {
2+
checkArrIndex: (index, arrLength) => index >= 0 && index < arrLength,
3+
getInstance: function (Fn) {
4+
new (Function.prototype.bind.apply(Fn, arguments))();
5+
},
6+
resolve: (result) => Promise.resolve(result),
7+
getCopyState: function (state) {
8+
return {
9+
selectedTabID: state.selectedTabID,
10+
openTabIDs: (state.openTabIDs || []).slice(),
3411
};
35-
});
36-
return Object.defineProperties(obj, noneEnum);
37-
};
38-
helper.getArraysDiff = function (arr1, arr2) {
39-
const arr1Copy = [...arr1],
40-
arr2Copy = [...arr2];
41-
arr1.map((item) => {
42-
if (arr2.indexOf(item) >= 0) {
43-
arr1Copy.splice(arr1Copy.indexOf(item), 1);
44-
arr2Copy.splice(arr2Copy.indexOf(item), 1);
12+
},
13+
assingAll: function (targetObj, ...sourcObjs) {
14+
// copy all enumerable and not enumerable properties into the target
15+
sourcObjs.map((sourcObj) => {
16+
const enum_only = Object.keys(sourcObj);
17+
Object.getOwnPropertyNames(sourcObj).map((prop) => {
18+
if (enum_only.indexOf(prop) >= 0) targetObj[prop] = sourcObj[prop];
19+
else
20+
Object.defineProperty(targetObj, prop, {
21+
value: sourcObj[prop],
22+
writable: true,
23+
});
24+
});
25+
});
26+
return targetObj;
27+
},
28+
setNoneEnumProps: function (obj, props) {
29+
const noneEnum = {};
30+
Object.keys(props).map((prop) => {
31+
noneEnum[prop] = {
32+
writable: true,
33+
value: props[prop],
34+
};
35+
});
36+
return Object.defineProperties(obj, noneEnum);
37+
},
38+
getArraysDiff: function (arr1, arr2) {
39+
const arr1Copy = [...arr1],
40+
arr2Copy = [...arr2];
41+
arr1.map((item) => {
42+
if (arr2.indexOf(item) >= 0) {
43+
arr1Copy.splice(arr1Copy.indexOf(item), 1);
44+
arr2Copy.splice(arr2Copy.indexOf(item), 1);
45+
}
46+
});
47+
return [arr1Copy, arr2Copy];
48+
},
49+
filterArrayUntilFirstValue: (arr, callback, isRightToLeft) => {
50+
isRightToLeft && arr.reverse();
51+
for (let i = 0, l = arr.length; i < l; i++) {
52+
if (callback(arr[i], i, arr)) return arr[i];
4553
}
46-
});
47-
return [arr1Copy, arr2Copy];
48-
};
49-
helper.filterArrayUntilFirstValue = (arr, callback, isRightToLeft) => {
50-
isRightToLeft && arr.reverse();
51-
for (let i = 0, l = arr.length; i < l; i++) {
52-
if (callback(arr[i], i, arr)) return arr[i];
53-
}
54-
return null;
55-
};
56-
helper.throwMissingParam = (FnName) => {
57-
throw `Missing parameter in "${FnName}" function`;
54+
return null;
55+
},
56+
throwMissingParam: (FnName) => {
57+
throw new Error(`Missing parameter in "${FnName}" function`);
58+
},
59+
thorwInvalidParam: (FnName) => {
60+
throw new Error(`Invalid parameter values passed to ${FnName} function`);
61+
},
62+
isObj: (obj) => Object.prototype.toString.call(obj) === '[object Object]',
63+
isArray: (arr) => typeof arr === 'object' && arr.constructor === Array,
5864
};
59-
helper.isObj = (obj) => Object.prototype.toString.call(obj) === '[object Object]';
6065
export default helper;

src/utils/stateManagement/actions.js

+1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ const actions = {
33
close: 'close',
44
active: 'active',
55
refresh: 'refresh',
6+
sort: 'sort',
67
};
78
export default actions;

src/utils/stateManagement/reducer.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,16 @@ export default function reducer(state, action) {
2929
if (state.selectedTabID !== tabId) return {selectedTabID: tabId, openTabIDs: state.openTabIDs};
3030
return state;
3131
}
32-
32+
case actions.sort: {
33+
const arr = state.openTabIDs,
34+
newArr = action.tabId,
35+
newArrCount = newArr.length;
36+
if (arr.length !== newArrCount) return state;
37+
for (let i = 0; i < newArrCount; i++) {
38+
if (arr.indexOf(newArr[i]) === -1) return state;
39+
}
40+
return {selectedTabID: state.selectedTabID, openTabIDs: newArr};
41+
}
3342
default:
3443
throw new Error(`Undefined action type '${action.type}'`);
3544
}

0 commit comments

Comments
 (0)