Skip to content

Commit

Permalink
Merge pull request #230 from hypermedia-app/multi-select
Browse files Browse the repository at this point in the history
feat: autocomplete and multi-select
  • Loading branch information
tpluscode authored Aug 28, 2022
2 parents ffa6dbe + 7354f22 commit 83e2a04
Show file tree
Hide file tree
Showing 39 changed files with 561 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/funny-comics-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hydrofoil/shaperone-core": patch
---

Added a module which exports `sh1` namespace for extensions
6 changes: 6 additions & 0 deletions .changeset/great-nails-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hydrofoil/shaperone-core": patch
"@hydrofoil/shaperone-wc": patch
---

Export default metadata for multi instances select editor
5 changes: 5 additions & 0 deletions .changeset/swift-jars-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hydrofoil/shaperone-hydra": patch
---

Apply search decorator to multi instance editor
5 changes: 5 additions & 0 deletions .changeset/wise-brooms-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hydrofoil/shaperone-wc-shoelace": patch
---

Added `dash:AutocompleteEditor` and `sh1:InstanceMultiSelectEditor`
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
types/
node_modules/
dist/
*.snap.js
1 change: 1 addition & 0 deletions demos/lit-html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="Description" content="Put your description here.">
<link href="https://fonts.googleapis.com/css?family=Material+Icons&display=block" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/dist/themes/light.css" />
<script>try{new EventTarget}catch(e){document.write('<script src="https://unpkg.com/@ungap/[email protected]/min.js"><\x2fscript>')}</script>

<style>
Expand Down
1 change: 1 addition & 0 deletions demos/lit-html/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@hydrofoil/shaperone-wc": "0.7.2",
"@hydrofoil/shaperone-wc-material": "0.5.5",
"@hydrofoil/shaperone-wc-vaadin": "0.4.7",
"@hydrofoil/shaperone-wc-shoelace": "0.1.7",
"@hydrofoil/shaperone-hydra": "0.3.9",
"@hydrofoil/shaperone-rdf-validate-shacl": "1.0.0",
"@material/mwc-icon": "^0.25",
Expand Down
5 changes: 5 additions & 0 deletions demos/lit-html/src/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import * as StarRating from '@hydrofoil/shaperone-playground-examples/StarRating
import { component as starRating } from '@hydrofoil/shaperone-playground-examples/StarRating'
import { DescriptionTooltip } from '@hydrofoil/shaperone-playground-examples/DescriptionTooltip'
import * as vaadinComponents from '@hydrofoil/shaperone-wc-vaadin/components'
import * as shoelaceComponents from '@hydrofoil/shaperone-wc-shoelace/components'
import { settings as shoelaceSettings } from '@hydrofoil/shaperone-wc-shoelace/settings.js'
import { components, editors, renderer, validation } from '@hydrofoil/shaperone-wc/configure'
import { dash } from '@tpluscode/rdf-ns-builders'
import { Decorate, RenderTemplate, templates } from '@hydrofoil/shaperone-wc/templates'
Expand All @@ -18,10 +20,13 @@ import { ComponentsState } from './state/models/components'
import { RendererState } from './state/models/renderer'
import { errorSummary } from '../../examples/ErrorSummary'

shoelaceSettings.hoist = false

export const componentSets: Record<ComponentsState['components'], Record<string, Component>> = {
native: { ...nativeComponents, starRating },
material: { ...nativeComponents, ...mwcComponents, languages: LanguageSelect.component('material'), starRating },
vaadin: { ...nativeComponents, ...vaadinComponents, languages: LanguageSelect.component('lumo'), starRating },
shoelace: { ...nativeComponents, ...shoelaceComponents, starRating },
}

editors.addMetadata([...LanguageSelect.metadata(), ...StarRating.metadata()])
Expand Down
3 changes: 2 additions & 1 deletion demos/lit-html/src/state/models/components.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createModel } from '@captaincodeman/rdx'

export interface ComponentsState {
components: 'native' | 'material' | 'vaadin'
components: 'native' | 'material' | 'vaadin' | 'shoelace'
disableEditorChoice: boolean
}

