diff --git a/.eslintignore b/.eslintignore index a65b41774..f3ebf520e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,7 @@ lib +*.html +*.md +*.json +Documentation +Documentation/forms-intro.html +package.json diff --git a/Documentation/form-ecosystem.html b/Documentation/form-ecosystem.html new file mode 100644 index 000000000..c5408c689 --- /dev/null +++ b/Documentation/form-ecosystem.html @@ -0,0 +1,276 @@ + + + + + + + An Ecosystem of forms + + + + +

Forms in the ecosystem

+ +

+ The User Interface ontology at http://www.w3.org/ns/ui defines + RDF terms for describing forms. The + solid-ui project + provides functions to use these forms within your web application to + create a quick user interface solution. An introduction + basically describes how forms work. This document describes how forms fit into the ecosystem of apps, + views/panes, and data browsers and stuff. +

+ +

How to forms end up getting used? How do form end up getting created, copied, and modified?

+ +

+ The simplest way is for a developer of a web app to included the mashlib, and then insert a form + into the web page as a control, specifying the form and the subject, and the place he data is to be + stored. This is a good way of building workflow quickly. It also separates out the + logic of the form from the presentation details of style. + A solid-ui form is not just a set of components, it is a binding to the underlying linked data. + So no more code is needed by the developer. +

+

+ Now look at other ways in which forms can be used within connected data system like the mashlib and + the databrowser. +

+

Forms and classes

+ +

Forms create graphs of information starting off a root node, a seed node, if you like. + They are made for recording information about things of particular types, + in particular RFDS Classes. There are two relationships: +

+ + + + + + + + + + + + + + + +
ui:creationFormCreation form + A form which may be used to collect information about a + hitherto locally undocumented instance instance of this class. +
ui:annotationFormAnnotation form + A form which may be used to add more information to an existing + instance of this class which we know something about. Anything from + adding just add one more fact, to adding a whole lot of information about a specific + facet of the thing. +
+

A creation form must have enough in it to make a record which makes sense. + Just having height and weight, for example, produces a node with nothing to identify + what the thing is. A creation form may have say name, address, phone number + so that the record is useful in apps. Once you have made something using a creation form, then you can + later add more data with an annotation form. You can also use any creation form + as an annotation form later to edit the data.

+

So in the form ecosystem, when a user wants to create + something new, it is reasonable to give them a list of creation forms, + for whatever classes they may be interested in. But then when they are looking in the UI at + an object of a particular class, then it is reasonable to offer them those forms (of either type) relate to that Class of the object, if they + want to record more things. +

+

Using RDFS inference is useful here. If there are forms declared as being for Person class, + and the user is looking at something which is of class Student, then it follows that + the subject is also of type Person, and can so those forms may be used. + One trick is to present first the forms of the most specific type (Student) before the ones for the more generic type (Person) + as they may at a guess be more relevant. + +

+

How to find out forms for a given class? A simple way is to include the link to the + form in the ontology. This obviously gives it a pretty definitive status.

+

Many existing ontologies

+

Communities, projects, organizations and groups may want to share sets of forms they use, and + so community-based indexes of these are interesting. +

+ +

Forms from shapes

+

+ Forms are very simpler to shapes, as noted elsewhere. Forms can be generated from shapes. +

+ +

Creating forms

+

Filling in forms should be something which is easy for any user to do. + It will be conceptually complicated, should be as easy as possible also for them to make a new form. + How does the work flow work? This one is easy to implement but not very easy. +

+ +

Maybe they want to edit an existing form.

+ + +

There are many ways of creating and editing forms, but because there is a form for editing forms, + one way is with that. +

+ +

Forms and Ontologies

+

If you are using the form form to create a form, then for each field you will have to supply a + predicate. The solid-ui system will give you a selector to + chose from all the predicates it knows about. + For this reason it is good to load in ontologies you want to use + before you start editing. + The form playground has tools to do that. +

+

Some days, the user finds that the good old vocabularies the Solid project has been using + provide all the concepts they need. Some days, they will have to look further afield and find + an existing ontology just new to them. Some days they will find one almost perfect and want to imagine + of suggest + friendly amendment to it. Some days they will decide to just create a new ontology, + for speed, for control, or for fun. + Some days they will find the ad hoc ontology they created at one point is now one they want to + get people to adopt or link to from their own ontologies.

+

+ So one way or another users will become ontology users, + and then will become ontology editors and creators. We need tools to enable these workflows, + particularly the social collaborative parts. These tools are not covered here. + It is worth saying that the power of systems in a big world depends on wide interop, and you get + interop from shared vocabularies, and you get shared vocabularies from hard work, where there is much glory + but no free lunch. +

+

So if ontology editing becomes a practical part of these workflows, + can we use forms as one way of editing an ontology? + It seems reasonable, though a custom engineered UI may be much nicer. +

+

+ + + + + + +
To we have a ...ontologyshapeform
for a ...
ontologyRDFS, OWL????
shapeSHACLshape shape
formUI??FormForm
+

+

Filling in a bit of this table seems like it may by a good move. + +

+ +

Conclusion

+

A worthy goal is to build a platform where collaborative systems can be build + by their users without having to write code. Forms can contribute to that quest.

+
+ + diff --git a/Documentation/forms-intro.html b/Documentation/forms-intro.html index 806184738..8faf1b4d4 100644 --- a/Documentation/forms-intro.html +++ b/Documentation/forms-intro.html @@ -1,283 +1,600 @@ - - -solid-ui: Introduction using forms - - - -

Using Forms in the UI ontology

- -

The User Interface ontology at http://www.w3.org/ns/ui defines RDF terms for - describing forms. The solid-ui projet provides functions to - use these forms within your web application to create a quick user interface solution. This document describes how. -

- -

The form system allows you to define a user interface - declaratively in RDF. - In your web app, you then: -

    -
  1. make sure the ontology files are loaded
  2. -
  3. load the file with the form itself
  4. -
  5. call UI.widgets.appendForm(dom, container, {}, subject, form, doc, callback)
  6. -
-

-

- where - - - - - - - - -
domis the DOM HTMLDocument object, a/k/a document
containeris a DOM element to contain the form
{}are unused at present
subjectis the RDF thing about which data will be stored
formis the RDF object in the store for the form
docis the RDF document on the web where the data will be stored. Often, subject.doc()
callbackis a function taking an error flag and a message (if the error flag is true)
- If the form is a complex form, as the user adds more data, more form UI will be created. - The data in each field is saved back to the web the moment the user has entered it. There is no general Save Button. -

-

-There is a form form for editing forms. -It is in the form ontology itself. -

- -

-You can of course go and write other implementations of the form system using -your favorite user interface language. -

-

Go to the source

-

- -

Form field types

-

Form fields may be named or blank nodes in your file; -the form system does not care. -It is often useful to name them to keep track of them. -

-

Below, all Field Classes and Properties are in the UI namespace, - http://www.w3.org/ns/ui#, except - the data types, like Integer, which are in the normal XSD namespace. -

-

