Skip to content
This repository was archived by the owner on Aug 7, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/oui-angular/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import CriteriaAdder from "@ovh-ui/oui-criteria-adder";
import CriteriaContainer from "@ovh-ui/oui-criteria-container";
import Datagrid from "@ovh-ui/oui-datagrid";
import Dropdown from "@ovh-ui/oui-dropdown";
import DualList from "@ovh-ui/oui-dual-list";
import Field from "@ovh-ui/oui-field";
import FormActions from "@ovh-ui/oui-form-actions";
import GuideMenu from "@ovh-ui/oui-guide-menu";
Expand Down Expand Up @@ -50,6 +51,7 @@ export default angular
CriteriaContainer,
Datagrid,
Dropdown,
DualList,
Field,
FormActions,
GuideMenu,
Expand Down
1 change: 1 addition & 0 deletions packages/oui-angular/src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ loadTests(require.context("../../oui-criteria-adder/src/", true, /.*((\.spec)|(i
loadTests(require.context("../../oui-criteria-container/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-datagrid/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-dropdown/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-dual-list/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-field/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-form-actions/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-guide-menu/src/", true, /.*((\.spec)|(index))$/));
Expand Down
123 changes: 123 additions & 0 deletions packages/oui-dual-list/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Dual List

<component-status cx-design="complete" ux="rc"></component-status>

## Usage

### Basic

#### Array of strings

```html:preview
<oui-dual-list
source="$ctrl.sourceStrings"
target="$ctrl.targetStrings">
<oui-dual-list-source searchable></oui-dual-list-source>
<oui-dual-list-target></oui-dual-list-target>
</oui-dual-list>
```

#### Array of objects

```html:preview
<oui-dual-list
source="$ctrl.sourceObjects"
target="$ctrl.targetObjects"
property="name">
<oui-dual-list-source searchable></oui-dual-list-source>
<oui-dual-list-target></oui-dual-list-target>
</oui-dual-list>
```

#### Array of objects (deep nested property)

```html:preview
<oui-dual-list
source="$ctrl.sourceNestedObjects"
target="$ctrl.targetNestedObjects"
property="name.firstName">
<oui-dual-list-source searchable></oui-dual-list-source>
<oui-dual-list-target></oui-dual-list-target>
</oui-dual-list>
```

### Loading state

**Note**: If `source` or `target` attribute are undefined, the loading will be automatically active.

```html:preview
<div class="oui-doc-preview-only">
<p>
<button class="oui-button oui-button_primary" type="button" ng-class="{
'oui-button_primary': $ctrl.togglerSourceLoading,
'oui-button_secondary': !$ctrl.togglerSourceLoading
}" ng-click="$ctrl.togglerSourceLoading = !$ctrl.togglerSourceLoading">
Toggle source loading
</button>
<button class="oui-button oui-button_primary" type="button" ng-class="{
'oui-button_primary': $ctrl.togglerTargetLoading,
'oui-button_secondary': !$ctrl.togglerTargetLoading
}" ng-click="$ctrl.togglerTargetLoading = !$ctrl.togglerTargetLoading">
Toggle target loading
</button>
</p>
</div>
<oui-dual-list
source="$ctrl.sourceLoading"
target="$ctrl.targetLoading">
<oui-dual-list-source loading="$ctrl.togglerSourceLoading" searchable></oui-dual-list-source>
<oui-dual-list-target loading="$ctrl.togglerTargetLoading"></oui-dual-list-target>
</oui-dual-list>
```

### Events

**Note**: Bulk actions `Add all` and `Remove all` will trigger callbacks for each moved items.

```html:preview
<oui-dual-list
source="$ctrl.sourceEvents"
target="$ctrl.targetEvents"
on-add="$ctrl.onAdd(item)"
on-remove="$ctrl.onRemove(item)"
on-change="$ctrl.onChange(item)">
<oui-dual-list-source searchable></oui-dual-list-source>
<oui-dual-list-target></oui-dual-list-target>
</oui-dual-list>
<div class="oui-doc-preview-only" style="margin-top: 15px;">
<p><strong>onAdd ({{$ctrl.onAddCount}}):</strong> {{$ctrl.onAddItem | json}}</p>
<p><strong>onRemove ({{$ctrl.onRemoveCount}}):</strong> {{$ctrl.onRemoveItem | json}}</p>
<p><strong>onChange ({{$ctrl.onChangeCount}}):</strong> {{$ctrl.onChangeItem | json}}</p>
</div>
```

## API

### oui-dual-list

| Attribute | Type | Binding | One-time binding | Values | Default | Description
| ---- | ---- | ---- | ---- | ---- | ---- | ----
| `source` | array | = | no | n/a | n/a | source model bound to component
| `target` | array | = | no | n/a | n/a | target model bound to component
| `property` | string | @? | no | n/a | n/a | property path used to get value from item
| `on-add` | function | & | no | n/a | n/a | handler triggered when an item is added
| `on-remove` | function | & | no | n/a | n/a | handler triggered when an item is removed
| `on-change` | function | & | no | n/a | n/a | handler triggered when items have changed

### oui-dual-list-source

| Attribute | Type | Binding | One-time binding | Values | Default | Description
| ---- | ---- | ---- | ---- | ---- | ---- | ----
| `heading` | string | @? | yes | n/a | n/a | heading text
| `placeholder` | string | @? | yes | n/a | n/a | placeholder text
| `loading` | boolean | <? | no | `true`, `false` | `false` | loading flag
| `searchable` | boolean | <? | no | `true`, `false` | `false` | searchbale flag

### oui-dual-list-target

| Attribute | Type | Binding | One-time binding | Values | Default | Description
| ---- | ---- | ---- | ---- | ---- | ---- | ----
| `heading` | string | @? | yes | n/a | n/a | heading text
| `placeholder` | string | @? | yes | n/a | n/a | placeholder text
| `loading` | boolean | <? | no | `true`, `false` | `false` | loading flag
| `searchable` | boolean | <? | no | `true`, `false` | `false` | searchbale flag
7 changes: 7 additions & 0 deletions packages/oui-dual-list/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@ovh-ui/oui-dual-list",
"version": "1.0.0",
"main": "./src/index.js",
"license": "BSD-3-Clause",
"author": "OVH SAS"
}
13 changes: 13 additions & 0 deletions packages/oui-dual-list/src/dual-list.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import controller from "./dual-list.controller";

export default {
controller,
bindings: {
source: "=",
target: "=",
property: "@?",
onAdd: "&",
onRemove: "&",
onChange: "&"
}
};
59 changes: 59 additions & 0 deletions packages/oui-dual-list/src/dual-list.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import get from "lodash/get";
import remove from "lodash/remove";

export default class {
constructor ($attrs, $element, $filter, $timeout) {
"ngInject";

this.$attrs = $attrs;
this.$element = $element;
this.$filter = $filter;
this.$timeout = $timeout;
}

$postLink () {
this.$timeout(() =>
this.$element.addClass("oui-dual-list")
);
}

getProperty (item) {
return get(item, this.property, item);
}

moveToSource (item) {
if (angular.isArray(this.source)) {
remove(this.target, (source) => source === item);
this.source.push(item);

// Callbacks
this.onChange({ item });
this.onRemove({ item });
}
}

moveAllToSource (searchQuery) {
// Need to do a while for the callbacks
while (angular.isArray(this.source) && this.$filter("filter")(this.target, searchQuery).length) {
this.moveToSource(this.$filter("filter")(this.target, searchQuery)[0]);
}
}

moveToTarget (item) {
if (angular.isArray(this.target)) {
remove(this.source, (source) => source === item);
this.target.push(item);

// Callbacks
this.onChange({ item });
this.onAdd({ item });
}
}

moveAllToTarget (searchQuery) {
// Need to do a while for the callbacks
while (angular.isArray(this.target) && this.$filter("filter")(this.source, searchQuery).length) {
this.moveToTarget(this.$filter("filter")(this.source, searchQuery)[0]);
}
}
}
38 changes: 38 additions & 0 deletions packages/oui-dual-list/src/dual-list.provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import merge from "lodash/merge";

export default class {
constructor () {
this.translations = {
source: {
heading: "Items to select",
placeholder: "No item to select",
move: "Add",
moveAll: "Add all",
search: "Search in source content"
},
target: {
heading: "Selected items",
placeholder: "No selected item",
move: "Remove",
moveAll: "Remove all",
search: "Search in target content"
}
};
}

/**
* Set the translations
* @param {Object} translations a map of translations
*/
setTranslations (translations) {
this.translations = merge(this.translations, translations);
return this;
}

$get () {
return {
translations: this.translations
};
}
}

12 changes: 12 additions & 0 deletions packages/oui-dual-list/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import DualList from "./dual-list.component";
import DualListProvider from "./dual-list.provider";
import DualListSource from "./source/dual-list-source.component";
import DualListTarget from "./target/dual-list-target.component";

export default angular
.module("oui.dual-list", [])
.component("ouiDualList", DualList)
.component("ouiDualListSource", DualListSource)
.component("ouiDualListTarget", DualListTarget)
.provider("ouiDualListConfiguration", DualListProvider)
.name;
65 changes: 65 additions & 0 deletions packages/oui-dual-list/src/index.spec.data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"string": {
"source": ["Lorem"],
"target": ["Ipsum"]
},
"strings": {
"source": [
"Andrew",
"Red",
"Lila",
"Marie",
"Robert",
"Hope",
"Alexis",
"Madison",
"Vanessa"
],
"target": [
"Kevin",
"Mark"
]
},
"object": {
"source": [{ "name": "Lorem", "age": 15 }],
"target": [{ "name": "Ipsum", "age": 25 }]
},
"objects": {
"source" : [
{ "name": "Andrew", "age": 15 },
{ "name": "Red", "age": 25 },
{ "name": "Lila", "age": 34 },
{ "name": "Marie", "age": 12 },
{ "name": "Robert", "age": 64 },
{ "name": "Hope", "age": 15 },
{ "name": "Alexis", "age": 25 },
{ "name": "Madison", "age": 34 },
{ "name": "Vanessa", "age": 12 }
],
"target" : [
{ "name": "Kevin", "age": 12 },
{ "name": "Mark", "age": 64 }
]
},
"nestedObject": {
"source": [{ "name": { "firstName": "Lorem" }, "age": 15 }],
"target": [{ "name": { "firstName": "Ipsum" }, "age": 25 }]
},
"nestedObjects" : {
"source": [
{ "name": { "firstName": "Andrew" }, "age": 15 },
{ "name": { "firstName": "Red" }, "age": 25 },
{ "name": { "firstName": "Lila" }, "age": 34 },
{ "name": { "firstName": "Marie" }, "age": 12 },
{ "name": { "firstName": "Robert" }, "age": 64 },
{ "name": { "firstName": "Hope" }, "age": 15 },
{ "name": { "firstName": "Alexis" }, "age": 25 },
{ "name": { "firstName": "Madison" }, "age": 34 },
{ "name": { "firstName": "Vanessa" }, "age": 12 }
],
"target" : [
{ "name": { "firstName": "Kevin" }, "age": 12 },
{ "name": { "firstName": "Mark" }, "age": 64 }
]
}
}
Loading