contacts-pane 2.4.8 → 2.4.12-beta4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.eslintrc +3 -1
  2. package/.github/workflows/ci.yml +74 -0
  3. package/.nvmrc +1 -1
  4. package/LICENSE.md +0 -0
  5. package/Makefile +0 -0
  6. package/README.md +0 -0
  7. package/__tests__/unit/data-reformat-test.js +166 -0
  8. package/__tests__/unit/data-reformat-test.ts +185 -0
  9. package/__tests__/unit/setup.js +74 -0
  10. package/__tests__/unit/setup.ts +77 -0
  11. package/babel.config.js +13 -0
  12. package/card.ai +0 -0
  13. package/card.png +0 -0
  14. package/contactLogic.js +84 -7
  15. package/contactsPane.js +64 -20
  16. package/diff.txt +0 -0
  17. package/exampleOfOpenData/mit-wikidata-details.ttl +0 -0
  18. package/exampleOfOpenData/mit-wikidata-query.sparql +0 -0
  19. package/exampleOfOpenData/wikidata-get.json +0 -0
  20. package/groupMembershipControl.js +56 -13
  21. package/individual.js +3 -2
  22. package/individualForm.js +0 -0
  23. package/jest.config.js +11 -0
  24. package/jest.setup.ts +10 -0
  25. package/lib/autocompleteBar.js +99 -0
  26. package/lib/autocompleteBar.js.map +1 -0
  27. package/lib/autocompleteField.js +157 -0
  28. package/lib/autocompleteField.js.map +1 -0
  29. package/lib/autocompletePicker.js +240 -0
  30. package/lib/autocompletePicker.js.map +1 -0
  31. package/lib/forms.js +315 -0
  32. package/lib/instituteDetailsQuery.js +38 -0
  33. package/lib/publicData.js +387 -0
  34. package/lib/publicData.js.map +1 -0
  35. package/lib/vcard.js +916 -0
  36. package/mintNewAddressBook.js +5 -4
  37. package/mugshotGallery.js +16 -4
  38. package/organizationForm.js +0 -0
  39. package/organizationForm.ttl +0 -0
  40. package/package.json +26 -13
  41. package/shapes/contacts-shapes.ttl +0 -0
  42. package/src/autocompleteBar.ts +92 -0
  43. package/src/autocompleteField.ts +180 -0
  44. package/src/autocompletePicker.ts +253 -0
  45. package/src/forms.ttl +1 -1
  46. package/src/instituteDetailsQuery.sparql +0 -0
  47. package/src/publicData.ts +385 -0
  48. package/src/vcard.ttl +0 -0
  49. package/toolsPane.js +70 -19
  50. package/tsconfig.json +16 -0
  51. package/webidControl.js +49 -4
@@ -1,5 +1,7 @@
1
- const UI = require('solid-ui')
1
+ import * as UI from 'solid-ui'
2
+ import { solidLogicSingleton } from 'solid-logic'
2
3
 
4
+ const { setACLUserPublic } = solidLogicSingleton.acl
3
5
  // const mime = require('mime-types')
4
6
  // const toolsPane0 = require('./toolsPane')
5
7
  // const toolsPane = toolsPane0.toolsPane
@@ -12,7 +14,7 @@ const $rdf = UI.rdf
12
14
 