- Here are some properties which you can use with any field (except the documentation fields). - - - - -
labelStringA label for the form field. This is the prompt for the user, e.g., "Name", "Employer".
propertyrdf:PropertyWhen the user enters the data, it is stored in the web as a triple with this property as its predicate.
default[according to field type] OptionalThe input control is set to this value by default. - It is easiest for the user to enter this value. (This value is not automatically stored by the form system if the user does not select or enter it in some way.
-Other properties are given for each field type. -

-

Form

-

The form itself has a collection of fields. -The parts property gives an order list of -the fields in each form. - - - - -
partsrdf:Collection (aka List, Array) of FieldThe parts of the form in the order in which they are
partField (Obsolete)A field which is a part of the form or group. This property is obsolete. Use parts.
-If you use the obsolete "part" method for listing the parts of a form, then -each field needs an additional property: - - - -
sequenceIntegerThe parts of the form in the order in which they are
- -For each part, declare its type, and the extra data that type requires, as below. -

-

Group

-

Group is a field which is just a collection of other fields. - It is in fact interchangeable with Form. - - -

Single Value fields - Numeric

- -

These prompt the user for a single value. They typically take default values, -and min and max values.

- -

BooleanField

-

A checkbox on the form, stored an RDF boolean true or false value.

- -

TriStateField

-

A checkbox on the form, stores an RDF boolean true or false value, - or no value if the box is left in its third, blank state.

- -

IntegerField

-

An RDF integer value

- -

DecimalField

-

An RDF decimal value. Useful for monetary amounts

- -

FloatField

-

A floating point number

- - -

Single Value fields: Special Types

- -

ColorField

-

A color picker is used, and genertes a string which is a CSS_compatible color in -a string like #ffeebb

- -

DateField

- -

Uses a date picker on a good browser. - Leaves an RDF date literal as is value.

-

DateTimeField

-

- -Leaves an RDF dateTime literal as is value.

-

PhoneField

-

-Leaves as its value a named node with a uri which starts 'tel:'

-

EmailField

-

- -Leaves as its value a named now with a uri which starts 'mailto:' -

-

Single Value fields - Text

- -

SingleLineTextField

-

- -

MultiLineTextField

-

- -

Complex fields

-

Group

-

- -A group is simply a static set of fields of any type. -Its properties are the same as for Form. -

-

Choice

-

The user choses an item from a class. - - - - - - -
fromrdfs:ClassThe selected thing must be a member of this class. E.g. Person.
propertyrdf:PropertyWhen the item is found, the new data links it from the subject with tis property. E.g. friend.
canMintNewxsd:BooleanIf the user doesn't find the thing they want, can they introduce a item of that class by filling in a form about it? [Boolean]
-If a new thing is minted, that will be done with a form which is a ui:creationForm for the class. -

- -

Multiple

-

- -

When the subject can have several of the same thing, -like friends, ro phone numbers, then the Multiple field -allows this. The user clicks on the green plus icon, and is prompted -for a subform for the related thing. -The user can also delete existing ones.

-

For each new thing, the system generates an arbitrary (timestamp) URI within the file -where the data is being stored. The subform is then about that thing: the subject of the subform is not -the subject of the original form. It is the field, or the address, and so on. -

-

Classifier

-

- - -
categoryrdfs:ClassThe object will already be in this class. - The user will select subclasses of this class.
- This form field leverages the ontology heavily. - It pulls the subclasses of the given class, and makes a pop-up menu -for the user to chose one. -If and only if the ontology says that the class is a disjoint union (owl:disjointUnionOf) of the subclasses, then the -user interface will only allow the user to pick one. -If the user picks a subclass, and the ontology shows that that subclass has its own subclasses, then the -user will be prompted to pick one of those, to (if they like) further refine the selection. And so on. -

- -

The classifier pops a menu to allow the user to select a set of valued to classify the subject. -

-

Options

- -

And Options field is the 'case statement' of the form system. - It will chose at runtime a subfield depending - on a property, often the type, of the subject. Often used after a classifier. -

-

- - - - - -
Options propertyrangesignificance
dependingOnrdf:PropertyThe predicate in the data used to select the case.
caseCaseA case object, with for x use y. (2 or more cases)
-and for each case: - - - - -
Case propertyrangesignificance
for[The range of the dependingOn property]The value this case applies to
useFieldsub form to be used in case the value matches the "for"
- -

- - -

Documentation fields

-

Heading

-

Help the user find parts of a long form, or just for a title of a short form. - - -
contentsStringThe text content of the heading
- -

- -

Comment

-

Use comments in the form to help users understand what is going on, -what their options are, and what the fields mean. - - - -
contentsStringThe text content of the comment. - (This should be displayed by form systems as pre-wrap mode)
-

- - -

Conclusion

-

The form language and the form implementation in solid-ui - can't do everything, but it can handle - a pretty wide selection of tasks in common -daily life at home and at work. -It can be vary efficient as developers can reuse material between forms. -Users can even generate their own forms. -

-Future directions include separate implementations of the form UI code in -for various platforms, and using various UI frameworks. -There may also be extension of the system with new field types, -more options for setting style from various sources, -

- - - + + + + + + + solid-ui: Introduction using forms + + + + +

Using Forms in the UI ontology

+ +

+ The User Interface ontology at http://www.w3.org/ns/ui defines + RDF terms for describing forms. The + solid-ui project + provides functions to use these forms within your web application to + create a quick user interface solution. This document describes how. +

+ +

+ The form system allows you to define a user interface declaratively in + RDF. In your web app, you then: +

+ +
    +
  1. make sure the ontology files are loaded
  2. + +
  3. load the file with the form itself
  4. + +
  5. + call + UI.widgets.appendForm(dom, container, {}, subject, form, doc, + callback) +
  6. +
+ +

where

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
domis the DOM HTMLDocument object, a/k/a document
containeris a DOM element to contain the form
{}are unused at present
subjectis the RDF thing about which data will be stored
formis the RDF object in the store for the form
doc + is the RDF document on the web where the data will be stored. Often, + subject.doc() +
callback + is a function taking an error flag and a message (if the error flag is + true) +
+ + If the form is a complex form, as the user adds more data, more form UI will + be created. The data in each field is saved back to the web the moment the + user has entered it. There is no general Save Button. + +

+ There is a form form for editing forms. It is in the form ontology itself. +

+ +

+ You can of course go and write other implementations of the form system + using your favorite user interface language. +

+ +

Go to the source

+ + + +

Form field types

+ +

+ Form fields may be named or blank nodes in your file; the form system does + not care. It is often useful to name them to keep track of them. +

+ +

+ Below, all Field Classes and Properties are in the UI namespace, + http://www.w3.org/ns/ui#, except the data types, like Integer, which are in the normal + XSD + namespace. +

+ +

+ Here are some properties which you can use with any field (except the + documentation fields). +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
labelString + A label for the form field. This is the prompt for the user, e.g., + "Name", "Employer". +
propertyrdf:Property + When the user enters the data, it is stored in the web as a triple + with this property as its predicate. +
default[according to field type] Optional + The input control is set to this value by default. It is easiest for + the user to enter this value. (This value is not automatically + stored by the form system if the user does not select or enter it in + some way. +
+ +

+ Other properties are given for each field type. +

+ +

Form

+ +

+ The form itself has a collection of fields. The parts property + gives an order list of the fields in each form. +

+ + + + + + + + + + + + + + + + + +
partsrdf:Collection (aka List, Array) of FieldThe parts of the form in the order in which they are
partField (Obsolete) + A field which is a part of the form or group. This property is + obsolete. Use parts. +
+ + If you use the obsolete "part" method for listing the parts of a form, then + each field needs an additional property: + + + + + + + + + +
sequenceIntegerThe parts of the form in the order in which they are
+ + For each part, declare its type, and the extra data that type requires, as + below. + +

Group

+ +

+ Group is a field which is just a collection of other fields. It is in fact + interchangeable with Form. +

+ +

Single Value fields - Numeric

+ These prompt the user for a single value. They typically take default + values, and min and max values. + +

BooleanField

+ +

A checkbox on the form, stored an RDF boolean true or false value.

+ +

TriStateField

+ +

+ A checkbox on the form, stores an RDF boolean true or false value, or no + value if the box is left in its third, blank state. +

+ +

IntegerField

+ +

An RDF integer value

+ +

DecimalField

+ +

An RDF decimal value. Useful for monetary amounts

+ +

FloatField

+ +

A floating point number

+ +

Single Value fields: Special Types

+ +

ColorField

+ +

+ A color picker is used, and generates a string which is a CSS_compatible + color in a string like #ffeebb +

+ +

DateField

+ +

+ Uses a date picker on a good browser. Leaves an RDF date literal as its + value. +

+ +

DateTimeField

+ +

Leaves an RDF dateTime literal as its value.

+ +

PhoneField

+ +

Leaves as its value a named node with a tel: scheme URI

+ +

EmailField

+ +

Leaves as its value a named node with a 'mailto:' scheme URI

+ +

Single Value fields - Text

+ +

SingleLineTextField

+ +

MultiLineTextField

+ +

Complex fields

+ +

Group

+ +

+ A group is simply a static set of fields of any type. Its properties are + the same as for Form. +

+ +

Choice

+ +

The user choses an item from a class.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
fromrdfs:ClassThe selected thing must be a member of this class, e.g., Person.
propertyrdf:Property + When the item is found, the new data links it from the subject with + this property, e.g., friend. +
canMintNewxsd:Boolean + If the user doesn't find the thing they want, can they introduce an + item of that class by filling in a form about it? [Boolean] +
+ + If a new thing is minted, that will be done with a form which is a + ui:creationForm for the class. + +

Multiple

+ +

+ When the subject can have several of the same thing, like friends or + phone numbers, then the Multiple field allows this. The user clicks on the + green plus icon, and is prompted for a subform for the related thing. The + user can also delete existing ones. +

+ +

+ For each new thing, the system generates an arbitrary (timestamp) URI + within the file where the data is being stored. The subform is then about + that thing; the subject of the subform is not the subject of the original + form. It is the field, or the address, and so on. +

+ +

Classifier

+ + + + + + + + + +
categoryrdfs:Class + The object will already be in this class. The user will select + subclasses of this class. +
+ + This form field leverages the ontology heavily. It pulls the subclasses of + the given class, and makes a pop-up menu for the user to chose one. If and + only if the ontology says that the class is a disjoint union + (owl:disjointUnionOf) of the subclasses, then the user interface will only + allow the user to pick one. If the user picks a subclass, and the ontology + shows that that subclass has its own subclasses, then the user will be + prompted to pick one of those, to (if they like) further refine the + selection. And so on. + +

+ The classifier pops a menu to allow the user to select a set of values to + classify the subject. +

+ +

Options

+ +

+ An Options field is the 'case statement' of the form system. It will + choose at runtime a subfield depending on a property, often the type, of + the subject. Often used after a classifier. +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Options propertyrangesignificance
dependingOnrdf:PropertyThe predicate in the data used to select the case.
caseCaseA case object, with for x use y. (2 or more cases)
+ + and for each case: + + + + + + + + + + + + + + + + + + + + + + + + + +
Case propertyrangesignificance
for[The range of the dependingOn property]The value this case applies to
useFieldsub form to be used in case the value matches the "for"
+ +

Documentation fields

+ +

Heading

+ +

+ Help the user find parts of a long form, or just for a title of a short + form. +

+ + + + + + + + + +
contentsStringThe text content of the heading
+ +

Comment

+ +

+ Use comments in the form to help users understand what is going on, what + their options are, and what the fields mean. +

+ + + + + + + + + +
contentsString + The text content of the comment. (This should be displayed by form + systems as pre-wrap mode.) +
+ +

Conclusion

+ +

+ The form language and the form implementation in solid-ui can't do + everything, but it can handle a pretty wide selection of tasks in common + daily life at home and at work. It can be vary efficient as developers can + reuse material between forms. Users can even generate their own forms. +

+ Future directions include separate implementations of the form UI code in + for various platforms, and using various UI frameworks. There may also be + extensions of the system with new field types, more options for setting style + from various sources, etc. + diff --git a/README.md b/README.md index 1be116e85..1a0754969 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,18 @@ [![NPM Package](https://img.shields.io/npm/v/solid-ui.svg)](https://www.npmjs.com/package/solid-ui) - User Interface widgets and utilities for Solid -These are HTML5 widgets which connect to a solid store. Building blocks for solid-based apps. +These are HTML5 widgets which connect to a solid store. Building blocks for solid-based apps. A selection + ``` var UI = require('solid-ui') var acl = require('solid-ui').acl ``` + The submodules at the moment include log, acl, acl-control, messageArea, etc - A login widget @@ -29,7 +30,7 @@ The submodules at the moment include log, acl, acl-control, messageArea, etc The typical style of the widgets is to know what data it has been derived from, allow users to edit it, and to automatically sync with data as it changes in the future. -TO see how these are used, see the panes which use them within the solid-app-set +TO see how these are used, see the panes which use them within the solid-app-set The level of support for this varies. diff --git a/package-lock.json b/package-lock.json index bbc7c8ecf..6d59efccb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2293,16 +2293,78 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.6.1.tgz", - "integrity": "sha512-Z0rddsGqioKbvqfohg7BwkFC3PuNLsB+GE9QkFza7tiDzuHoy0y823Y+oGNDzxNZrYyLjqkZtCTl4vCqOmEN4g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.7.0.tgz", + "integrity": "sha512-H5G7yi0b0FgmqaEUpzyBlVh0d9lq4cWG2ap0RKa6BkF3rpBb6IrAoubt1NWh9R2kRs/f0k6XwRDiDz3X/FqXhQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.6.1", + "@typescript-eslint/experimental-utils": "2.7.0", "eslint-utils": "^1.4.2", "functional-red-black-tree": "^1.0.1", "regexpp": "^2.0.1", "tsutils": "^3.17.1" + }, + "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.7.0.tgz", + "integrity": "sha512-9/L/OJh2a5G2ltgBWJpHRfGnt61AgDeH6rsdg59BH0naQseSwR7abwHq3D5/op0KYD/zFT4LS5gGvWcMmegTEg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.7.0", + "eslint-scope": "^5.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.7.0.tgz", + "integrity": "sha512-vVCE/DY72N4RiJ/2f10PTyYekX2OLaltuSIBqeHYI44GQ940VCYioInIb8jKMrK9u855OEJdFC+HmWAZTnC+Ag==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "glob": "^7.1.4", + "is-glob": "^4.0.1", + "lodash.unescape": "4.0.1", + "semver": "^6.3.0", + "tsutils": "^3.17.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@typescript-eslint/experimental-utils": { @@ -2557,9 +2619,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz", + "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==" }, "babel-plugin-dynamic-import-node": { "version": "2.3.0", @@ -2910,6 +2972,29 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "contains-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", @@ -2990,6 +3075,22 @@ "parse-json": "^4.0.0" } }, + "cross-fetch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.4.tgz", + "integrity": "sha512-MSHgpjQqgbT/94D4CyADeNoYh52zMkCX4pcJvPP5WqPsLFMKjr2TCMg381ox5qI0ii2dPwaLx/00477knXqXVw==", + "requires": { + "node-fetch": "2.6.0", + "whatwg-fetch": "3.0.0" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + } + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -3028,7 +3129,7 @@ }, "debug-log": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debug-log/-/debug-log-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/debug-log/-/debug-log-1.0.1.tgz", "integrity": "sha1-IwdjLUwEOCuN+KMvcLiVBG1SdF8=", "dev": true }, @@ -3499,7 +3600,7 @@ "dependencies": { "doctrine": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "resolved": "http://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", "dev": true, "requires": { @@ -3887,9 +3988,9 @@ }, "dependencies": { "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" } } }, @@ -4777,9 +4878,9 @@ } }, "jsonld": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-1.8.0.tgz", - "integrity": "sha512-a3bwbR0wqFstxKsGoimUIIKBdfJ+yb9kWK+WK7MpVyvfYtITMpUtF3sNoN1wG/W+jGDgya0ACRh++jtTozxtyQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-1.8.1.tgz", + "integrity": "sha512-f0rusl5v8aPKS3jApT5fhYsdTC/JpyK1PoJ+ZtYYtZXoyb1J0Z///mJqLwrfL/g4NueFSqPymDYIi1CcSk7b8Q==", "requires": { "canonicalize": "^1.0.1", "rdf-canonize": "^1.0.2", @@ -5441,9 +5542,9 @@ "dev": true }, "n3": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/n3/-/n3-1.2.1.tgz", - "integrity": "sha512-5va8zsh02owDul7+5bj35cGOzWU7DeJHwQK3F8NDKtiYqPgWcFTg/zLxJs1JeRF2n6j5PI/eR9DCokS7nLrevA==" + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.3.4.tgz", + "integrity": "sha512-Eu5EVYGncuwiTlOV1J6p3OFBNSfI84D+fW0o8o5s2aRowO3yRcM4SvqPTOKzCCJutRvaXP0J9GIzwrP6tINm2Q==" }, "nanomatch": { "version": "1.2.13", @@ -6050,9 +6151,9 @@ } }, "psl": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", - "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.6.0.tgz", + "integrity": "sha512-SYKKmVel98NCOYXpkwUqZqh0ahZeeKfmisiLIcEZdsb+WbLv02g/dI5BUmZnIyOe7RzZtLax81nnb2HbvC2tzA==" }, "pump": { "version": "3.0.0", @@ -6876,16 +6977,16 @@ } }, "solid-auth-cli": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/solid-auth-cli/-/solid-auth-cli-1.0.8.tgz", - "integrity": "sha512-cZYNLM6/BDwbcywsrfFRIrGVjTPc/f3snubabqd1WRCvNwW7wkvpo0yvoHp3NQjVAfKHw5kIrZtaV8IRHPK/KQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/solid-auth-cli/-/solid-auth-cli-1.0.10.tgz", + "integrity": "sha512-fEvgpdr429peDFmXQk2FDeD14bGsHccT+4Pn7l8Gw6b3aCWYCaCXxkSEdiHkSt6N6rGx0sGiq9Yp3DB9Z/I6kg==", "requires": { "@solid/cli": "^0.1.1", "async": "^2.6.1", - "isomorphic-fetch": "^2.2.1", + "cross-fetch": "^3.0.4", "jsonld": "^1.4.0", "n3": "^1.0.3", - "solid-rest": "^1.0.7" + "solid-rest": "^1.0.9" }, "dependencies": { "async": { @@ -6921,9 +7022,9 @@ "integrity": "sha512-yQGQlTNDVtcMfzLz7OwL6Z8lJy9rN1ep0MgX28DPcja0DtA5pu1MzTpBVD9kvXl9X6eSEX73I7IYt0cUim0DrA==" }, "solid-rest": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/solid-rest/-/solid-rest-1.0.7.tgz", - "integrity": "sha512-OiNKV1nW00RVdnd88HfCVIcY+LKI6VAc6DbY0Tujy5/eiURkCnxqAkMJXLd10lKmpQZ2NNYpsfBN/QWnoIBczA==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/solid-rest/-/solid-rest-1.0.9.tgz", + "integrity": "sha512-+V8h180TkQhGK9BkLfzm4aSJZaqyEst0TmBkvlXB1bchoO2F7+yM//m6HAZBbTnDcDhf1jsrt34nfNgXf6t8tg==", "requires": { "concat-stream": "^2.0.0", "fs-extra": "^8.0.1", @@ -6931,31 +7032,10 @@ "node-fetch": "^2.6.0" }, "dependencies": { - "concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } } } }, @@ -7314,7 +7394,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } diff --git a/package.json b/package.json index 637d85b75..51298f3cf 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@babel/preset-env": "^7.6.2", "@babel/preset-typescript": "^7.6.0", "@types/rdflib": "^0.20.1", - "@typescript-eslint/eslint-plugin": "^2.6.1", + "@typescript-eslint/eslint-plugin": "^2.7.0", "@typescript-eslint/parser": "^2.6.1", "eslint": "^6.6.0", "husky": "^3.0.9", diff --git a/src/acl/acl-control.ts b/src/acl/acl-control.ts index 140db5f6e..a6c10a62b 100644 --- a/src/acl/acl-control.ts +++ b/src/acl/acl-control.ts @@ -393,7 +393,7 @@ export function ACLControlBox5 ( uris.map(function (u) { return handleOneDroppedURI(u) // can add to meetingDoc but must be sync }) - ).then(function (_a) { + ).then(function () { saveAndRestoreUI() }) } diff --git a/src/create.js b/src/create.js index baf068464..f16fcd6ab 100644 --- a/src/create.js +++ b/src/create.js @@ -32,14 +32,15 @@ module.exports = { ** */ function newThingUI (createContext, dataBrowserContext, thePanes) { + if (!thePanes) throw new Error('@@ newThingUI: update API') // phase out const dom = createContext.dom const div = createContext.div if (createContext.me && !createContext.me.uri) { throw new Error('newThingUI: Invalid userid: ' + createContext.me) } - var iconStyle = 'padding: 0.7em; width: 2em; height: 2em;' // was: 'padding: 1em; width: 3em; height: 3em;' - var star = div.appendChild(dom.createElement('img')) + const iconStyle = 'padding: 0.7em; width: 2em; height: 2em;' // was: 'padding: 1em; width: 3em; height: 3em;' + const star = div.appendChild(dom.createElement('img')) var visible = false // the inividual tools tools // noun_272948.svg = black star // noun_34653_green.svg = green plus @@ -53,7 +54,23 @@ function newThingUI (createContext, dataBrowserContext, thePanes) { pre.appendChild(dom.createTextNode(message)) } - var selectNewTool = function (_event) { + function styleTheIcons (style) { + for (var i = 0; i < iconArray.length; i++) { + var st = iconStyle + style + if (iconArray[i].disabled) { + // @@ unused + st += 'opacity: 0.3;' + } + iconArray[i].setAttribute('style', st) // eg 'background-color: #ccc;' + } + } + + function selectTool (icon) { + styleTheIcons('display: none;') // 'background-color: #ccc;' + icon.setAttribute('style', iconStyle + 'background-color: yellow;') + } + + function selectNewTool (_event) { visible = !visible star.setAttribute( 'style', @@ -225,21 +242,6 @@ function newThingUI (createContext, dataBrowserContext, thePanes) { }) } }) - - var styleTheIcons = function (style) { - for (var i = 0; i < iconArray.length; i++) { - var st = iconStyle + style - if (iconArray[i].disabled) { - // @@ unused - st += 'opacity: 0.3;' - } - iconArray[i].setAttribute('style', st) // eg 'background-color: #ccc;' - } - } - var selectTool = function (icon) { - styleTheIcons('display: none;') // 'background-color: #ccc;' - icon.setAttribute('style', iconStyle + 'background-color: yellow;') - } } // Form to get the name of a new thing before we create it diff --git a/src/style.js b/src/style.js index 31746e7c1..95094ce30 100644 --- a/src/style.js +++ b/src/style.js @@ -6,12 +6,26 @@ module.exports = { textInputStyle: - 'background-color: #eef; padding: 0.5em; border: .5em solid white; font-size: 100%;', + 'background-color: #eef; padding: 0.5em; border: .05em solid #88c; border-radius:0.2em; font-size: 100%; margin:0.2em; ', buttonStyle: - 'background-color: #fff; padding: 0.5em; border: .01em solid white; font-size: 100%;', // 'background-color: #eef; + 'background-color: #fff; padding: 0.7em; border: .01em solid white; border-radius:0.2em; font-size: 100%;', // 'background-color: #eef; + textButtonStyle: + 'background-color: #fff; padding: 0.7em; border: .01em solid grey; border-radius:0.2em; font-size: 100%;', // 'background-color: #eef; // The width of the text field must bot be 100% or it switches to overlapping messageBodyStyle: - 'white-space: pre-wrap; width: 99%; font-size:100%; border: 0.07em solid #eee; padding: .3em 0.5em; margin: 0.1em;', + 'white-space: pre-wrap; width: 99%; font-size:100%; border: 0.07em solid #eee; border-radius:0.2em; padding: .3em 0.5em; margin: 0.1em;', pendingeditModifier: 'color: #bbb;', - highlightColor: '#7C4DFF' // Solid lavendar https://design.inrupt.com/atomic-core/?cat=Core + highlightColor: '#7C4DFF', // Solid lavendar https://design.inrupt.com/atomic-core/?cat=Core + + // Login buttons + + signInButtonStyle: 'padding: 1em; border-radius:0.2em; margin: 2em; font-size: 100%;', // was 0.5em radius + // Forms + + formBorderColor: '#888888', // originall was brown now grey + formHeadingColor: '#888888', // originall was brown now grey + formTextInput: 'font-size: 100%; margin: 0.1em; padding: 0.1em;', // originally used this + + multilineTextInputStyle: 'font-size:100%; white-space: pre-wrap; background-color: #eef;' + + ' border: 0.07em solid gray; padding: 1em 0.5em; margin: 1em 1em;' } diff --git a/src/utils.js b/src/utils.js index b684297dc..4a2115cf5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,3 @@ -/* global tabulator */ // Solid-UI general Utilities // ========================== // @@ -32,7 +31,8 @@ module.exports = { RDFComparePredicateSubject, shortName, stackString, - syncTableToArray + syncTableToArray, + syncTableToArrayReOrdered } var UI = { @@ -121,6 +121,7 @@ function genUuid () { * @param {function({NamedNode})} createNewRow(thing) returns a TR table row for a new thing * * Tolerates out of order elements but puts new ones in order. + * Can be used for any element type; does not have to be a table and tr. */ function syncTableToArray (table, things, createNewRow) { let foundOne @@ -165,6 +166,52 @@ function syncTableToArray (table, things, createNewRow) { } } // syncTableToArray +/** Sync a DOM table with an array of things + * + * @param {DomElement} table - will have a tr for each thing + * @param {Array} things - ORDERED array of UNIQUE NamedNode objects. No duplicates + * @param {function({NamedNode})} createNewRow(thing) returns a rendering of a new thing + * + * Ensures order matches exacly. We will re-rder existing elements if necessary + * Can be used for any element type; does not have to be a table and tr. + * Any RDF node value can only appear ONCE in the array + */ +function syncTableToArrayReOrdered (table, things, createNewRow) { + const elementMap = {} + + for (let i = 0; i < table.children.length; i++) { + const row = table.children[i] + elementMap[row.subject.toNT()] = row // More sophisticaed would be to have a bag of duplicates + } + + for (let g = 0; g < things.length; g++) { + var thing = things[g] + if (g >= table.children.length) { // table needs extending + const newRow = createNewRow(thing) + newRow.subject = thing + table.appendChild(newRow) + } else { + const row = table.children[g] + if (row.subject.sameTerm(thing)) { + } else { + const existingRow = elementMap[thing.toNT()] + if (existingRow) { + table.removeChild(existingRow) + table.insertBefore(existingRow, row) // Insert existing row in place of this one + } else { + const newRow = createNewRow(thing) + row.before(newRow) // Insert existing row in place of this one + newRow.subject = thing + } + } + } + } // loop g + // Lop off any we don't need any more: + while (table.children.length > things.length) { + table.removeChild(table.children[table.children.length - 1]) + } +} // syncTableToArrayReOrdered + /* Error stack to string for better diagnotsics ** ** See http://snippets.dzone.com/posts/show/6632 @@ -200,8 +247,7 @@ function stackString (e) { function emptyNode (node) { const nodes = node.childNodes const len = nodes.length - let i - for (i = len - 1; i >= 0; i--) node.removeChild(nodes[i]) + for (let i = len - 1; i >= 0; i--) node.removeChild(nodes[i]) return node } @@ -274,14 +320,12 @@ function getTerm (target) { case 'undetermined selected': return target.nextSibling ? st.predicate - : getUndeterminedSelection(statementTr, st) + : !statementTr.AJAR_inverse + ? st.object + : st.subject } } -function getUndeterminedSelection (statementTr, st) { - return !statementTr.AJAR_inverse ? st.object : st.subject -} - function include (document, linkstr) { var lnk = document.createElement('script') lnk.setAttribute('type', 'text/javascript') @@ -485,6 +529,7 @@ function label (x, initialCap) { // The tabulator labeler is more sophisticated if it exists // Todo: move it to a solid-ui option. + /* var lab if (typeof tabulator !== 'undefined' && tabulator.lb) { lab = tabulator.lb.label(x) @@ -492,7 +537,7 @@ function label (x, initialCap) { return doCap(lab.value) } } - + */ // Hard coded known label predicates // @@ TBD: Add subproperties of rdfs:label diff --git a/src/widgets/buttons.js b/src/widgets/buttons.js index 58ca41e12..95c319192 100644 --- a/src/widgets/buttons.js +++ b/src/widgets/buttons.js @@ -404,7 +404,7 @@ buttons.button = function (dom, iconURI, text, handler) { img.setAttribute('style', 'width: 2em; height: 2em;') // trial and error. 2em disappears img.title = text if (handler) { - button.addEventListener('click', handler) + button.addEventListener('click', handler, false) } return button } @@ -723,12 +723,21 @@ buttons.allClassURIs = function () { return set } -// Figuring which propertites could by be used -// -buttons.propertyTriage = function () { +/** Figuring which properties we know about +* +* When the user inputs an RDF property, like for a form field +* or when specifying the relationship between two arbitrary things, +* then er can prompt them with properties the session knows about +* +* TODO: Look again by catching this somewhere. (On the kb?) +* TODO: move to diff module? Not really a button. +* @param {Store} kb The quadstore to be searched. +*/ + +buttons.propertyTriage = function (kb) { var possibleProperties = {} // if (possibleProperties === undefined) possibleProperties = {} - var kb = UI.store + // var kb = UI.store var dp = {} var op = {} var no = 0 @@ -748,7 +757,7 @@ buttons.propertyTriage = function () { var ps = kb.each(undefined, UI.ns.rdf('type'), UI.ns.rdf('Property')) for (var i = 0; i < ps.length; i++) { p = ps[i].toNT() - UI.log.debug('propertyTriage: unknown: ' + p) + // UI.log.debug('propertyTriage: unknown: ' + p) if (!op[p] && !dp[p]) { dp[p] = true op[p] = true diff --git a/src/widgets/forms.js b/src/widgets/forms.js index 555e7d86e..9093382a0 100644 --- a/src/widgets/forms.js +++ b/src/widgets/forms.js @@ -1,7 +1,9 @@ -/* +/* F O R M S + * + * A Vanilla Dom implementation of the form language */ -const $rdf = require('rdflib') +/* global alert */ module.exports = {} @@ -17,8 +19,10 @@ var UI = { style: require('../style'), widgets: forms } +const $rdf = require('rdflib') const error = require('./error') const buttons = require('./buttons') +const ns = require('../ns') const utils = require('../utils') const checkMarkCharacter = '\u2713' @@ -30,24 +34,36 @@ const dashCharacter = '-' /* Form Field implementations ** */ -/* Group of different fields +/** Group of different fields + ** + ** One type of form field is an ordered Group of other fields. + ** A Form is actually just the same as a group. ** + ** @param {Document} dom The HTML Document object aka Document Object Model + ** @param {Element?} container If present, the created widget will be appended to this + ** @param {Map} already A hash table of (form, subject) kept to prevent recursive forms looping + ** @param {Node} subject The thing about which the form displays/edits data + ** @param {Node} form The form or field to be rendered + ** @param {Node} store The web document in which the data is + ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed? + ** + ** @returns {Element} The HTML widget created */ -forms.field[UI.ns.ui('Form').uri] = forms.field[ - UI.ns.ui('Group').uri +forms.field[ns.ui('Form').uri] = forms.field[ + ns.ui('Group').uri ] = function (dom, container, already, subject, form, store, callbackFunction) { - var kb = UI.store + const kb = UI.store var box = dom.createElement('div') - box.setAttribute('style', 'padding-left: 2em; border: 0.05em solid brown;') // Indent a group - var ui = UI.ns.ui - container.appendChild(box) + box.setAttribute('style', `padding-left: 2em; border: 0.05em solid ${UI.style.formBorderColor};`) // Indent a group + const ui = UI.ns.ui + if (container) container.appendChild(box) // Prevent loops var key = subject.toNT() + '|' + form.toNT() if (already[key]) { // been there done that box.appendChild(dom.createTextNode('Group: see above ' + key)) - var plist = [$rdf.st(subject, UI.ns.owl('sameAs'), subject)] // @@ need prev subject + var plist = [$rdf.st(subject, ns.owl('sameAs'), subject)] // @@ need prev subject dom.outlineManager.appendPropertyTRs(box, plist) return box } @@ -72,7 +88,7 @@ forms.field[UI.ns.ui('Form').uri] = forms.field[ var original = [] for (var i = 0; i < p2.length; i++) { var field = p2[i] - var t = forms.bottomURI(field) // Field type + var t = forms.mostSpecificClassURI(field) // Field type if (t === ui('Options').uri) { var dep = kb.any(field, ui('dependingOn')) if (dep && kb.any(subject, dep)) original[i] = kb.any(subject, dep).toNT() @@ -85,7 +101,7 @@ forms.field[UI.ns.ui('Form').uri] = forms.field[ for (var j = 0; j < p2.length; j++) { // This is really messy. var field = p2[j] - var t = forms.bottomURI(field) // Field type + var t = forms.mostSpecificClassURI(field) // Field type if (t === ui('Options').uri) { var dep = kb.any(field, ui('dependingOn')) var newOne = fn( @@ -112,10 +128,20 @@ forms.field[UI.ns.ui('Form').uri] = forms.field[ return box } -/* Options: Select one or more cases +/** Options field: Select one or more cases + ** + ** @param {Document} dom The HTML Document object aka Document Object Model + ** @param {Element?} container If present, the created widget will be appended to this + ** @param {Map} already A hash table of (form, subject) kept to prevent recursive forms looping + ** @param {Node} subject The thing about which the form displays/edits data + ** @param {Node} form The form or field to be rendered + ** @param {Node} store The web document in which the data is + ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed? ** + ** @returns {Element} The HTML widget created */ -forms.field[UI.ns.ui('Options').uri] = function ( + +forms.field[ns.ui('Options').uri] = function ( dom, container, already, @@ -124,22 +150,22 @@ forms.field[UI.ns.ui('Options').uri] = function ( store, callbackFunction ) { - var kb = UI.store + const kb = UI.store var box = dom.createElement('div') // box.setAttribute('style', 'padding-left: 2em; border: 0.05em dotted purple;') // Indent Options - var ui = UI.ns.ui - container.appendChild(box) + const ui = UI.ns.ui + if (container) container.appendChild(box) var dependingOn = kb.any(form, ui('dependingOn')) if (!dependingOn) { - dependingOn = UI.ns.rdf('type') + dependingOn = ns.rdf('type') } // @@ default to type (do we want defaults?) var cases = kb.each(form, ui('case')) if (!cases) { box.appendChild(error.errorMessageBlock(dom, 'No cases to Options form. ')) } var values - if (dependingOn.sameTerm(UI.ns.rdf('type'))) { + if (dependingOn.sameTerm(ns.rdf('type'))) { values = kb.findTypeURIs(subject) } else { var value = kb.any(subject, dependingOn) @@ -188,10 +214,19 @@ forms.field[UI.ns.ui('Options').uri] = function ( return box } -/* Multiple similar fields (unordered) +/** Multiple field: zero or more similar subFields + ** + ** @param {Document} dom The HTML Document object aka Document Object Model + ** @param {Element?} container If present, the created widget will be appended to this + ** @param {Map} already A hash table of (form, subject) kept to prevent recursive forms looping + ** @param {Node} subject The thing about which the form displays/edits data + ** @param {Node} form The form or field to be rendered + ** @param {Node} store The web document in which the data is + ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed? ** + ** @returns {Element} The HTML widget created */ -forms.field[UI.ns.ui('Multiple').uri] = function ( +forms.field[ns.ui('Multiple').uri] = function ( dom, container, already, @@ -200,16 +235,193 @@ forms.field[UI.ns.ui('Multiple').uri] = function ( store, callbackFunction ) { - // var plusIcon = UI.icons.originalIconBase + 'tango/22-list-add.png' // blue plus + /** Diagnostic function + */ + function debugString (values) { + return values.map(x => x.toString().slice(-7)).join(', ') + } + + /** Add an item to the local quadstore not the UI or the web + * + * @param {Node} object The RDF object to be represented by this item. + */ + async function addItem (object) { + if (!object) object = forms.newThing(store) // by default just add new nodes + if (ordered) { + createListIfNecessary() // Sets list and unsavedList + list.elements.push(object) + await saveListThenRefresh() + } else { + const toBeInserted = [$rdf.st(subject, property, object, store)] + try { + await kb.updater.update([], toBeInserted) + } catch (err) { + const msg = 'Error adding to unordered multiple: ' + err + box.appendChild(error.errorMessageBlock(dom, msg)) + console.error(msg) + } + refresh() // 20191213 + } + } + + /** Make a dom representation for an item + * @param {Event} anyEvent if used as an event handler + * @param {Node} object The RDF object to be represented by this item. + */ + function renderItem (object) { + async function deleteThisItem () { + if (ordered) { + console.log('pre delete: ' + debugString(list.elements)) + for (let i = 0; i < list.elements.length; i++) { + if (list.elements[i].sameTerm(object)) { + list.elements.splice(i, 1) + await saveListThenRefresh() + return + } + } + } else { + // unordered + if (kb.holds(subject, property, object)) { + var del = [$rdf.st(subject, property, object, store)] + kb.updater.update(del, [], function (uri, ok, message) { + if (ok) { + body.removeChild(subField) + } else { + body.appendChild( + error.errorMessageBlock( + dom, + 'Multiple: delete failed: ' + message + ) + ) + } + }) + } + } + } + + /** Move the object up or down in the ordered list + * @param {Event} anyEvent if used as an event handler + * @param {Boolean} upwards Move this up (true) or down (false). + */ + async function moveThisItem (event, upwards) { + // @@ possibly, allow shift+click to do move to top or bottom? + console.log('pre move: ' + debugString(list.elements)) + for (var i = 0; i < list.elements.length; i++) { + // Find object in array + if (list.elements[i].sameTerm(object)) { + break + } + } + if (i === list.elements.length) { + alert('list move: not found element for ' + object) + } + if (upwards) { + if (i === 0) { + alert('@@ boop - already at top -temp message') // @@ make boop sound + return + } + list.elements.splice(i - 1, 2, list.elements[i], list.elements[i - 1]) + } else { + // downwards + if (i === list.elements.length - 1) { + alert('@@ boop - already at bottom -temp message') // @@ make boop sound + return + } + list.elements.splice(i, 2, list.elements[i + 1], list.elements[i]) + } + await saveListThenRefresh() + } + /* A subField has been filled in + * + * One possibility is to not actually make the link to the thing until + * this callback happens to avoid widow links + */ + function itemDone (uri, ok, message) { + console.log(`Item ${uri} done callback for item ${object.uri.slice(-7)}`) + if (!ok) { // when does this happen? errors typically deal with upstream + console.error(' Item done callback: Error: ' + message) + } else { + linkDone(uri, ok, message) + } + /* Put this as a function and call it from only one place + var ins, del + // alert('Multiple: item calklback.' + uri) + if (ok) { + // @@@ Check IT hasnt alreday been written in + if (ordered) { + list = kb.any(subject, property, null, store) + if (!list) { + list = new $rdf.Collection([object]) + ins = [$rdf.st(subject, property, list)] // Will this work? + } else { + const oldList = new $rdf.Collection(list.elments) + list.append(object) + del = [$rdf.st(subject, property, oldList)] // If this doesn't work, kb.saveBack(store) + ins = [$rdf.st(subject, property, list)] + } + } else { + if (!kb.holds(subject, property, object, store)) { + ins = [$rdf.st(subject, property, object, store)] + } + kb.updater.update(del, ins, linkDone) + } + } else { + box.appendChild( + error.errorMessageBlock(dom, 'Multiple: item failed: ' + body) + ) + callbackFunction(ok, message) + } + */ + } + var linkDone = function (uri, ok, message) { + return callbackFunction(ok, message) + } + + // if (!object) object = forms.newThing(store) + UI.log.debug('Multiple: render object: ' + object) + // var tr = box.insertBefore(dom.createElement('tr'), tail) + // var ins = [] + // var del = [] + + var fn = forms.fieldFunction(dom, element) + var subField = fn(dom, null, already, object, element, store, itemDone) // p2 was: body. moving to not passing that + subField.subject = object // Keep a back pointer between the DOM array and the RDF objects + + // delete button and move buttons + if (kb.updater.editable(store.uri)) { + buttons.deleteButtonWithCheck(dom, subField, utils.label(property), + deleteThisItem) + if (ordered) { + subField.appendChild( + buttons.button( + dom, UI.icons.iconBase + 'noun_1369237.svg', 'Move Up', + async event => moveThisItem(event, true)) + ) + subField.appendChild( + buttons.button( + dom, UI.icons.iconBase + 'noun_1369241.svg', 'Move Down', + async event => moveThisItem(event, false)) + ) + } + } + return subField // unused + } // renderItem + + /// ///////// Body of form field implementation + var plusIconURI = UI.icons.iconBase + 'noun_19460_green.svg' // white plus in green circle - var kb = UI.store + const kb = UI.store kb.updater = kb.updater || new $rdf.UpdateManager(kb) var box = dom.createElement('table') - // We don't indent multiple as it is a sort of a prefix o fthe next field and has contents of one. + // We don't indent multiple as it is a sort of a prefix of the next field and has contents of one. // box.setAttribute('style', 'padding-left: 2em; border: 0.05em solid green;') // Indent a multiple - var ui = UI.ns.ui - container.appendChild(box) + const ui = UI.ns.ui + if (container) container.appendChild(box) + + const orderedNode = kb.any(form, ui('ordered')) + const ordered = orderedNode ? $rdf.Node.toJS(orderedNode) : false + var property = kb.any(form, ui('property')) if (!property) { box.appendChild( @@ -218,7 +430,7 @@ forms.field[UI.ns.ui('Multiple').uri] = function ( return box } var min = kb.any(form, ui('min')) // This is the minimum number -- default 0 - min = min ? min.value : 0 + min = min ? 0 + min.value : 0 // var max = kb.any(form, ui('max')) // This is the minimum number // max = max ? max.value : 99999999 @@ -230,87 +442,96 @@ forms.field[UI.ns.ui('Multiple').uri] = function ( return box } - // box.appendChild(dom.createElement('h3')).textContent = "Fields:". - var body = box.appendChild(dom.createElement('tr')) - var tail = box.appendChild(dom.createElement('tr')) + var body = box.appendChild(dom.createElement('tr')) // 20191207 + var list // The RDF collection which keeps the ordered version + var values // Initial values - an array. Even when no list yet. - var addItem = function (e, object) { - UI.log.debug('Multiple add: ' + object) - // ++count - if (!object) object = forms.newThing(store) - var tr = box.insertBefore(dom.createElement('tr'), tail) - var itemDone = function (uri, ok, message) { - if (ok) { - // @@@ Check IT hasnt alreday been written in - if (!kb.holds(subject, property, object, store)) { - var ins = [$rdf.st(subject, property, object, store)] - kb.updater.update([], ins, linkDone) - } - } else { - tr.appendChild( - error.errorMessageBlock(dom, 'Multiple: item failed: ' + body) - ) - callbackFunction(ok, message) - } - } - var linkDone = function (uri, ok, message) { - return callbackFunction(ok, message) - } - - var fn = forms.fieldFunction(dom, element) - var subField = fn(dom, body, already, object, element, store, itemDone) - - // delete button - var deleteItem = function () { - if (kb.holds(subject, property, object)) { - var del = [$rdf.st(subject, property, object, store)] - kb.updater.update(del, [], function (uri, ok, message) { - if (ok) { - body.removeChild(subField) - } else { - body.appendChild( - error.errorMessageBlock( - dom, - 'Multiple: delete failed: ' + message - ) - ) - } - }) - } - } - if (kb.updater.editable(store.uri)) { - buttons.deleteButtonWithCheck( - dom, - subField, - utils.label(property), - deleteItem - ) + // var unsavedList = false // Flag that + if (ordered) { + list = kb.any(subject, property) + if (list) { + values = list.elements + } else { + // unsavedList = true + values = [] } + } else { + values = kb.each(subject, property) + list = null } - - var values = kb.each(subject, property) + // Add control on the bottom for adding more items if (kb.updater.editable(store.uri)) { + var tail = box.appendChild(dom.createElement('tr')) + tail.style.padding = '0.5em' var img = tail.appendChild(dom.createElement('img')) img.setAttribute('src', plusIconURI) // plus sign - img.setAttribute('style', 'margin: 0.2em; width: 1em; height:1em') + img.setAttribute('style', 'margin: 0.2em; width: 1.5em; height:1.5em') img.title = 'Click to add one or more ' + utils.label(property) var prompt = tail.appendChild(dom.createElement('span')) prompt.textContent = (values.length === 0 ? 'Add one or more ' : 'Add more ') + utils.label(property) - tail.addEventListener('click', addItem, true) // img.addEventListener('click', addItem, true) + tail.addEventListener('click', async _eventNotUsed => { + await addItem() + }, true) } - values.map(function (obj) { - addItem(null, obj) - }) - var extra = min - values.length - for (var j = 0; j < extra; j++) { - console.log('Adding extra: min ' + min) - addItem() // Add blanks if less than minimum + function createListIfNecessary () { + if (!list) { + list = new $rdf.Collection() + kb.add(subject, property, list, store) + } + } + + async function saveListThenRefresh () { + console.log('save list: ' + debugString(list.elements)) // 20191214 + + createListIfNecessary() + try { + await kb.fetcher.putBack(store) + } catch (err) { + box.appendChild( + error.errorMessageBlock(dom, 'Error trying to put back a list: ' + err) + ) + return + } + refresh() + } + + function refresh () { + let vals + if (ordered) { + const li = kb.the(subject, property) + vals = li ? li.elements : [] + } else { + vals = kb.each(subject, property) + vals.sort() // achieve consistency on each refresh + } + utils.syncTableToArrayReOrdered(body, vals, renderItem) } + body.refresh = refresh // Allow live update + refresh() + + async function asyncStuff () { + var extra = min - values.length + if (extra > 0) { + for (var j = 0; j < extra; j++) { + console.log('Adding extra: min ' + min) + await addItem() // Add blanks if less than minimum + } + await saveListThenRefresh() + } + // if (unsavedList) { + // await saveListThenRefresh() // async + // } + } + asyncStuff().then( + () => { console.log(' Multiple render: async stuff ok') }, + (err) => { console.error(' Multiple render: async stuff fails. #### ', err) } + ) // async + return box -} +} // Multiple /* Text field ** @@ -321,93 +542,96 @@ forms.field[UI.ns.ui('Multiple').uri] = function ( forms.fieldParams = {} -forms.fieldParams[UI.ns.ui('ColorField').uri] = { +forms.fieldParams[ns.ui('ColorField').uri] = { size: 9, type: 'color', dt: 'color' } // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color forms.fieldParams[ - UI.ns.ui('ColorField').uri + ns.ui('ColorField').uri ].pattern = /^\s*#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]([0-9a-f][0-9a-f])?\s*$/ -forms.fieldParams[UI.ns.ui('DateField').uri] = { +forms.fieldParams[ns.ui('DateField').uri] = { size: 20, type: 'date', dt: 'date' } forms.fieldParams[ - UI.ns.ui('DateField').uri + ns.ui('DateField').uri ].pattern = /^\s*[0-9][0-9][0-9][0-9](-[0-1]?[0-9]-[0-3]?[0-9])?Z?\s*$/ -forms.fieldParams[UI.ns.ui('DateTimeField').uri] = { +forms.fieldParams[ns.ui('DateTimeField').uri] = { size: 20, type: 'date', dt: 'dateTime' } forms.fieldParams[ - UI.ns.ui('DateTimeField').uri + ns.ui('DateTimeField').uri ].pattern = /^\s*[0-9][0-9][0-9][0-9](-[0-1]?[0-9]-[0-3]?[0-9])?(T[0-2][0-9]:[0-5][0-9](:[0-5][0-9])?)?Z?\s*$/ -forms.fieldParams[UI.ns.ui('TimeField').uri] = { +forms.fieldParams[ns.ui('TimeField').uri] = { size: 10, type: 'time', dt: 'time' } forms.fieldParams[ - UI.ns.ui('TimeField').uri + ns.ui('TimeField').uri ].pattern = /^\s*([0-2]?[0-9]:[0-5][0-9](:[0-5][0-9])?)\s*$/ -forms.fieldParams[UI.ns.ui('IntegerField').uri] = { +forms.fieldParams[ns.ui('IntegerField').uri] = { size: 12, style: 'text-align: right', dt: 'integer' } -forms.fieldParams[UI.ns.ui('IntegerField').uri].pattern = /^\s*-?[0-9]+\s*$/ +forms.fieldParams[ns.ui('IntegerField').uri].pattern = /^\s*-?[0-9]+\s*$/ -forms.fieldParams[UI.ns.ui('DecimalField').uri] = { +forms.fieldParams[ns.ui('DecimalField').uri] = { size: 12, style: 'text-align: right', dt: 'decimal' } forms.fieldParams[ - UI.ns.ui('DecimalField').uri + ns.ui('DecimalField').uri ].pattern = /^\s*-?[0-9]*(\.[0-9]*)?\s*$/ -forms.fieldParams[UI.ns.ui('FloatField').uri] = { +forms.fieldParams[ns.ui('FloatField').uri] = { size: 12, style: 'text-align: right', dt: 'float' } forms.fieldParams[ - UI.ns.ui('FloatField').uri + ns.ui('FloatField').uri ].pattern = /^\s*-?[0-9]*(\.[0-9]*)?((e|E)-?[0-9]*)?\s*$/ -forms.fieldParams[UI.ns.ui('SingleLineTextField').uri] = {} -forms.fieldParams[UI.ns.ui('NamedNodeURIField').uri] = { namedNode: true } -forms.fieldParams[UI.ns.ui('TextField').uri] = {} +forms.fieldParams[ns.ui('SingleLineTextField').uri] = {} +forms.fieldParams[ns.ui('NamedNodeURIField').uri] = { namedNode: true } +forms.fieldParams[ns.ui('TextField').uri] = {} -forms.fieldParams[UI.ns.ui('PhoneField').uri] = { size: 20, uriPrefix: 'tel:' } -forms.fieldParams[UI.ns.ui('PhoneField').uri].pattern = /^\+?[\d-]+[\d]*$/ +forms.fieldParams[ns.ui('PhoneField').uri] = { size: 20, uriPrefix: 'tel:' } +forms.fieldParams[ns.ui('PhoneField').uri].pattern = /^\+?[\d-]+[\d]*$/ -forms.fieldParams[UI.ns.ui('EmailField').uri] = { +forms.fieldParams[ns.ui('EmailField').uri] = { size: 30, uriPrefix: 'mailto:' } -forms.fieldParams[UI.ns.ui('EmailField').uri].pattern = /^\s*.*@.*\..*\s*$/ // @@ Get the right regexp here - -forms.field[UI.ns.ui('PhoneField').uri] = forms.field[ - UI.ns.ui('EmailField').uri -] = forms.field[UI.ns.ui('ColorField').uri] = forms.field[ - UI.ns.ui('DateField').uri -] = forms.field[UI.ns.ui('DateTimeField').uri] = forms.field[ - UI.ns.ui('TimeField').uri -] = forms.field[UI.ns.ui('NumericField').uri] = forms.field[ - UI.ns.ui('IntegerField').uri -] = forms.field[UI.ns.ui('DecimalField').uri] = forms.field[ - UI.ns.ui('FloatField').uri -] = forms.field[UI.ns.ui('TextField').uri] = forms.field[ - UI.ns.ui('SingleLineTextField').uri -] = forms.field[UI.ns.ui('NamedNodeURIField').uri] = function ( +forms.fieldParams[ns.ui('EmailField').uri].pattern = /^\s*.*@.*\..*\s*$/ // @@ Get the right regexp here + +/** Render a basic form field + * + ** @param {Document} dom The HTML Document object aka Document Object Model + ** @param {Element?} container If present, the created widget will be appended to this + ** @param {Map} already A hash table of (form, subject) kept to prevent recursive forms looping + ** @param {Node} subject The thing about which the form displays/edits data + ** @param {Node} form The form or field to be rendered + ** @param {Node} store The web document in which the data is + ** @param {function(ok, errorMessage)} callbackFunction Called when data is changed? + ** + ** @returns {Element} The HTML widget created + ** + ** The same function is used for many similar one-value fields, with different + ** regexps used to validate. + */ +function basicField ( dom, container, already, @@ -416,11 +640,11 @@ forms.field[UI.ns.ui('PhoneField').uri] = forms.field[ store, callbackFunction ) { - var ui = UI.ns.ui - var kb = UI.store + const ui = UI.ns.ui + const kb = UI.store var box = dom.createElement('tr') - container.appendChild(box) + if (container) container.appendChild(box) var lhs = dom.createElement('td') lhs.setAttribute('class', 'formFieldName') lhs.setAttribute('style', ' vertical-align: middle;') @@ -437,12 +661,13 @@ forms.field[UI.ns.ui('PhoneField').uri] = forms.field[ return box } lhs.appendChild(forms.fieldLabel(dom, property, form)) - var uri = forms.bottomURI(form) + var uri = forms.mostSpecificClassURI(form) var params = forms.fieldParams[uri] if (params === undefined) params = {} // non-bottom field types can do this - var style = params.style || 'font-size: 100%; margin: 0.1em; padding: 0.1em;' + var style = params.style || UI.style.textInputStyle || 'font-size: 100%; margin: 0.1em; padding: 0.1em;' // box.appendChild(dom.createTextNode(' uri='+uri+', pattern='+ params.pattern)) var field = dom.createElement('input') + field.style = UI.style.textInputStyle // Do we have to override length etc? rhs.appendChild(field) field.setAttribute('type', params.type ? params.type : 'text') @@ -470,14 +695,14 @@ forms.field[UI.ns.ui('PhoneField').uri] = forms.field[ field.setAttribute('style', style) if (!kb.updater.editable(store.uri)) { - field.disabled = true + field.readonly = true // was: disabled. readonly is better return box } - // read-write: + // read-write: field.addEventListener( 'keyup', - function (_event) { + function (_e) { if (params.pattern) { field.setAttribute( 'style', @@ -492,7 +717,7 @@ forms.field[UI.ns.ui('PhoneField').uri] = forms.field[ ) field.addEventListener( 'change', - function (_event) { + function (_e) { // i.e. lose focus with changed data if (params.pattern && !field.value.match(params.pattern)) return field.disabled = true // See if this stops getting two dates from fumbling e.g the chrome datepicker. @@ -509,7 +734,7 @@ forms.field[UI.ns.ui('PhoneField').uri] = forms.field[ result = new $rdf.Literal( field.value.trim(), undefined, - UI.ns.xsd(params.dt) + ns.xsd(params.dt) ) } else { result = new $rdf.Literal(field.value) @@ -524,14 +749,10 @@ forms.field[UI.ns.ui('PhoneField').uri] = forms.field[ function updateMany (ds, is, callback) { var docs = [] is.forEach(st => { - if (!docs.includes(st.why.uri)) { - docs.push(st.why.uri) - } + if (!docs.includes(st.why.uri)) docs.push(st.why.uri) }) ds.forEach(st => { - if (!docs.includes(st.why.uri)) { - docs.push(st.why.uri) - } + if (!docs.includes(st.why.uri)) docs.push(st.why.uri) }) if (docs.length === 0) { throw new Error('updateMany has no docs to patch') @@ -539,7 +760,8 @@ forms.field[UI.ns.ui('PhoneField').uri] = forms.field[ if (docs.length === 1) { return kb.updater.update(ds, is, callback) } - console.log('Update many: ' + docs) + // return kb.updater.update(ds, is, callback) + const doc = docs.pop() const is1 = is.filter(st => st.why.uri === doc) const is2 = is.filter(st => st.why.uri !== doc) @@ -571,11 +793,25 @@ forms.field[UI.ns.ui('PhoneField').uri] = forms.field[ return box } +forms.field[ns.ui('PhoneField').uri] = basicField +forms.field[ns.ui('EmailField').uri] = basicField +forms.field[ns.ui('ColorField').uri] = basicField +forms.field[ns.ui('DateField').uri] = basicField +forms.field[ns.ui('DateTimeField').uri] = basicField +forms.field[ns.ui('TimeField').uri] = basicField +forms.field[ns.ui('NumericField').uri] = basicField +forms.field[ns.ui('IntegerField').uri] = basicField +forms.field[ns.ui('DecimalField').uri] = basicField +forms.field[ns.ui('FloatField').uri] = basicField +forms.field[ns.ui('TextField').uri] = basicField +forms.field[ns.ui('SingleLineTextField').uri] = basicField +forms.field[ns.ui('NamedNodeURIField').uri] = basicField + /* Multiline Text field ** */ -forms.field[UI.ns.ui('MultiLineTextField').uri] = function ( +forms.field[ns.ui('MultiLineTextField').uri] = function ( dom, container, already, @@ -584,15 +820,16 @@ forms.field[UI.ns.ui('MultiLineTextField').uri] = function ( store, callbackFunction ) { - var ui = UI.ns.ui - var kb = UI.store + const ui = UI.ns.ui + const kb = UI.store var property = kb.any(form, ui('property')) if (!property) { return error.errorMessageBlock(dom, 'No property to text field: ' + form) } - container.appendChild(forms.fieldLabel(dom, property, form)) + const box = dom.createElement('div') + box.appendChild(forms.fieldLabel(dom, property, form)) store = forms.fieldStore(subject, property, store) - var box = forms.makeDescription( + var field = forms.makeDescription( dom, kb, subject, @@ -601,12 +838,14 @@ forms.field[UI.ns.ui('MultiLineTextField').uri] = function ( callbackFunction ) // box.appendChild(dom.createTextNode('<-@@ subj:'+subject+', prop:'+property)) - container.appendChild(box) + box.appendChild(field) + if (container) container.appendChild(box) return box } /* Boolean field and Tri-state version (true/false/null) ** + ** @@ todo: remove tristate param */ function booleanField ( dom, @@ -618,13 +857,16 @@ function booleanField ( callbackFunction, tristate ) { - var ui = UI.ns.ui - var kb = UI.store + const ui = UI.ns.ui + const kb = UI.store var property = kb.any(form, ui('property')) if (!property) { - return container.appendChild( - error.errorMessageBlock(dom, 'No property to boolean field: ' + form) + const errorBlock = error.errorMessageBlock( + dom, + 'No property to boolean field: ' + form ) + if (container) container.appendChild(errorBlock) + return errorBlock } var lab = kb.any(form, ui('label')) if (!lab) lab = utils.label(property, true) // Init capital @@ -637,18 +879,17 @@ function booleanField ( var ins = $rdf.st(subject, property, true, store) var del = $rdf.st(subject, property, false, store) var box = buildCheckboxForm(dom, kb, lab, del, ins, form, store, tristate) - container.appendChild(box) + if (container) container.appendChild(box) return box } -forms.field[UI.ns.ui('BooleanField').uri] = function ( +forms.field[ns.ui('BooleanField').uri] = function ( dom, container, already, subject, form, store, - callbackFunction, - _tristate + callbackFunction ) { return booleanField( dom, @@ -662,7 +903,7 @@ forms.field[UI.ns.ui('BooleanField').uri] = function ( ) } -forms.field[UI.ns.ui('TristateField').uri] = function ( +forms.field[ns.ui('TristateField').uri] = function ( dom, container, already, @@ -690,7 +931,7 @@ forms.field[UI.ns.ui('TristateField').uri] = function ( ** @@ To do: If a classification changes, then change any dependent Options fields. */ -forms.field[UI.ns.ui('Classifier').uri] = function ( +forms.field[ns.ui('Classifier').uri] = function ( dom, container, already, @@ -699,8 +940,8 @@ forms.field[UI.ns.ui('Classifier').uri] = function ( store, callbackFunction ) { - var kb = UI.store - var ui = UI.ns.ui + const kb = UI.store + const ui = UI.ns.ui var category = kb.any(form, ui('category')) if (!category) { return error.errorMessageBlock(dom, 'No category for classifier: ' + form) @@ -726,11 +967,11 @@ forms.field[UI.ns.ui('Classifier').uri] = function ( store, checkOptions ) - container.appendChild(box) + if (container) container.appendChild(box) return box } -/* Choice field +/** Choice field ** ** Not nested. Generates a link to something from a given class. ** Optional subform for the thing selected. @@ -742,7 +983,7 @@ forms.field[UI.ns.ui('Classifier').uri] = function ( ** Todo: Deal with multiple. Maybe merge with multiple code. */ -forms.field[UI.ns.ui('Choice').uri] = function ( +forms.field[ns.ui('Choice').uri] = function ( dom, container, already, @@ -751,13 +992,13 @@ forms.field[UI.ns.ui('Choice').uri] = function ( store, callbackFunction ) { - var ui = UI.ns.ui var ns = UI.ns - var kb = UI.store + const ui = UI.ns.ui + const kb = UI.store var multiple = false var p var box = dom.createElement('tr') - container.appendChild(box) + if (container) container.appendChild(box) var lhs = dom.createElement('td') box.appendChild(lhs) var rhs = dom.createElement('td') @@ -783,24 +1024,24 @@ forms.field[UI.ns.ui('Choice').uri] = function ( } // Use rdfs // UI.log.debug("%%% Choice field: possible.length 1 = "+possible.length) if (from.sameTerm(ns.rdfs('Class'))) { - for (p in forms.allClassURIs()) possible.push(kb.sym(p)) + for (p in buttons.allClassURIs()) possible.push(kb.sym(p)) // UI.log.debug("%%% Choice field: possible.length 2 = "+possible.length) } else if (from.sameTerm(ns.rdf('Property'))) { - possibleProperties = forms.propertyTriage() + possibleProperties = buttons.propertyTriage(kb) for (p in possibleProperties.op) possible.push(kb.fromNT(p)) for (p in possibleProperties.dp) possible.push(kb.fromNT(p)) opts.disambiguate = true // This is a big class, and the labels won't be enough. } else if (from.sameTerm(ns.owl('ObjectProperty'))) { - possibleProperties = forms.propertyTriage() + possibleProperties = buttons.propertyTriage(kb) for (p in possibleProperties.op) possible.push(kb.fromNT(p)) opts.disambiguate = true } else if (from.sameTerm(ns.owl('DatatypeProperty'))) { - possibleProperties = forms.propertyTriage() + possibleProperties = buttons.propertyTriage(kb) for (p in possibleProperties.dp) possible.push(kb.fromNT(p)) opts.disambiguate = true } var object = kb.any(subject, property) - function addSubForm (_ok, _body) { + function addSubForm () { object = kb.any(subject, property) forms.fieldFunction(dom, subForm)( dom, @@ -829,24 +1070,24 @@ forms.field[UI.ns.ui('Choice').uri] = function ( callbackFunction ) rhs.appendChild(selector) - if (object && subForm) addSubForm(true, '') + if (object && subForm) addSubForm() return box } // Documentation - non-interactive fields // -forms.fieldParams[UI.ns.ui('Comment').uri] = { +forms.fieldParams[ns.ui('Comment').uri] = { element: 'p', - style: 'padding: 0.1em 1.5em; color: brown; white-space: pre-wrap;' + style: `padding: 0.1em 1.5em; color: ${UI.style.formHeadingColor}; white-space: pre-wrap;` } -forms.fieldParams[UI.ns.ui('Heading').uri] = { +forms.fieldParams[ns.ui('Heading').uri] = { element: 'h3', - style: 'font-size: 110%; color: brown;' + style: `font-size: 110%; color: ${UI.style.formHeadingColor};` } -forms.field[UI.ns.ui('Comment').uri] = forms.field[ - UI.ns.ui('Heading').uri +forms.field[ns.ui('Comment').uri] = forms.field[ + ns.ui('Heading').uri ] = function ( dom, container, @@ -856,19 +1097,19 @@ forms.field[UI.ns.ui('Comment').uri] = forms.field[ _store, _callbackFunction ) { - var ui = UI.ns.ui - var kb = UI.store + const ui = UI.ns.ui + const kb = UI.store var contents = kb.any(form, ui('contents')) if (!contents) contents = 'Error: No contents in comment field.' - var uri = forms.bottomURI(form) + var uri = forms.mostSpecificClassURI(form) var params = forms.fieldParams[uri] if (params === undefined) { params = {} } // non-bottom field types can do this var box = dom.createElement('div') - container.appendChild(box) + if (container) container.appendChild(box) var p = box.appendChild(dom.createElement(params.element)) p.textContent = contents @@ -883,8 +1124,13 @@ forms.field[UI.ns.ui('Comment').uri] = forms.field[ /// ////////////// Form-related functions -forms.bottomURI = function (x) { - var kb = UI.store +/** Which class of field is this? + * @param x a field + * @returns the URI of the most specific class + */ + +forms.mostSpecificClassURI = function (x) { + const kb = UI.store var ft = kb.findTypeURIs(x) var bot = kb.bottomTypeURIs(ft) // most specific var bots = [] @@ -894,7 +1140,8 @@ forms.bottomURI = function (x) { } forms.fieldFunction = function (dom, field) { - var uri = forms.bottomURI(field) + const uri = forms.mostSpecificClassURI(field) // What type + // const uri = field.uri var fun = forms.field[uri] UI.log.debug( 'paneUtils: Going to implement field ' + field + ' of type ' + uri @@ -924,26 +1171,26 @@ forms.editFormButton = function ( ) { var b = dom.createElement('button') b.setAttribute('type', 'button') - b.innerHTML = 'Edit ' + utils.label(UI.ns.ui('Form')) + b.innerHTML = 'Edit ' + utils.label(ns.ui('Form')) b.addEventListener( 'click', - function (_event) { + function (_e) { var ff = forms.appendForm( dom, container, {}, form, - UI.ns.ui('FormForm'), + ns.ui('FormForm'), store, callbackFunction ) ff.setAttribute( 'style', - UI.ns.ui('FormForm').sameTerm(form) + ns.ui('FormForm').sameTerm(form) ? 'background-color: #fee;' : 'background-color: #ffffe7;' ) - container.removeChild(b) + b.parentNode.removeChild(b) }, true ) @@ -970,13 +1217,13 @@ forms.appendForm = function ( ) } -// Find list of properties for class +/** Find list of properties for class // // Three possible sources: Those mentioned in schemas, which exludes many // those which occur in the data we already have, and those predicates we -// have come across anywahere and which are not explicitly excluded from +// have come across anywhere and which are not explicitly excluded from // being used with this class. -// +*/ forms.propertiesForClass = function (kb, c) { var ns = UI.ns @@ -1007,8 +1254,11 @@ forms.propertiesForClass = function (kb, c) { return result } -// @param cla - the URI of the class -// @proap +/** Find the closest class +* @param kb The store +* @param cla - the URI of the class +* @param prop +*/ forms.findClosest = function findClosest (kb, cla, prop) { var agenda = [kb.sym(cla)] // ordered - this is breadth first search while (agenda.length > 0) { @@ -1017,7 +1267,7 @@ forms.findClosest = function findClosest (kb, cla, prop) { var lists = kb.each(c, prop) UI.log.debug('Lists for ' + c + ', ' + prop + ': ' + lists.length) if (lists.length !== 0) return lists - var supers = kb.each(c, UI.ns.rdfs('subClassOf')) + var supers = kb.each(c, ns.rdfs('subClassOf')) for (var i = 0; i < supers.length; i++) { agenda.push(supers[i]) UI.log.debug('findClosest: add super: ' + supers[i]) @@ -1030,7 +1280,7 @@ forms.findClosest = function findClosest (kb, cla, prop) { forms.formsFor = function (subject) { var ns = UI.ns - var kb = UI.store + const kb = UI.store UI.log.debug('formsFor: subject=' + subject) var t = kb.findTypeURIs(subject) @@ -1055,7 +1305,7 @@ forms.formsFor = function (subject) { forms.sortBySequence = function (list) { var p2 = list.map(function (p) { - var k = UI.store.any(p, UI.ns.ui('sequence')) + var k = UI.store.any(p, ns.ui('sequence')) return [k || 9999, p] }) p2.sort(function (a, b) { @@ -1076,11 +1326,11 @@ forms.sortByLabel = function (list) { }) } -// Button to add a new whatever using a form +/** Button to add a new whatever using a form // // @param form - optional form , else will look for one // @param store - optional store else will prompt for one (unimplemented) - +*/ forms.newButton = function ( dom, kb, @@ -1096,7 +1346,7 @@ forms.newButton = function ( b.innerHTML = 'New ' + utils.label(theClass) b.addEventListener( 'click', - function (_event) { + function (_e) { b.parentNode.appendChild( forms.promptForNew( dom, @@ -1115,8 +1365,7 @@ forms.newButton = function ( return b } -// Prompt for new object of a given class -// +/** Prompt for new object of a given class // // @param dom - the document DOM for the user interface // @param kb - the graph which is the knowledge base we are working with @@ -1127,7 +1376,7 @@ forms.newButton = function ( // @param store - The web document being edited // @param callbackFunction - takes (boolean ok, string errorBody) // @returns a dom object with the form DOM - +*/ forms.promptForNew = function ( dom, kb, @@ -1142,7 +1391,7 @@ forms.promptForNew = function ( var box = dom.createElement('form') if (!form) { - var lists = forms.findClosest(kb, theClass.uri, ns.ui('creationForm')) + var lists = forms.findClosest(kb, theClass.uri, UI.ns.ui('creationForm')) if (lists.length === 0) { var p = box.appendChild(dom.createElement('p')) p.textContent = @@ -1155,7 +1404,7 @@ forms.promptForNew = function ( b.innerHTML = 'Goto ' + utils.label(theClass) b.addEventListener( 'click', - function (_event) { + function (_e) { dom.outlineManager.GotoSubject( theClass, true, @@ -1172,7 +1421,7 @@ forms.promptForNew = function ( form = lists[0] // Pick any one } UI.log.debug('form is ' + form) - box.setAttribute('style', 'border: 0.05em solid brown; color: brown') + box.setAttribute('style', `border: 0.05em solid ${UI.style.formBorderColor}; color: ${UI.style.formBorderColor}`) // @@color? box.innerHTML = '

New ' + utils.label(theClass) + '

' var formFunction = forms.fieldFunction(dom, form) @@ -1197,7 +1446,7 @@ forms.promptForNew = function ( } // tabulator.outline.GotoSubject(object, true, undefined, true, undefined) } - var linkDone = function (uri, ok, body) { + function linkDone (uri, ok, body) { return callbackFunction(ok, body) } UI.log.info('paneUtils Object is ' + object) @@ -1218,7 +1467,7 @@ forms.makeDescription = function ( ) { var group = dom.createElement('div') - var sts = kb.statementsMatching(subject, predicate, undefined) // Only one please + var sts = kb.statementsMatching(subject, predicate, null, store) // Only one please if (sts.length > 1) { return error.errorMessageBlock( dom, @@ -1231,7 +1480,7 @@ forms.makeDescription = function ( group.appendChild(field) field.rows = desc ? desc.split('\n').length + 2 : 2 field.cols = 80 - var style = + var style = UI.style.multilineTextInputStyle || 'font-size:100%; white-space: pre-wrap; background-color: white;' + ' border: 0.07em solid gray; padding: 1em 0.5em; margin: 1em 1em;' field.setAttribute('style', style) @@ -1244,18 +1493,18 @@ forms.makeDescription = function ( } group.refresh = function () { - var v = kb.any(subject, predicate) + var v = kb.any(subject, predicate, null, store) if (v && v.value !== field.value) { field.value = v.value // don't touch widget if no change // @@ this is the place to color the field from the user who chanaged it } } - function saveChange (_event) { + function saveChange (_e) { submit.disabled = true submit.setAttribute('style', 'visibility: hidden; float: right;') // Keep UI clean field.disabled = true field.setAttribute('style', style + 'color: gray;') // pending - var ds = kb.statementsMatching(subject, predicate) + var ds = kb.statementsMatching(subject, predicate, null, store) var is = $rdf.st(subject, predicate, field.value, store) UI.store.updater.update(ds, is, function (uri, ok, body) { if (ok) { @@ -1289,7 +1538,7 @@ forms.makeDescription = function ( field.addEventListener( 'keyup', - function (_event) { + function (_e) { // Green means has been changed, not saved yet field.setAttribute('style', style + 'color: green;') if (submit) { @@ -1307,7 +1556,7 @@ forms.makeDescription = function ( return group } -// Make SELECT element to select options +/** Make SELECT element to select options // // @param subject - a term, the subject of the statement(s) being edited. // @param predicate - a term, the predicate of the statement(s) being edited @@ -1319,7 +1568,7 @@ forms.makeDescription = function ( // @param options.subForm - If mint, then the form to be used for minting the new thing // @param store - The web document being edited // @param callbackFunction - takes (boolean ok, string errorBody) - +*/ forms.makeSelectForOptions = function ( dom, kb, @@ -1336,9 +1585,9 @@ forms.makeSelectForOptions = function ( var editable = UI.store.updater.editable(store.uri) for (var i = 0; i < possible.length; i++) { - var sub = possible[i] - // UI.log.debug('Select element: '+ sub) - if (sub.uri in uris) continue + var sub = possible[i] // @@ Maybe; make this so it works with blank nodes too + if (!sub.uri) console.warn(`makeSelectForOptions: option does not have an uri: ${sub}, with predicate: ${predicate}`) + if (!sub.uri || sub.uri in uris) continue uris[sub.uri] = true n++ } // uris is now the set of possible options @@ -1356,10 +1605,10 @@ forms.makeSelectForOptions = function ( var getActual = function () { actual = {} - if (predicate.sameTerm(UI.ns.rdf('type'))) { + if (predicate.sameTerm(ns.rdf('type'))) { actual = kb.findTypeURIs(subject) } else { - kb.each(subject, predicate).map(function (x) { + kb.each(subject, predicate, null, store).map(function (x) { actual[x.uri] = true }) } @@ -1369,7 +1618,7 @@ forms.makeSelectForOptions = function ( // var newObject = null - var onChange = function (_event) { + var onChange = function (_e) { select.disabled = true // until data written back - gives user feedback too var ds = [] var is = [] @@ -1435,7 +1684,7 @@ forms.makeSelectForOptions = function ( UI.log.info('selectForOptions: stote = ' + store) UI.store.updater.update(ds, is, function (uri, ok, body) { actual = getActual() // refresh - // kb.each(subject, predicate).map(function(x){actual[x.uri] = true}) + // kb.each(subject, predicate, null, store).map(function(x){actual[x.uri] = true}) if (ok) { select.disabled = false // data written back if (newObject) { @@ -1530,11 +1779,11 @@ forms.makeSelectForCategory = function ( callbackFunction ) { var log = UI.log - var du = kb.any(category, UI.ns.owl('disjointUnionOf')) + var du = kb.any(category, ns.owl('disjointUnionOf')) var subs var multiple = false if (!du) { - subs = kb.each(undefined, UI.ns.rdfs('subClassOf'), category) + subs = kb.each(undefined, ns.rdfs('subClassOf'), category) multiple = true } else { subs = du.elements @@ -1564,7 +1813,7 @@ forms.makeSelectForCategory = function ( dom, kb, subject, - UI.ns.rdf('type'), + ns.rdf('type'), subs, { multiple: multiple, nullPrompt: '--classify--' }, store, @@ -1572,12 +1821,13 @@ forms.makeSelectForCategory = function ( ) } -// Make SELECT element to select subclasses recurively +/** Make SELECT element to select subclasses recurively // // It will so a mutually exclusive dropdown, with another if there are nested // disjoint unions. -// Callback takes (boolean ok, string errorBody) - +// +// @param callbackFunction takes (boolean ok, string errorBody) +*/ forms.makeSelectForNestedCategory = function ( dom, kb, @@ -1610,7 +1860,7 @@ forms.makeSelectForNestedCategory = function ( } if ( select.currentURI && - kb.any(kb.sym(select.currentURI), UI.ns.owl('disjointUnionOf')) + kb.any(kb.sym(select.currentURI), ns.owl('disjointUnionOf')) ) { child = forms.makeSelectForNestedCategory( dom, @@ -1690,7 +1940,7 @@ function buildCheckboxForm (dom, kb, lab, del, ins, form, store, tristate) { } if (!state && !negation) { state = null - const defa = kb.any(form, UI.ns.ui('default')) + const defa = kb.any(form, ns.ui('default')) displayState = defa ? defa.value === '1' : tristate ? null : false } } @@ -1705,15 +1955,18 @@ function buildCheckboxForm (dom, kb, lab, del, ins, form, store, tristate) { refresh() if (!editable) return box - var boxHandler = function (_event) { + var boxHandler = function (_e) { tx.style = 'color: #bbb;' // grey -- not saved yet var toDelete = input.state === true ? ins : input.state === false ? del : [] + input.newState = + input.state === null + ? true + : input.state === true + ? false + : tristate + ? null + : true - function getState (input, tristate) { - return input.state === true ? false : tristate ? null : true - } - - input.newState = input.state === null ? true : getState(input, tristate) var toInsert = input.newState === true ? ins : input.newState === false ? del : [] console.log(` Deleting ${toDelete}`) @@ -1761,7 +2014,7 @@ function buildCheckboxForm (dom, kb, lab, del, ins, form, store, tristate) { forms.buildCheckboxForm = buildCheckboxForm forms.fieldLabel = function (dom, property, form) { - var lab = UI.store.any(form, UI.ns.ui('label')) + var lab = UI.store.any(form, ns.ui('label')) if (!lab) lab = utils.label(property, true) // Init capital if (property === undefined) { return dom.createTextNode('@@Internal error: undefined property')