Expand All @@ -16,6 +16,7 @@ export const componentsSettings = createModel({
case 'material':
case 'native':
case 'vaadin':
case 'shoelace':
return { ...state, components }
default:
return state
Expand Down
7 changes: 7 additions & 0 deletions packages/core/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import $rdf from '@rdf-esm/data-model'
import { dash, rdf } from '@tpluscode/rdf-ns-builders'
import sh1 from './ns.js'

export default [
$rdf.quad(sh1.InstancesMultiSelectEditor, rdf.type, dash.MultiEditor),
]
3 changes: 3 additions & 0 deletions packages/core/ns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import namespace from '@rdf-esm/namespace'

export default namespace('https://hypermedia.app/shaperone#')
6 changes: 4 additions & 2 deletions packages/hydra/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
* @module @hydrofoil/shaperone-hydra/components
*/

import * as instancesSelectorNs from './lib/components/instancesSelector'
import * as autocompleteNs from './lib/components/autocomplete'
import * as instancesSelectorNs from './lib/components/instancesSelector.js'
import * as autocompleteNs from './lib/components/autocomplete.js'
import * as multiInstanceSelectorNs from './lib/components/multiInstanceSelector.js'

export { instancesSelectorNs as instancesSelector }
export { autocompleteNs as autocomplete }
export { multiInstanceSelectorNs as multiInstanceSelector }
3 changes: 2 additions & 1 deletion packages/hydra/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { StoreDispatch } from '@captaincodeman/rdx'
import type { editors } from '@hydrofoil/shaperone-core/models/editors'
import type { components } from '@hydrofoil/shaperone-core/models/components'
import type { HydraClient } from 'alcaeus/alcaeus'
import { autocomplete, instancesSelector } from './components'
import { autocomplete, instancesSelector, multiInstanceSelector } from './components'

interface Config {
models: {
Expand All @@ -25,6 +25,7 @@ interface Options {
export default function setup(configuration: Configuration, { client }: Options = {}): void {
configuration.components.decorate(instancesSelector.decorator(client))
configuration.components.decorate(autocomplete.decorator(client))
configuration.components.decorate(multiInstanceSelector.decorator(client))
configuration.editors.decorate(instancesSelector.matcher)
configuration.editors.decorate(autocomplete.matcher)
}
2 changes: 1 addition & 1 deletion packages/hydra/lib/components/instancesSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ export const matcher: MatcherDecorator = {
},
}

export { decorator } from './searchDecorator'
export { decorator } from './searchDecorator.js'
13 changes: 13 additions & 0 deletions packages/hydra/lib/components/multiInstanceSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import sh1 from '@hydrofoil/shaperone-core/ns.js'
import { ComponentDecorator } from '@hydrofoil/shaperone-core/models/components'
import { InstancesSelectEditor } from '@hydrofoil/shaperone-core/lib/components/instancesSelect'
import { decorator as searchDecorator } from './searchDecorator.js'

export function decorator(...args: Parameters<typeof searchDecorator>): ComponentDecorator<InstancesSelectEditor> {
return {
...searchDecorator(...args),
applicableTo(component) {
return component.editor.equals(sh1.InstancesMultiSelectEditor)
},
}
}
17 changes: 17 additions & 0 deletions packages/hydra/test/lib/components/multiInstanceSelector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { expect } from '@open-wc/testing'
import sh1 from '@hydrofoil/shaperone-core/ns.js'
import { decorator } from '../../../lib/components/multiInstanceSelector'

describe('hydra/lib/components/multiInstancesSelector', () => {
describe('decorator', () => {
it('applies to Multi Instances Selector', () => {
// given
const component = {
editor: sh1.InstancesMultiSelectEditor,
}

// then
expect(decorator().applicableTo(component)).to.be.true
})
})
})
27 changes: 27 additions & 0 deletions packages/wc-shoelace/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
# sh-sl-autocomplete

## Properties

| Property | Attribute | Type | Default |
|--------------|--------------|--------------------------------------------------|---------|
| `empty` | `empty` | `boolean` | true |
| `hoist` | `hoist` | `boolean` | true |
| `inputValue` | `inputValue` | `string` | "" |
| `selected` | `selected` | `GraphPointer<Term, DatasetCore<Quad, Quad>> \| undefined` | |

## Methods

| Method | Type |
|------------------------|-------------------------------|
| `dispatchItemSelected` | `(e: CustomEvent<any>): void` |
| `dispatchSearch` | `(): void` |
| `updateEmpty` | `(e: Event): void` |

## Events

| Event | Type |
|----------------|-----------------------------------|
| `itemSelected` | `CustomEvent<{ value: any; }>` |
| `search` | `CustomEvent<{ value: string; }>` |


# sh-sl-object

## Properties
Expand Down
57 changes: 57 additions & 0 deletions packages/wc-shoelace/component-extras.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
instancesSelect,
} from '@hydrofoil/shaperone-core/components.js'
import * as select from '@hydrofoil/shaperone-core/lib/components/base/instancesSelect.js'
import sh1 from '@hydrofoil/shaperone-core/ns.js'
import { html } from 'lit'
import { repeat } from 'lit/directives/repeat.js'
import { Lazy, MultiEditorComponent } from '@hydrofoil/shaperone-wc'
import { difference } from '@ngard/tiny-difference'
import { SlSelect } from '@shoelace-style/shoelace'
import type { GraphPointer } from 'clownface'
import { renderItem } from './lib/components.js'
import { stop } from './lib/handlers.js'
import { settings } from './settings.js'

export const instancesMultiSelectEditor: Lazy<MultiEditorComponent> = {
...select,
editor: sh1.InstancesMultiSelectEditor,
init(...args: [any, any]) {
return instancesSelect.init?.call(this, ...args) || true
},
async lazyRender() {
return ({ property, componentState }, { update }) => {
const values = property.objects.map(o => o.object?.value).filter(isDef)
const pointers: GraphPointer[] = componentState.instances || []

function onChange(e: CustomEvent) {
const target = e.target as SlSelect

if (Array.isArray(target.value) && difference(target.value, values).length !== 0) {
const selected = pointers
.filter(({ value }) => target.value.includes(value))
.map(({ term }) => term)

update(selected)
}
}

function selectAll() {
const all = pointers.map(({ term }) => term)
update(all)
}

return html`
<sl-select ?hoist="${settings.hoist}" multiple clearable .value=${values} @sl-hide=${stop} @sl-change=${onChange}>
${repeat(pointers || [], renderItem)}
</sl-select>
<sl-button @click=${selectAll}>
Select all
</sl-button>`
}
},
}

function isDef<T>(x: T | undefined): x is T {
return !!x
}
6 changes: 5 additions & 1 deletion packages/wc-shoelace/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import rdf from '@rdfjs/data-model'
import { isGraphPointer, isLiteral } from 'is-graph-pointer'
import type { GraphPointer } from 'clownface'

export { autocomplete } from './components/autocomplete.js'
export { enumSelect } from './components/enumSelect.js'
export { instancesSelect } from './components/instancesSelect.js'

interface EditorState extends ComponentInstance {
noLabel?: boolean
}
Expand All @@ -28,7 +32,7 @@ export const textFieldWithLang: Lazy<SingleEditorComponent<TextFieldWithLang>> =
async lazyRender() {
const [{ inputRenderer }] = await Promise.all([
import('./renderer/input'),
import('./components/sh-sl-with-lang-editor'),
import('./elements/sh-sl-with-lang-editor'),
])

function extractLanguage(ptr: GraphPointer | undefined) {
Expand Down
102 changes: 102 additions & 0 deletions packages/wc-shoelace/components/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Lazy } from '@hydrofoil/shaperone-core'
import * as Core from '@hydrofoil/shaperone-core/components.js'
import { dash, rdfs } from '@tpluscode/rdf-ns-builders'
import { html } from 'lit'
import { repeat } from 'lit/directives/repeat.js'
import { localizedLabel } from '@rdfjs-elements/lit-helpers/localizedLabel.js'
import isGraphPointer from 'is-graph-pointer'
import { NamedNode } from 'rdf-js'
import { SingleEditorRenderParams } from '@hydrofoil/shaperone-core/models/components'
import type { GraphPointer } from 'clownface'
import { renderItem } from '../lib/components.js'
import { settings } from '../settings.js'

interface Options {
labelProperties: NamedNode | NamedNode[]
}

export interface AutoCompleteEditor extends Core.AutoCompleteEditor {
initLabel(arg: SingleEditorRenderParams): void
}

declare module '@hydrofoil/shaperone-core/components' {
/* eslint-disable @typescript-eslint/no-empty-interface */
interface AutoComplete {
selected?: GraphPointer
}
}

export const autocomplete: Lazy<AutoCompleteEditor> & Options = {
...Core.instancesSelect,
labelProperties: rdfs.label,
editor: dash.AutoCompleteEditor,
async lazyRender() {
await import('../elements/sh-sl-autocomplete.js')

return (params, { update }) => {
const { value } = params
const pointers = value.componentState.instances || []
const freetextQuery = value.componentState.freetextQuery || ''
const { selected } = value.componentState

const search = (e: CustomEvent) => {
params.updateComponentState({
freetextQuery: e.detail.value,
})
}

const itemSelected = (e: CustomEvent) => {
const selected = pointers.find(({ value }) => value === e.detail.value)

params.updateComponentState({
freetextQuery: '',
selected,
})
if (selected) {
update(selected.term)
}
}

let nodeValue = value.object?.value
if (isGraphPointer.isNamedNode(value.object)) {
const nodeUrl = new URL(value.object.value)
nodeValue = nodeUrl.hash || nodeUrl.pathname
}
const fallback = nodeValue || freetextQuery

return html`
<sh-sl-autocomplete .selected=${selected}
.inputValue=${localizedLabel(selected, { property: autocomplete.labelProperties, fallback })}
@search=${search}
@itemSelected=${itemSelected}
.hoist="${settings.hoist}"
>
${repeat(pointers, renderItem)}
</sh-sl-autocomplete>`
}
},
initLabel(this: AutoCompleteEditor, { property: { shape }, value, updateComponentState }) {
const {
object,
componentState: { freetextQuery, selectionLoading },
} = value

if (object && !freetextQuery && !selectionLoading) {
const selectionLoading = this.loadInstance({ property: shape, value: object })
.then((resource) => {
updateComponentState({
selected: resource,
})
})

updateComponentState({ selectionLoading })
}
},
init(...args) {
Core.instancesSelect.init?.call(this, ...args)

this.initLabel(args[0])

return true
},
}
15 changes: 15 additions & 0 deletions packages/wc-shoelace/components/enumSelect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Lazy } from '@hydrofoil/shaperone-core'
import * as Core from '@hydrofoil/shaperone-core/components.js'

export const enumSelect: Lazy<Core.EnumSelectEditor> = {
...Core.enumSelect,
async lazyRender() {
const { select } = await import('./select.js')

return ({ value, componentState }, { update }) => {
const pointers = componentState.instances || []

return select(value, pointers, update)
}
},
}
Loading

0 comments on commit 83e2a04

Please sign in to comment.