13
15
  export function mintNewAddressBook (dataBrowserContext, context) {
14
16
  return new Promise(function (resolve, reject) {
15
- UI.authn.logInLoadProfile(context).then(
17
+ UI.login.ensureLoadedProfile(context).then(
16
18
  context => {
17
19
  // 20180713
18
20
  console.log('Logged in as ' + context.me)
@@ -120,8 +122,7 @@ export function mintNewAddressBook (dataBrowserContext, context) {
120
122
  return reject(new Error('Error writing new file ' + task.to))
121
123
  }
122
124
 
123
- UI.authn
124
- .setACLUserPublic(dest, me, aclOptions)
125
+ setACLUserPublic(dest, me, aclOptions)
125
126
  .then(() => doNextTask())
126
127
  .catch(err => {
127
128
  const message =
package/mugshotGallery.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import * as UI from 'solid-ui'
2
+ import { store } from 'solid-logic'
2
3
  import * as mime from 'mime-types'
3
4
 
4
5
  const $rdf = UI.rdf
5
6
  const ns = UI.ns
6
7
  const utils = UI.utils
7
- const kb = UI.store
8
+ const kb = store
8
9
 
9
10
  /* Mugshot Gallery
10
11
  *
@@ -116,7 +117,7 @@ export function renderMugshotGallery (dom, subject) {
116
117
  // When a set of URIs are dropped on
117
118
  async function handleURIsDroppedOnMugshot (uris) {
118
119
  for (const u of uris) {
119
- var thing = $rdf.sym(u) // Attachment needs text label to disinguish I think not icon.
120
+ let thing = $rdf.sym(u) // Attachment needs text label to disinguish I think not icon.
120
121
  console.log('Dropped on mugshot thing ' + thing) // icon was: UI.icons.iconBase + 'noun_25830.svg'
121
122
  if (u.startsWith('http') && u.indexOf('#') < 0) {
122
123
  // Plain document
@@ -125,8 +126,9 @@ export function renderMugshotGallery (dom, subject) {
125
126
  thing = $rdf.sym('https:' + u.slice(5))
126
127
  }
127
128
  const options = { withCredentials: false, credentials: 'omit' }
129
+ let result
128
130
  try {
129
- var result = await kb.fetcher.webOperation('GET', thing.uri, options)
131
+ result = await kb.fetcher.webOperation('GET', thing.uri, options)
130
132
  } catch (err) {
131
133
  complain(
132
134
  `Gallery: fetch error trying to read picture ${thing} data: ${err}`
@@ -198,8 +200,18 @@ export function renderMugshotGallery (dom, subject) {
198
200
  droppedFileHandler
199
201
  )
200
202
  if (image) {
201
- img.setAttribute('src', image.uri)
203
+ // img.setAttribute('src', image.uri) use token and works with NSS but not with CSS
204
+ // we need to get image with authenticated fetch
205
+ store.fetcher._fetch(image.uri)
206
+ .then(function(response) {
207
+ return response.blob()
208
+ })
209
+ .then(function(myBlob) {
210
+ const objectURL = URL.createObjectURL(myBlob)
211
+ img.setAttribute('src', objectURL)
212
+ })
202
213
  UI.widgets.makeDraggable(img, image)
214
+
203
215
  }
204
216
  return img
205
217
  }
File without changes
File without changes
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "contacts-pane",
3
- "version": "2.4.8",
3
+ "version": "2.4.12-beta4",
4
4
  "description": "Contacts Pane: Contacts manager for Address Book, Groups, and Individuals.",
5
5
  "main": "./contactsPane.js",
6
6
  "scripts": {
7
- "build": "npm run build-lib",
8
- "build-lib": "npx eslint *.js && make",
7
+ "build": "npm run clean && npm run build-lib",
8
+ "clean": "rm -rf lib",
9
+ "build-lib": "mkdir lib && make && npx tsc-transpile-only src/*.ts --outDir lib",
9
10
  "lint": "eslint '*.js'",
10
11
  "lint-fix": "eslint '*.js' --fix",
11
- "test": "npm run lint",
12
- "prepublishOnly": "npm test",
13
- "postpublish": "git push origin master --follow-tags"
12
+ "test": "npm run lint && npm run build && npx tsc --target es2015 --moduleResolution node --allowSyntheticDefaultImports __tests__/unit/*.ts && jest __tests__/unit/*test.ts",
13
+ "jest": "jest __tests__/unit/*test.ts",
14
+ "prepublishOnly": "npm run lint && npm run build && npm run jest",
15
+ "postpublish": "git push origin main --follow-tags"
14
16
  },
15
17
  "repository": {
16
18
  "type": "git",
@@ -36,15 +38,26 @@
36
38
  },
37
39
  "homepage": "https://github.com/solid/contacts-pane",
38
40
  "dependencies": {
39
- "pane-registry": "^2.3.5",
40
- "rdflib": "^2.2.2",
41
- "solid-ui": "^2.4.2"
41
+ "solid-ui": "^2.4.33-beta4"
42
42
  },
43
43
  "devDependencies": {
44
- "eslint": "^7.18.0",
45
- "husky": "^4.3.8",
46
- "lint-staged": "^10.5.3",
47
- "standard": "^16.0.3"
44
+ "@babel/cli": "^7.24.1",
45
+ "@babel/core": "^7.24.3",
46
+ "@babel/preset-env": "^7.24.3",
47
+ "@babel/preset-typescript": "^7.24.1",
48
+ "@testing-library/jest-dom": "^6.4.2",
49
+ "@types/jest": "^29.5.12",
50
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
51
+ "@typescript-eslint/parser": "^6.21.0",
52
+ "eslint": "^8.57.0",
53
+ "eslint-plugin-import": "^2.29.1",
54
+ "husky": "^8.0.3",
55
+ "jest": "^29.7.0",
56
+ "jest-environment-jsdom": "^29.7.0",
57
+ "jest-fetch-mock": "^3.0.3",
58
+ "lint-staged": "^13.3.0",
59
+ "typescript": "^5.4.3",
60
+ "typescript-transpile-only": "0.0.4"
48
61
  },
49
62
  "husky": {
50
63
  "hooks": {
File without changes
@@ -0,0 +1,92 @@
1
+ // The Control with decorations
2
+
3
+ import { NamedNode } from 'rdflib'
4
+ import { store } from 'solid-logic'
5
+ import { ns, widgets, icons } from 'solid-ui'
6
+ import { renderAutoComplete } from './autocompletePicker' // dbpediaParameters
7
+ import { wikidataParameters } from './publicData'
8
+
9
+
10
+ const WEBID_NOUN = 'Solid ID'
11
+
12
+ const kb = store
13
+
14
+ const AUTOCOMPLETE_THRESHOLD = 4 // don't check until this many characters typed
15
+ const AUTOCOMPLETE_ROWS = 12 // 20?
16
+
17
+ const GREEN_PLUS = icons.iconBase + 'noun_34653_green.svg'
18
+ const SEARCH_ICON = icons.iconBase + 'noun_Search_875351.svg'
19
+
20
+ export async function renderAutocompleteControl (dom:HTMLDocument,
21
+ person:NamedNode, options, addOneIdAndRefresh): Promise<HTMLElement> {
22
+
23
+ async function autoCompleteDone (object, _name) {
24
+ const webid = object.uri
25
+ removeDecorated()
26
+ return addOneIdAndRefresh(person, webid)
27
+ }
28
+
29
+ async function greenButtonHandler (_event) {
30
+ const webid = await widgets.askName(dom, store, creationArea, ns.vcard('url'), null, WEBID_NOUN)
31
+ if (!webid) {
32
+ return // cancelled by user
33
+ }
34
+ return addOneIdAndRefresh(person, webid)
35
+ }
36
+ function removeDecorated () {
37
+ creationArea.removeChild(decoratedAutocomplete)
38
+ decoratedAutocomplete = null
39
+ }
40
+ async function searchButtonHandler (_event) {
41
+ if (decoratedAutocomplete) {
42
+ creationArea.removeChild(decoratedAutocomplete)
43
+ decoratedAutocomplete = null
44
+ } else {
45
+ decoratedAutocomplete = dom.createElement('div')
46
+ decoratedAutocomplete.setAttribute('style', 'display: flex; flex-flow: wrap;')
47
+ decoratedAutocomplete.appendChild(await renderAutoComplete(dom, acOptions, autoCompleteDone))
48
+ decoratedAutocomplete.appendChild(acceptButton)
49
+ decoratedAutocomplete.appendChild(cancelButton)
50
+ creationArea.appendChild(decoratedAutocomplete)
51
+ }
52
+ }
53
+
54
+ async function droppedURIHandler (uris) {
55
+ for (const webid of uris) { // normally one but can be more than one
56
+ await addOneIdAndRefresh(person, webid)
57
+ }
58
+ }
59
+
60
+ const queryParams = options.queryParameters || wikidataParameters
61
+ const acceptButton = widgets.continueButton(dom)
62
+ const cancelButton = widgets.cancelButton(dom, removeDecorated)
63
+ const klass = options.class
64
+ const acOptions = {
65
+ queryParams,
66
+ class:klass,
67
+ acceptButton,
68
+ cancelButton
69
+ }
70
+
71
+ var decoratedAutocomplete = null
72
+ // const { dom } = dataBrowserContext
73
+ options = options || {}
74
+ options.editable = kb.updater.editable(person.doc().uri, kb)
75
+
76
+ const creationArea = dom.createElement('div')
77
+ creationArea.setAttribute('style', 'display: flex; flex-flow: wrap;')
78
+
79
+ if (options.editable) {
80
+
81
+ // creationArea.appendChild(await renderAutoComplete(dom, options, autoCompleteDone)) wait for searchButton
82
+ creationArea.style.width = '100%'
83
+ const plus = creationArea.appendChild(widgets.button(dom, GREEN_PLUS, options.idNoun, greenButtonHandler))
84
+ widgets.makeDropTarget(plus, droppedURIHandler, null)
85
+ if (options.dbLookup) {
86
+ creationArea.appendChild(widgets.button(dom, SEARCH_ICON, options.idNoun, searchButtonHandler))
87
+ }
88
+ }
89
+ return creationArea
90
+ } // renderAutocompleteControl
91
+
92
+ // ends
@@ -0,0 +1,180 @@
1
+ /* Form field for doing autocompleete
2
+ */
3
+ import { BlankNode, NamedNode, st, Variable } from 'rdflib'
4
+ import { store } from 'solid-logic'
5
+ import { ns, style, widgets } from 'solid-ui'
6
+ import { renderAutoComplete } from './autocompletePicker' // dbpediaParameters
7
+
8
+ const AUTOCOMPLETE_THRESHOLD = 4 // don't check until this many characters typed
9
+ const AUTOCOMPLETE_ROWS = 12 // 20?
10
+
11
+ /**
12
+ * Render a autocomplete form field
13
+ *
14
+ * The same function is used for many similar one-value fields, with different
15
+ * regexps used to validate.
16
+ *
17
+ * @param dom The HTML Document object aka Document Object Model
18
+ * @param container If present, the created widget will be appended to this
19
+ * @param already A hash table of (form, subject) kept to prevent recursive forms looping
20
+ * @param subject The thing about which the form displays/edits data
21
+ * @param form The form or field to be rendered
22
+ * @param doc The web document in which the data is
23
+ * @param callbackFunction Called when data is changed?
24
+ *
25
+ * @returns The HTML widget created
26
+ */
27
+ // eslint-disable-next-line complexity
28
+ export function autocompleteField ( // @@ are they allowed too be async??
29
+ dom: HTMLDocument,
30
+ container: HTMLElement | undefined,
31
+ already,
32
+ subject: NamedNode | BlankNode | Variable,
33
+ form: NamedNode,
34
+ doc: NamedNode | undefined,
35
+ callbackFunction: (ok: boolean, errorMessage: string) => void
36
+ ): HTMLElement {
37
+
38
+ async function addOneIdAndRefresh (result, _name) {
39
+ const ds = kb.statementsMatching(subject, property as any) // remove any multiple values
40
+
41
+ let is = ds.map(statement => st(statement.subject, statement.predicate, result, statement.why)) // can include >1 doc
42
+ if (is.length === 0) {
43
+ // or none
44
+ is = [st(subject, property as any, result, doc)]
45
+ }
46
+ try {
47
+ await kb.updater.updateMany(ds, is)
48
+ } catch (err) {
49
+ callbackFunction(false, err)
50
+ box.appendChild(widgets.errorMessageBlock(dom, 'Autocomplete form data write error:' + err))
51
+ return
52
+ }
53
+ callbackFunction(true, '')
54
+ }
55
+
56
+ const kb = store
57
+ const formDoc = form.doc ? form.doc() : null // @@ if blank no way to know
58
+
59
+ const box = dom.createElement('tr')
60
+ if (container) container.appendChild(box)
61
+ const lhs = dom.createElement('td')
62
+ lhs.setAttribute('class', 'formFieldName')
63
+ lhs.setAttribute('style', ' vertical-align: middle;')
64
+ box.appendChild(lhs)
65
+ const rhs = dom.createElement('td')
66
+ rhs.setAttribute('class', 'formFieldValue')
67
+ box.appendChild(rhs)
68
+
69
+ const property = kb.any(form, ns.ui('property'))
70
+ if (!property) {
71
+ box.appendChild(
72
+ dom.createTextNode('Error: No property given for autocomplete field: ' + form)
73
+ )
74
+ return box
75
+ }
76
+ const searchClass = kb.any(form, ns.ui('searchClass'))
77
+ if (!searchClass) {
78
+ box.appendChild(
79
+ dom.createTextNode('Error: No searchClass given for autocomplete field: ' + form)
80
+ )
81
+ return box
82
+ }
83
+ const endPoint = kb.any(form, ns.ui('endPoint'))
84
+ if (!endPoint) {
85
+ box.appendChild(
86
+ dom.createTextNode('Error: No SPARQL endPoint given for autocomplete field: ' + form)
87
+ )
88
+ return box
89
+ }
90
+ const queryTemplate = kb.any(form, ns.ui('queryTemplate'))
91
+ if (!queryTemplate) {
92
+ box.appendChild(
93
+ dom.createTextNode('Error: No queryTemplate given for autocomplete field: ' + form)
94
+ )
95
+ return box
96
+ }
97
+ // It can be cleaner to just remove empty fields if you can't edit them anyway
98
+ const suppressEmptyUneditable = kb.anyJS(form, ns.ui('suppressEmptyUneditable'), null, formDoc)
99
+
100
+ lhs.appendChild(widgets.fieldLabel(dom, property as any, form))
101
+ const uri = widgets.mostSpecificClassURI(form)
102
+ let params = widgets.fieldParams[uri]
103
+ if (params === undefined) params = {} // non-bottom field types can do this
104
+ const theStyle = params.style || style.textInputStyle
105
+ const klass = kb.the(form, ns.ui('category'), null, formDoc)
106
+ /*
107
+ { label: string;
108
+ logo: string;
109
+ searchByNameQuery?: string;
110
+ searchByNameURI?: string;
111
+ insitituteDetailsQuery?: string;
112
+ endPoint?: string;
113
+ class: object
114
+ }
115
+ */
116
+
117
+ queryParams.endPoint = endPoint.uri
118
+
119
+ const searchByNameQuery = kb.the(form, ns.ui('searchByNameQuery'), null, formDoc)
120
+ queryParams.searchByNameQuery = searchByNameQuery
121
+
122
+ var queryParams = {label: 'from form', logo: '', class: klass, endPoint, searchByNameQuery}
123
+
124
+ const options = { // cancelButton?: HTMLElement,
125
+ // acceptButton?: HTMLElement,
126
+ class: klass,
127
+ queryParams }
128
+
129
+ // const acWiget = rhs.appendChild(await renderAutoComplete(dom, options, addOneIdAndRefresh))
130
+
131
+ // @@ set existing value is any
132
+ renderAutoComplete(dom, options, addOneIdAndRefresh).then(acWiget => rhs.appendChild(acWiget))
133
+
134
+
135
+ const field = dom.createElement('input')
136
+ ;(field as any).style = style.textInputStyle // Do we have to override length etc?
137
+ rhs.appendChild(field)
138
+ field.setAttribute('type', params.type ? params.type : 'text')
139
+
140
+ const size = kb.any(form, ns.ui('size')) // Form has precedence
141
+ field.setAttribute(
142
+ 'size',
143
+ size ? '' + size : params.size ? '' + params.size : '20'
144
+ )
145
+ const maxLength = kb.any(form, ns.ui('maxLength'))
146
+ field.setAttribute('maxLength', maxLength ? '' + maxLength : '4096')
147
+
148
+ doc = doc || widgets.fieldStore(subject, property as any, doc)
149
+
150
+ let obj = kb.any(subject, property as any, undefined, doc)
151
+ if (!obj) {
152
+ obj = kb.any(form, ns.ui('default'))
153
+ }
154
+ if (obj) {
155
+ /* istanbul ignore next */
156
+ field.value = obj.value || obj.value || ''
157
+ }
158
+ field.setAttribute('style', style)
159
+ if (!kb.updater) {
160
+ throw new Error('kb has no updater')
161
+ }
162
+ if (!kb.updater.editable((doc as NamedNode).uri)) {
163
+ field.readOnly = true // was: disabled. readOnly is better
164
+ ;(field as any).style = style.textInputStyleUneditable
165
+ // backgroundColor = textInputBackgroundColorUneditable
166
+ if (suppressEmptyUneditable && field.value === '') {
167
+ box.style.display = 'none' // clutter
168
+ }
169
+ return box
170
+ }
171
+ return box
172
+ }
173
+
174
+
175
+
176
+
177
+
178
+
179
+
180
+ // ends
@@ -0,0 +1,253 @@
1
+ /* Create and edit data using public data
2
+ **
3
+ ** organization conveys many distinct types of thing.
4
+ **
5
+ */
6
+ import { NamedNode } from 'rdflib'
7
+ import { store } from 'solid-logic'
8
+ import { style, widgets } from 'solid-ui'
9
+ import {
10
+ AUTOCOMPLETE_LIMIT, filterByLanguage, getPreferredLanguages, QueryParameters, queryPublicDataByName
11
+ } from './publicData'
12
+
13
+ const AUTOCOMPLETE_THRESHOLD = 4 // don't check until this many characters typed
14
+ const AUTOCOMPLETE_ROWS = 20 // 20?
15
+ const AUTOCOMPLETE_ROWS_STRETCH = 40
16
+ const AUTOCOMPLETE_DEBOUNCE_MS = 300
17
+
18
+ const autocompleteRowStyle = 'border: 0.2em solid straw;' // @@ white
19
+
20
+ /*
21
+ Autocomplete happens in four phases:
22
+ 1. The search string is too small to bother
23
+ 2. The search string is big enough, and we have not loaded the array
24
+ 3. The search string is big enough, and we have loaded array up to the limit
25
+ Display them and wait for more user input
26
+ 4. The search string is big enough, and we have loaded array NOT to the limit
27
+ but including all matches. No more fetches.
28
+ If user gets more precise, wait for them to select one - or reduce to a single
29
+ 5. Optionally waiting for accept button to be pressed
30
+ */
31
+
32
+ type AutocompleteOptions = { cancelButton?: HTMLElement,
33
+ acceptButton?: HTMLElement,
34
+ class: NamedNode,
35
+ queryParams: QueryParameters }
36
+
37
+ interface Callback1 {
38
+ (subject: NamedNode, name: string): void;
39
+ }
40
+
41
+ // The core of the autocomplete UI
42
+ export async function renderAutoComplete (dom: HTMLDocument, options:AutocompleteOptions, // subject:NamedNode, predicate:NamedNode,
43
+ callback: Callback1) {
44
+ function complain (message) {
45
+ const errorRow = table.appendChild(dom.createElement('tr'))
46
+ console.log(message)
47
+ errorRow.appendChild(widgets.errorMessageBlock(dom, message, 'pink'))
48
+ style.setStyle(errorRow, 'autocompleteRowStyle')
49
+ errorRow.style.padding = '1em'
50
+ }
51
+ function remove (ele?: HTMLElement) {
52
+ if (ele) {
53
+ ele.parentNode.removeChild(ele)
54
+ }
55
+ }
56
+ function finish (object, name) {
57
+ console.log('Auto complete: finish! ' + object)
58
+ // remove(options.cancelButton)
59
+ // remove(options.acceptButton)
60
+ // remove(div)
61
+ callback(object, name)
62
+ }
63
+ async function gotIt(object:NamedNode, name:string) {
64
+ if (options.acceptButton) {
65
+ (options.acceptButton as any).disabled = false
66
+ searchInput.value = name // complete it
67
+ foundName = name
68
+ foundObject = object
69
+ console.log('Auto complete: name: ' + name)
70
+ console.log('Auto complete: waiting for accept ' + object)
71
+ return
72
+ }
73
+ finish(object, name)
74
+ }
75
+
76
+ async function acceptButtonHandler (_event) {
77
+ if (searchInput.value === foundName) { // still
78
+ finish(foundObject, foundName)
79
+ } else {
80
+ (options.acceptButton as any).disabled = true
81
+ }
82
+ }
83
+
84
+ async function cancelButtonHandler (_event) {
85
+ console.log('Auto complete: Canceled by user! ')
86
+ div.innerHTML = '' // Clear out the table
87
+ }
88
+
89
+ function nameMatch (filter:string, candidate: string):boolean {
90
+ const parts = filter.split(' ') // Each name part must be somewhere
91
+ for (let j = 0; j < parts.length; j++) {
92
+ const word = parts[j]
93
+ if (candidate.toLowerCase().indexOf(word) < 0) return false
94
+ }
95
+ return true
96
+ }
97
+
98
+ function cancelText (_event) {
99
+ searchInput.value = '';
100
+ if (options.acceptButton) {
101
+ (options.acceptButton as any).disabled == true; // start again
102
+ }
103
+ candidatesLoaded = false
104
+ }
105
+
106
+ function thinOut (filter) {
107
+ var hits = 0
108
+ var pick = null, pickedName = ''
109
+ for (let j = table.children.length - 1; j > 0; j--) { // backwards as we are removing rows
110
+ let row = table.children[j]
111
+ if (nameMatch(filter, row.textContent)) {
112
+ hits += 1
113
+ pick = row.getAttribute('subject')
114
+ pickedName = row.textContent
115
+ ;(row as any).style.display = ''
116
+ ;(row as any).style.color = 'blue' // @@ chose color
117
+ } else {
118
+ ;(row as any).style.display = 'none'
119
+ }
120
+ }
121
+ if (hits == 1) { // Maybe require green confirmation button be clicked?
122
+ console.log(` auto complete elimination: "${filter}" -> "${pickedName}"`)
123
+ gotIt(store.sym(pick), pickedName) // uri, name
124
+ }
125
+ }
126
+
127
+ function clearList () {
128
+ while (table.children.length > 1) {
129
+ table.removeChild(table.lastChild)
130
+ }
131
+ }
132
+
133
+ async function inputEventHHandler(_event) {
134
+ if (runningTimeout) {
135
+ clearTimeout(runningTimeout)
136
+ }
137
+ setTimeout(refreshList, AUTOCOMPLETE_DEBOUNCE_MS)
138
+ }
139
+
140
+ async function refreshList() {
141
+ if (inputEventHandlerLock) {
142
+ console.log (`Ignoring "${searchInput.value}" because of lock `)
143
+ return
144
+ }
145
+ inputEventHandlerLock = true
146
+ var languagePrefs = await getPreferredLanguages()
147
+ const filter = searchInput.value.trim().toLowerCase()
148
+ if (filter.length < AUTOCOMPLETE_THRESHOLD) { // too small
149
+ clearList()
150
+ candidatesLoaded = false
151
+ numberOfRows = AUTOCOMPLETE_ROWS
152
+ } else {
153
+ if (allDisplayed && lastFilter && filter.startsWith(lastFilter)) {
154
+ thinOut(filter) // reversible?
155
+ inputEventHandlerLock = false
156
+ return
157
+ }
158
+ var bindings
159
+ try {
160
+ bindings = await queryPublicDataByName(filter, OrgClass, options.queryParams)
161
+ // bindings = await queryDbpedia(sparql)
162
+ } catch (err) {
163
+ complain('Error querying db of organizations: ' + err)
164
+ inputEventHandlerLock = false
165
+ return
166
+ }
167
+ candidatesLoaded = true
168
+ const loadedEnough = bindings.length < AUTOCOMPLETE_LIMIT
169
+ if (loadedEnough) {
170
+ lastFilter = filter
171
+ } else {
172
+ lastFilter = null
173
+ }
174
+ clearList()
175
+ const slimmed = filterByLanguage(bindings, languagePrefs)
176
+ if (loadedEnough && slimmed.length <= AUTOCOMPLETE_ROWS_STRETCH) {
177
+ numberOfRows = slimmed.length // stretch if it means we get all items
178
+ }
179
+ allDisplayed = loadedEnough && slimmed.length <= numberOfRows
180
+ console.log(` Filter:"${filter}" bindings: ${bindings.length}, slimmed to ${slimmed.length}; rows: ${numberOfRows}, Enough? ${loadedEnough}, All displayed? ${allDisplayed}`)
181
+ slimmed.slice(0,numberOfRows).forEach(binding => {
182
+ const row = table.appendChild(dom.createElement('tr'))
183
+ style.setStyle(row, 'autocompleteRowStyle')
184
+ var uri = binding.subject.value
185
+ var name = binding.name.value
186
+ row.setAttribute('style', 'padding: 0.3em;')
187
+ row.setAttribute('subject', uri)
188
+ row.style.color = allDisplayed ? '#080' : '#000' // green means 'you should find it here'
189
+ row.textContent = name
190
+ row.addEventListener('click', async _event => {
191
+ console.log(' click row textContent: ' + row.textContent)
192
+ console.log(' click name: ' + name)
193
+ gotIt(store.sym(uri), name)
194
+ })
195
+ })
196
+ }
197
+ inputEventHandlerLock = false
198
+ } // refreshList
199
+
200
+
201
+ /* sparqlForSearch
202
+ *
203
+ * name -- e.g., "mass"
204
+ * theType -- e.g., <http://umbel.org/umbel/rc/EducationalOrganization>
205
+ */
206
+ function sparqlForSearch (name:string, theType:NamedNode):string {
207
+ let clean = name.replace(/\W/g, '') // Remove non alphanum so as to protect regexp
208
+ const sparql = `select distinct ?subject, ?name where {
209
+ ?subject a <${theType.uri}>; rdfs:label ?name
210
+ FILTER regex(?name, "${clean}", "i")
211
+ } LIMIT ${AUTOCOMPLETE_LIMIT}`
212
+ return sparql
213
+ }
214
+
215
+ const queryParams: QueryParameters = options.queryParams
216
+ const OrgClass = options.class // kb.sym('http://umbel.org/umbel/rc/EducationalOrganization') // @@@ other
217
+ if (options.acceptButton) {
218
+ options.acceptButton.addEventListener('click', acceptButtonHandler, false)
219
+ }
220
+ if (options.cancelButton) {
221
+ // options.cancelButton.addEventListener('click', cancelButtonHandler, false)
222
+ }
223
+
224
+ let candidatesLoaded = false
225
+ let runningTimeout = null
226
+ let inputEventHandlerLock = false
227
+ let allDisplayed = false
228
+ var lastFilter = null
229
+ var numberOfRows = AUTOCOMPLETE_ROWS
230
+ var div = dom.createElement('div')
231
+ var foundName = null // once found accepted string must match this
232
+ var foundObject = null
233
+ var table = div.appendChild(dom.createElement('table'))
234
+ table.setAttribute('style', 'max-width: 30em; margin: 0.5em;')
235
+ const head = table.appendChild(dom.createElement('tr'))
236
+ style.setStyle(head, 'autocompleteRowStyle')
237
+ const cell = head.appendChild(dom.createElement('td'))
238
+ const searchInput = cell.appendChild(dom.createElement('input'))
239
+ searchInput.setAttribute('type', 'text')
240
+ const searchInputStyle = style.searchInputStyle ||
241
+ 'border: 0.1em solid #444; border-radius: 0.5em; width: 100%; font-size: 100%; padding: 0.1em 0.6em' // @
242
+ searchInput.setAttribute('style', searchInputStyle)
243
+ searchInput.addEventListener('keyup', function (event) {
244
+ if (event.keyCode === 13) {
245
+ acceptButtonHandler(event)
246
+ }
247
+ }, false);
248
+
249
+ searchInput.addEventListener('input', inputEventHHandler)
250
+ return div
251
+ } // renderAutoComplete
252
+
253
+ const ends = 'ENDS';
package/src/forms.ttl CHANGED
@@ -166,7 +166,7 @@ vcard:Individual
166
166
 
167
167
  :oneAddress
168
168
  a ui:Group ;
169
- ui:parts ( :id1409437207443 :id1409437292400 :id1409437421996 :id1409437467649 :id1409437569420 :id1409437646712 ).
169
+ ui:parts ( :id1409437207443 :id1409437292400 :id1409437421996 :id1409437467649 :id1409437569420 ). # :id1409437646712
170
170
 
171
171
  :id1409437207443
172
172
  a ui:SingleLineTextField ;
File without changes