solid-app-set
Version:
Solid-compatible App set
1,215 lines (1,078 loc) • 54.2 kB
JavaScript
/* Contact AddressBook Pane
**
** This outline pane allows a user to interact with an contact,
to change its state according to an ontology, comment on it, etc.
**
** See also things like
** http://www.w3.org/TR/vcard-rdf/
** http://tools.ietf.org/html/rfc6350
** http://www.iana.org/assignments/vcard-elements/vcard-elements.xhtml
**
** Feross "Standard" style note: Callback functions should not be called "callback"
** or the "standard" linter will complain if the first param is not a node.js error code. (2018-01)
** Hence "callbackFunction"
*/
/* global alert, confirm, FileReader */
var UI = require('solid-ui')
var mime = require('mime-types')
var toolsPane0 = require('./toolsPane')
var toolsPane = toolsPane0.toolsPane
// if (typeof console === 'undefined') { // e.g. firefox extension. Node and browser have console
// console = {}
// console.log = function (msg) { UI.log.info(msg) }
// }
module.exports = {
icon: UI.icons.iconBase + 'noun_99101.svg', // changed from embedded icon 2016-05-01
name: 'contact',
// Does the subject deserve an contact pane?
label: function (subject) {
var kb = UI.store
var ns = UI.ns
var t = kb.findTypeURIs(subject)
if (t[ns.vcard('Individual').uri]) return 'Contact'
if (t[ns.vcard('Organization').uri]) return 'contact'
if (t[ns.vcard('Group').uri]) return 'Group'
if (t[ns.vcard('AddressBook').uri]) return 'Address book'
return null // No under other circumstances
},
mintClass: UI.ns.vcard('AddressBook'),
mintNew: function mintNew (context) {
return new Promise(function (resolve, reject) {
var dom = context.dom
var me = context.me
var div = context.div
var kb = UI.store
var ns = UI.ns
var newBase = context.newBase || context.newInstance.dir().uri
var instanceClass = context.instanceClass || ns.vcard('AddressBook')
if (instanceClass.sameTerm(ns.vcard('Group'))) {
// Make a group not an address book
var g = context.newInstance || kb.sym(context.newBase + 'index.ttl#this')
var doc = g.doc()
kb.add(g, ns.rdf('type'), ns.vcard('Group'), doc)
kb.add(g, ns.vcard('fn'), context.instanceName || 'untitled group', doc) // @@ write doc back
kb.fetcher.putBack(doc, {contentType: 'text/turtle'})
.then(function (xhr) {
resolve(context)
})
.catch(function (err) {
reject(new Error('Error creating document for new group ' + err))
})
return
}
var appInstanceNoun = 'address book'
function complain (message) {
div.appendChild(UI.widgets.errorMessageBlock(dom, message, 'pink'))
}
var bookContents = `@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.
@prefix ab: <http://www.w3.org/ns/pim/ab#>.
@prefix dc: <http://purl.org/dc/elements/1.1/>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
<#this> a vcard:AddressBook;
dc:title "New address Book";
vcard:nameEmailIndex <people.ttl>;
vcard:groupIndex <groups.ttl>.
`
bookContents += '<#this> <http://www.w3.org/ns/auth/acl#owner> <' + me.uri + '>.\n\n'
var toBeWritten = [
// { to: 'index.html', contentType: 'text/html' },
{ to: 'index.ttl', content: bookContents, contentType: 'text/turtle' },
{ to: 'groups.ttl', content: '', contentType: 'text/turtle' },
{ to: 'people.ttl', content: '', contentType: 'text/turtle' },
{ to: '', existing: true, aclOptions: { defaultForNew: true } }
]
var newAppPointer = newBase + 'index.html' // @@ assuming we can't trust server with bare dir
var offline = UI.authn.offlineTestID()
if (offline) {
toBeWritten.push({ to: 'local.html', from: 'local.html', contentType: 'text/html' })
newAppPointer = newBase + 'local.html' // kludge for testing
}
// @@ Ask user abut ACLs?
//
// @@ Add header to PUT If-None-Match: * to prevent overwrite
//
var claimSuccess = function (uri, appInstanceNoun) { // @@ delete or grey other stuff
console.log('Files created. App ready at ' + uri)
var p = div.appendChild(dom.createElement('p'))
p.setAttribute('style', 'font-size: 140%;')
p.innerHTML =
"Your <a href='" + uri + "'><b>new " + appInstanceNoun + '</b></a> is ready. ' +
"<br/><br/><a href='" + uri + "'>Go to new " + appInstanceNoun + '</a>'
resolve(context)
}
var doNextTask = function () {
if (toBeWritten.length === 0) {
claimSuccess(newAppPointer, appInstanceNoun)
} else {
var task = toBeWritten.shift()
console.log('Creating new file ' + task.to + ' in new instance ')
var dest = $rdf.uri.join(task.to, newBase) //
var aclOptions = task.aclOptions || {}
var checkOKSetACL = function (uri, ok) {
if (!ok) {
complain('Error writing new file ' + task.to)
return reject(new Error('Error writing new file ' + task.to))
}
UI.authn.setACLUserPublic(dest, me, aclOptions)
.then(() => doNextTask())
.catch(err => {
let message = 'Error setting access permissions for ' +
task.to + ' : ' + err.message
complain(message)
return reject(new Error(message))
})
}
if ('content' in task) {
kb.fetcher.webOperation('PUT', dest, { data: task.content, saveMetadata: true, contentType: task.contentType })
.then(() => checkOKSetACL(dest, true))
} else if ('existing' in task) {
checkOKSetACL(dest, true)
} else {
reject(new Error('copy not expected buiding new app'))
// var from = task.from || task.to // default source to be same as dest
// UI.widgets.webCopy(base + from, dest, task.contentType, checkOKSetACL)
}
}
}
doNextTask()
})
},
// Render the pane
render: function (subject, dom, paneOptions) {
paneOptions = paneOptions || {}
var kb = UI.store
var ns = UI.ns
// var DC = $rdf.Namespace('http://purl.org/dc/elements/1.1/')
// var DCT = $rdf.Namespace('http://purl.org/dc/terms/')
var div = dom.createElement('div')
var cardDoc = subject.doc()
UI.aclControl.preventBrowserDropEvents(dom) // protect drag and drop
div.setAttribute('class', 'contactPane')
var complain = function (message) {
console.log(message)
div.appendChild(UI.widgets.errorMessageBlock(dom, message, 'pink'))
}
var complainIfBad = function (ok, body) {
if (!ok) {
complain('Error: ' + body)
}
}
var thisPane = this
// Reproduction: Spawn a new instance of this app
var newAddressBookButton = function (thisAddressBook) {
return UI.authn.newAppInstance(dom,
{noun: 'address book', appPathSegment: 'contactorator.timbl.com'}, function (ws, newBase) {
thisPane.clone(thisAddressBook, newBase, {me: me, div: div, dom: dom})
})
} // newAddressBookButton
var updater = UI.store.updater
UI.aclControl.preventBrowserDropEvents(dom)
var t = kb.findTypeURIs(subject)
var me = UI.authn.currentUser()
var context = {
target: subject,
me: me,
noun: 'address book',
div: div,
dom: dom
} // missing: statusRegion
// Refresh the DOM tree
var refreshTree = function (root) {
if (root.refresh) {
root.refresh()
return
}
for (var i = 0; i < root.children.length; i++) {
refreshTree(root.children[i])
}
}
// Render a 3-column browser for an address book or a group
var renderThreeColumnBrowser = function (books, context, options) {
kb.fetcher.load(books).then(function (xhr) {
renderThreeColumnBrowser2(books, context, options)
}).catch(function (err) { complain(err) })
}
var renderThreeColumnBrowser2 = function (books, context, options) {
var classLabel = UI.utils.label(ns.vcard('AddressBook'))
// var IndividualClassLabel = UI.utils.label(ns.vcard('Individual'))
var book = books[0] // for now
var groupIndex = kb.any(book, ns.vcard('groupIndex'))
var selectedGroups = {}
var selectedPeople = {} // Actually prob max 1
var target = options.foreignGroup || book
var title = kb.any(target, ns.dc('title')) || kb.any(target, ns.vcard('fn'))
if (paneOptions.solo && title && typeof document !== 'undefined') {
document.title = title.value // @@ only when the outermmost pane
}
title = title ? title.value : classLabel
// The book could be the main subject, or linked from a group we are dealing with
var findBookFromGroups = function findBookFromGroups (book) {
if (book) {
return book
}
var g
for (let gu in selectedGroups) {
g = kb.sym(gu)
let b = kb.any(undefined, ns.vcard('includesGroup'), g)
if (b) return b
}
throw new Error('findBookFromGroups: Cant find address book which this group is part of')
}
// Write a new contact to the web
var createNewContact = function (book, name, selectedGroups, callbackFunction) {
book = findBookFromGroups(book)
var nameEmailIndex = kb.any(book, ns.vcard('nameEmailIndex'))
var uuid = UI.utils.genUuid()
var person = kb.sym(book.dir().uri + 'Person/' + uuid + '/index.ttl#this')
var doc = person.doc()
// Sets of statements to different files
var agenda = [ // Patch the main index to add the person
[ $rdf.st(person, ns.vcard('inAddressBook'), book, nameEmailIndex), // The people index
$rdf.st(person, ns.vcard('fn'), name, nameEmailIndex) ]
]
// @@ May be missing email - sync that differently
// sts.push(new $rdf.Statement(person, DCT('created'), new Date(), doc)); ??? include this?
for (var gu in selectedGroups) {
var g = kb.sym(gu)
var gd = g.doc()
agenda.push([ $rdf.st(g, ns.vcard('hasMember'), person, gd),
$rdf.st(person, ns.vcard('fn'), name, gd)
])
}
var updateCallback = function (uri, success, body) {
if (!success) {
console.log("Error: can't update " + uri + ' for new contact:' + body + '\n')
callbackFunction(false, "Error: can't update " + uri + ' for new contact:' + body)
} else {
if (agenda.length > 0) {
console.log('Patching ' + agenda[0] + '\n')
updater.update([], agenda.shift(), updateCallback)
} else { // done!
console.log('Done patching. Now reading back in.\n')
UI.store.fetcher.nowOrWhenFetched(doc, undefined, function (ok, body) {
if (ok) {
console.log('Read back in OK.\n')
callbackFunction(true, person)
} else {
console.log('Read back in FAILED: ' + body + '\n')
callbackFunction(false, body)
}
})
}
}
}
UI.store.fetcher.nowOrWhenFetched(nameEmailIndex, undefined, function (ok, message) {
if (ok) {
console.log(' People index must be loaded\n')
updater.put(doc, [
$rdf.st(person, ns.vcard('fn'), name, doc),
$rdf.st(person, ns.rdf('type'), ns.vcard('Individual'), doc) ],
'text/turtle', updateCallback)
} else {
console.log('Error loading people index!' + nameEmailIndex.uri + ': ' + message)
callbackFunction(false, 'Error loading people index!' + nameEmailIndex.uri + ': ' + message + '\n')
}
})
}
// Write new group to web
// Creates an empty new group file and adds it to the index
//
var saveNewGroup = function (book, name, callbackFunction) {
var gix = kb.any(book, ns.vcard('groupIndex'))
var x = book.uri.split('#')[0]
var gname = name.replace(' ', '_')
var doc = kb.sym(x.slice(0, x.lastIndexOf('/') + 1) + 'Group/' + gname + '.ttl')
var group = kb.sym(doc.uri + '#this')
console.log(' New group will be: ' + group + '\n')
UI.store.fetcher.nowOrWhenFetched(gix, function (ok, message) {
if (ok) {
console.log(' Group index must be loaded\n')
var insertTriples = [
$rdf.st(book, ns.vcard('includesGroup'), group, gix),
$rdf.st(group, ns.rdf('type'), ns.vcard('Group'), gix),
$rdf.st(group, ns.vcard('fn'), name, gix)
]
updater.update([], insertTriples, function (uri, success, body) {
if (ok) {
var triples = [
$rdf.st(book, ns.vcard('includesGroup'), group, gix), // Pointer back to book
$rdf.st(group, ns.rdf('type'), ns.vcard('Group'), doc),
$rdf.st(group, ns.vcard('fn'), name, doc)
]
updater.put(doc, triples, 'text/turtle', function (uri, ok, body) {
callbackFunction(ok, ok ? group : "Can't save new group file " + doc + body)
})
} else {
callbackFunction(ok, 'Could not update group index ' + body) // fail
}
})
} else {
console.log('Error loading people index!' + gix.uri + ': ' + message)
callbackFunction(false, 'Error loading people index!' + gix.uri + ': ' + message + '\n')
}
})
}
// organization-name is a hack for Mac records with no FN which is mandatory.
var nameFor = function (x) {
var name = kb.any(x, ns.vcard('fn')) ||
kb.any(x, ns.foaf('name')) || kb.any(x, ns.vcard('organization-name'))
return name ? name.value : '???'
}
var filterName = function (name) {
var filter = searchInput.value.trim().toLowerCase()
if (filter.length === 0) return true
var parts = filter.split(' ') // Each name part must be somewhere
for (var j = 0; j < parts.length; j++) {
var word = parts[j]
if (name.toLowerCase().indexOf(word) < 0) return false
}
return true
}
function selectPerson (person) {
cardMain.innerHTML = 'loading...'
selectedPeople = { }
selectedPeople[person.uri] = true
refreshFilteredPeople() // Color to remember which one you picked
var local = book ? localNode(person) : person
UI.store.fetcher.nowOrWhenFetched(local.doc(), undefined, function (ok, message) {
cardMain.innerHTML = ''
if (!ok) return complainIfBad(ok, "Can't load card: " + local + ': ' + message)
// console.log("Loaded card " + local + '\n')
cardMain.appendChild(cardPane(dom, local, 'contact'))
cardMain.appendChild(dom.createElement('br'))
cardMain.appendChild(UI.widgets.linkIcon(dom, local)) // hoverHide
// Add in a delete button to delete from AB
var deleteButton = UI.widgets.deleteButtonWithCheck(dom, cardMain, 'contact', function () {
let container = person.dir() // ASSUMPTION THAT CARD IS IN ITS OWN DIRECTORY
// function warn (message) { return UI.widgets.errorMessageBlock(dom, message, 'straw') }
alert('Conatiner to delete is ' + container)
let pname = kb.any(person, ns.vcard('fn'))
if (confirm('Delete contact ' + pname + ' completely?? ' + container)) {
console.log('Deleting a contact ' + pname)
deleteThing(person)
// - delete the references to it in group files and save them background
// - delete the reference in people.ttl and save it back
deleteRecursive(kb, container).then(res => {
refreshNames() // Doesn't work
cardMain.innerHTML = 'Contact Data Deleted.'
})
}
})
deleteButton.style = 'height: 2em;'
})
}
function refreshFilteredPeople (active) {
var count = 0
var lastRow = null
for (var i = 0; i < peopleMainTable.children.length; i++) {
var row = peopleMainTable.children[i]
let matches = filterName(nameFor(row.subject))
if (matches) {
count++
lastRow = row
}
row.setAttribute('style', matches
? (selectedPeople[row.subject.uri]
? 'background-color: #cce;'
: '')
: 'display: none;')
}
if (count === 1 && active) {
let unique = lastRow.subject
// selectedPeople = { }
// selectedPeople[unique.uri] = true
// lastRow.setAttribute('style', 'background-color: #cce;')
selectPerson(unique)
}
}
var selectAllGroups = function (selectedGroups, groupsMainTable, callbackFunction) {
var todo = groupsMainTable.children.length
var badness = []
for (var k = 0; k < groupsMainTable.children.length; k++) {
var groupRow = groupsMainTable.children[k]
var group = groupRow.subject
var foo = function (group, groupRow) {
groupRow.setAttribute('style', 'background-color: #ffe;')
kb.fetcher.nowOrWhenFetched(group.doc(), undefined, function (ok, message) {
if (!ok) {
var msg = "Can't load group file: " + group + ': ' + message
badness.push(msg)
return complainIfBad(ok, msg)
}
groupRow.setAttribute('style', 'background-color: #cce;')
selectedGroups[group.uri] = true
refreshGroupsSelected()
refreshNames() // @@ every time??
todo -= 1
if (!todo) {
if (callbackFunction) callbackFunction(badness.length === 0, badness)
}
})
}
foo(group, groupRow)
} // for each row
}
var sortGroups = function () {
groups = []
if (options.foreignGroup) {
groups.push(['', kb.any(options.foreignGroup, ns.vcard('fn')), options.foreignGroup])
}
if (book) {
books.map(function (book) {
var gs = book ? kb.each(book, ns.vcard('includesGroup')) : []
var gs2 = gs.map(function (g) { return [ book, kb.any(g, ns.vcard('fn')), g ] })
groups = groups.concat(gs2)
})
groups.sort()
}
}
var cardPane = function (dom, subject, paneName) {
var p = UI.panes.byName(paneName)
var d = p.render(subject, dom)
d.setAttribute('style', 'border: 0.1em solid #444; border-radius: 0.5em')
return d
}
var compareForSort = function (self, other) {
var s = nameFor(self)
var o = nameFor(other)
if (s && o) {
s = s.toLowerCase()
o = o.toLowerCase()
if (s > o) return 1
if (s < o) return -1
}
if (self.uri > other.uri) return 1
if (self.uri < other.uri) return -1
return 0
}
// In a LDP work, deletes the whole document describing a thing
// plus patch out ALL mentiosn of it! Use with care!
// beware of other dta picked up from other places being smushed
// together and then deleted.
function deleteThing (x) {
console.log('deleteThing: ' + x)
var ds = kb.statementsMatching(x).concat(kb.statementsMatching(undefined, undefined, x))
var targets = {}
ds.map(function (st) { targets[st.why.uri] = st })
var agenda = [] // sets of statements of same dcoument to delete
for (var target in targets) {
agenda.push(ds.filter(function (st) { return st.why.uri === target }))
console.log(' Deleting ' + agenda[agenda.length - 1].length + ' triples from ' + target)
}
function nextOne () {
if (agenda.length > 0) {
updater.update(agenda.shift(), [], function (uri, ok, body) {
if (!ok) {
complain('Error deleting all trace of: ' + x + ': ' + body)
return
}
nextOne()
})
} else {
console.log('Deleting resoure ' + x.doc())
kb.fetcher.delete(x.doc())
.then(function () {
console.log('Delete thing ' + x + ': complete.')
})
.catch(function (e) {
complain('Error deleting thing ' + x + ': ' + e)
})
}
}
nextOne()
}
// For deleting an addressbook sub-folder eg person - use with care!
function deleteRecursive (kb, folder) {
return new Promise(function (resolve, reject) {
kb.fetcher.load(folder).then(function () {
let promises = kb.each(folder, ns.ldp('contains')).map(file => {
if (kb.holds(file, ns.rdf('type'), ns.ldp('BasicContainer'))) {
return deleteRecursive(kb, file)
} else {
console.log('deleteRecirsive file: ' + file)
if (!confirm(' Really DELETE File ' + file)) {
throw new Error('User aborted delete file')
}
return kb.fetcher.webOperation('DELETE', file)
}
})
console.log('deleteRecirsive folder: ' + folder)
if (!confirm(' Really DELETE folder ' + folder)) {
throw new Error('User aborted delete file')
}
promises.push(kb.fetcher.webOperation('DELETE', folder))
Promise.all(promises).then(res => { resolve() })
})
})
}
var localNode = function (person, div) {
var aliases = kb.allAliases(person)
var prefix = book.dir().uri
for (var i = 0; i < aliases.length; i++) {
if (aliases[i].uri.slice(0, prefix.length) === prefix) {
return aliases[i]
}
}
throw new Error('No local URI for ' + person)
}
var refreshNames = function () {
var cards = []
for (var u in selectedGroups) {
if (selectedGroups[u]) {
var a = kb.each(kb.sym(u), ns.vcard('hasMember'))
// console.log('Adding '+ a.length + ' people from ' + u + '\n')
cards = cards.concat(a)
}
}
cards.sort(compareForSort) // @@ sort by name not UID later
for (var k = 0; k < cards.length - 1;) {
if (cards[k].uri === cards[k + 1].uri) {
cards.splice(k, 1)
} else {
k++
}
}
peopleMainTable.innerHTML = '' // clear
peopleHeader.textContent = (cards.length > 5 ? '' + cards.length + ' contacts' : 'contact')
for (var j = 0; j < cards.length; j++) {
var personRow = peopleMainTable.appendChild(dom.createElement('tr'))
var personLeft = personRow.appendChild(dom.createElement('td'))
// var personRight = personRow.appendChild(dom.createElement('td'))
personLeft.setAttribute('style', dataCellStyle)
var person = cards[j]
personRow.subject = person
var name = nameFor(person)
personLeft.textContent = name
personRow.subject = person
UI.widgets.makeDraggable(personRow, person)
var setPersonListener = function toggle (personLeft, person) {
/* No delete button on person in list: ambiguous: group or total? Do in card itself
UI.widgets.deleteButtonWithCheck(dom, personRight, 'contact', function () {
deleteThing(person) /// Just remove from group
refreshNames()
cardMain.innerHTML = ''
})
*/
personRow.addEventListener('click', function (event) {
event.preventDefault()
selectPerson(person)
})
}
setPersonListener(personRow, person)
}
refreshFilteredPeople()
}
function refreshThingsSelected (table, selectionArray) {
for (var i = 0; i < table.children.length; i++) {
var row = table.children[i]
if (row.subject) {
row.setAttribute('style', selectionArray[row.subject.uri] ? 'background-color: #cce;' : '')
}
}
}
function refreshGroupsSelected () {
return refreshThingsSelected(groupsMainTable, selectedGroups)
}
// Check every group is in the list and add it if not.
var syncGroupTable = function () {
var foundOne
sortGroups()
var i
for (i = 0; i < groupsMainTable.children.length; i++) {
var row = groupsMainTable.children[i]
row.trashMe = true
}
for (var g = 0; g < groups.length; g++) {
// var book = groups[g][0]
var name = groups[g][1]
var group = groups[g][2]
// selectedGroups[group.uri] = false
foundOne = false
for (i = 0; i < groupsMainTable.children.length; i++) {
let row = groupsMainTable.children[i]
if (row.subject && row.subject.sameTerm(group)) {
row.trashMe = false
foundOne = true
break
}
}
if (!foundOne) {
var groupRow = groupsMainTable.appendChild(dom.createElement('tr'))
groupRow.subject = group
UI.widgets.makeDraggable(groupRow, group)
groupRow.setAttribute('style', dataCellStyle)
groupRow.textContent = name
var foo = function addNewGroupRow2 (groupRow, group, name) {
// Is something is dropped on a group, add people to group
var handleURIsDroppedOnGroup = function (uris) {
uris.forEach(function (u) {
console.log('Dropped on group: ' + u)
var thing = kb.sym(u)
var toBeFetched = [thing.doc(), group.doc()]
kb.fetcher.load(toBeFetched).then(function (xhrs) {
var types = kb.findTypeURIs(thing)
for (var ty in types) {
console.log(' drop object type includes: ' + ty) // @@ Allow email addresses and phone numbers to be dropped?
}
if (ns.vcard('Individual').uri in types || ns.vcard('Organization').uri in types) {
var pname = kb.any(thing, ns.vcard('fn'))
if (!pname) return alert('No vcard name known for ' + thing)
var already = kb.holds(group, ns.vcard('hasMember'), thing, group.doc())
if (already) return alert('ALREADY added ' + pname + ' to group ' + name)
var message = 'Add ' + pname + ' to group ' + name + '?'
if (confirm(message)) {
var ins = [ $rdf.st(group, ns.vcard('hasMember'), thing, group.doc()),
$rdf.st(thing, ns.vcard('fn'), pname, group.doc())]
kb.updater.update([], ins, function (uri, ok, err) {
if (!ok) return complain('Error adding member to group ' + group + ': ' + err)
console.log('Added ' + pname + ' to group ' + name)
// @@ refresh UI
})
}
}
}).catch(function (e) {
complain('Error looking up dropped thing ' + thing + ' and group: ' + e)
})
})
}
UI.widgets.makeDropTarget(groupRow, handleURIsDroppedOnGroup)
UI.widgets.deleteButtonWithCheck(dom, groupRow, 'group ' + name, function () {
deleteThing(group)
syncGroupTable()
})
groupRow.addEventListener('click', function (event) {
event.preventDefault()
var groupList = kb.sym(group.uri.split('#')[0])
if (!event.metaKey) {
selectedGroups = {} // If Command key pressed, accumulate multiple
}
selectedGroups[group.uri] = !selectedGroups[group.uri]
refreshGroupsSelected()
peopleMainTable.innerHTML = '' // clear in case refreshNames doesn't work for unknown reason
kb.fetcher.nowOrWhenFetched(groupList.uri, undefined, function (ok, message) {
if (!ok) return complainIfBad(ok, "Can't load group file: " + groupList + ': ' + message)
refreshNames()
if (!event.metaKey) { // If only one group has beeen selected show ACL
cardMain.innerHTML = ''
var visible = false
var aclControl = UI.aclControl.ACLControlBox5(group, dom, 'group', kb, function (ok, body) {
if (!ok) cardMain.innerHTML = 'Failed: ' + body
})
var sharingButton = cardMain.appendChild(dom.createElement('button'))
sharingButton.style.cssText = 'padding: 1em; margin: 1em'
var img = sharingButton.appendChild(dom.createElement('img'))
img.style.cssText = 'width: 1.5em; height: 1.5em'
img.setAttribute('src', UI.icons.iconBase + 'noun_123691.svg')
sharingButton.addEventListener('click', function () {
visible = !visible
if (visible) {
cardMain.appendChild(aclControl)
} else {
cardMain.removeChild(aclControl)
}
})
}
})
}, true)
}
foo(groupRow, group, name)
} // if not foundOne
} // loop g
for (i = 0; i < groupsMainTable.children.length; i++) {
let r = groupsMainTable.children[i]
if (r.trashMe) {
groupsMainTable.removeChild(r)
}
}
refreshGroupsSelected()
} // syncGroupTable
// //////////////////////////// Three-column Contact Browser - Body
// UI.store.fetcher.nowOrWhenFetched(groupIndex.uri, book, function (ok, body) {
// if (!ok) return console.log('Cannot load group index: ' + body)
// ////////////////// Body of 3-column browser
var bookTable = dom.createElement('table')
bookTable.setAttribute('style', 'border-collapse: collapse; margin-right: 0; max-height: 9in;')
div.appendChild(bookTable)
var bookHeader = bookTable.appendChild(dom.createElement('tr'))
var bookMain = bookTable.appendChild(dom.createElement('tr'))
var bookFooter = bookTable.appendChild(dom.createElement('tr'))
var groupsHeader = bookHeader.appendChild(dom.createElement('td'))
var peopleHeader = bookHeader.appendChild(dom.createElement('td'))
var cardHeader = bookHeader.appendChild(dom.createElement('td'))
var groupsMain = bookMain.appendChild(dom.createElement('td'))
var groupsMainTable = groupsMain.appendChild(dom.createElement('table'))
var peopleMain = bookMain.appendChild(dom.createElement('td'))
var peopleMainTable = peopleMain.appendChild(dom.createElement('table'))
var groupsFooter = bookFooter.appendChild(dom.createElement('td'))
var peopleFooter = bookFooter.appendChild(dom.createElement('td'))
var cardFooter = bookFooter.appendChild(dom.createElement('td'))
cardHeader.appendChild(dom.createElement('div')) // searchDiv
// searchDiv.setAttribute('style', 'border: 0.1em solid #888; border-radius: 0.5em')
var searchInput = cardHeader.appendChild(dom.createElement('input'))
searchInput.setAttribute('type', 'text')
searchInput.setAttribute('style', 'border: 0.1em solid #444; border-radius: 0.5em; width: 100%; font-size: 100%; padding: 0.1em 0.6em')
searchInput.addEventListener('input', function (e) {
refreshFilteredPeople(true) // Active: select person if justone left
})
var cardMain = bookMain.appendChild(dom.createElement('td'))
cardMain.setAttribute('style', 'margin: 0;') // fill space available
var dataCellStyle = 'padding: 0.1em;'
groupsHeader.textContent = 'groups'
groupsHeader.setAttribute('style', 'min-width: 10em; padding-bottom 0.2em;')
var setGroupListVisibility = function (visible) {
var vis = visible ? '' : 'display: none;'
groupsHeader.setAttribute('style', 'min-width: 10em; padding-bottom 0.2em;' + vis)
var hfstyle = 'padding: 0.1em;'
groupsMain.setAttribute('style', hfstyle + vis)
groupsFooter.setAttribute('style', hfstyle + vis)
}
setGroupListVisibility(true)
var groups
if (options.foreignGroup) {
selectedGroups[options.foreignGroup.uri] = true
}
if (book) {
var allGroups = groupsHeader.appendChild(dom.createElement('button'))
allGroups.textContent = 'All'
var style = 'margin-left: 1em; font-size: 100%;'
allGroups.setAttribute('style', style)
allGroups.addEventListener('click', function (event) {
allGroups.state = allGroups.state ? 0 : 1
peopleMainTable.innerHTML = '' // clear in case refreshNames doesn't work for unknown reason
if (allGroups.state) {
allGroups.setAttribute('style', style + 'background-color: #ff8;')
selectAllGroups(selectedGroups, groupsMainTable, function (ok, message) {
if (!ok) return complain(message)
allGroups.setAttribute('style', style + 'background-color: black; color: white')
refreshGroupsSelected()
})
} else {
allGroups.setAttribute('style', style + 'background-color: #cfc;') // pale green hint groups loaded
selectedGroups = {}
refreshGroupsSelected()
}
}) // on button click
UI.store.fetcher.nowOrWhenFetched(groupIndex.uri, book, function (ok, body) {
if (!ok) return console.log('Cannot load group index: ' + body)
syncGroupTable()
refreshNames()
})
} else {
syncGroupTable()
refreshNames()
console.log('No book, only one group -> hide list of groups')
setGroupListVisibility(false) // If no books involved, hide group list
} // if not book
peopleHeader.textContent = 'name'
peopleHeader.setAttribute('style', 'min-width: 18em;')
peopleMain.setAttribute('style', 'overflow:scroll;')
// New Contact button
var newContactButton = dom.createElement('button')
var container = dom.createElement('div')
newContactButton.setAttribute('type', 'button')
if (!me) newContactButton.setAttribute('disabled', 'true')
UI.authn.checkUser()
.then(webId => {
if (webId) {
me = webId
newContactButton.removeAttribute('disabled')
}
})
container.appendChild(newContactButton)
newContactButton.innerHTML = 'New Contact' // + IndividualClassLabel
peopleFooter.appendChild(container)
newContactButton.addEventListener('click', function (e) {
// b.setAttribute('disabled', 'true'); (do we need o do this?)
cardMain.innerHTML = ''
var ourBook = findBookFromGroups(book)
kb.fetcher.load(ourBook)
.then(function (response) {
if (!response.ok) throw new Error('Book won\'t load:' + ourBook)
var nameEmailIndex = kb.any(ourBook, ns.vcard('nameEmailIndex'))
if (!nameEmailIndex) throw new Error('Wot no nameEmailIndex?')
return kb.fetcher.load(nameEmailIndex)
})
.then(function (response) { console.log('Name index loaded async' + response.url) })
UI.widgets.askName(dom, kb, cardMain, UI.ns.foaf('name'), ns.vcard('Individual'), 'person').then(function (name) {
if (!name) return // cancelled by user
createNewContact(book, name, selectedGroups, function (success, body) {
if (!success) {
console.log("Error: can't save new contact:" + body)
} else {
let person = body
cardMain.innerHTML = 'indexing...'
selectedPeople = {}
selectedPeople[person.uri] = true
refreshNames() // Add name to list of group
cardMain.appendChild(cardPane(dom, person, 'contact'))
}
})
})
}, false)
// New Group button
if (book) {
var newGroupButton = groupsFooter.appendChild(dom.createElement('button'))
newGroupButton.setAttribute('type', 'button')
newGroupButton.innerHTML = 'New Group' // + IndividualClassLabel
newGroupButton.addEventListener('click', function (e) {
// b.setAttribute('disabled', 'true'); (do we need o do this?)
cardMain.innerHTML = ''
var groupIndex = kb.any(book, ns.vcard('groupIndex'))
UI.store.fetcher.nowOrWhenFetched(groupIndex, undefined, function (ok, message) {
if (ok) {
console.log(' Group index has been loaded\n')
} else {
console.log('Error: Group index has NOT been loaded' + message + '\n')
}
})
UI.widgets.askName(dom, kb, cardMain, UI.ns.foaf('name'), ns.vcard('Group'), 'group').then(function (name) {
if (!name) return // cancelled by user
saveNewGroup(book, name, function (success, body) {
if (!success) {
console.log("Error: can't save new group:" + body)
cardMain.innerHTML = 'Failed to save group' + body
} else {
selectedGroups = {}
selectedGroups[body.uri] = true
syncGroupTable() // Refresh list of groups
cardMain.innerHTML = ''
cardMain.appendChild(UI.aclControl.ACLControlBox5(body, dom, 'group', kb, function (ok, body) {
if (!ok) cardMain.innerHTML = 'Group sharing setup failed: ' + body
}))
}
})
})
}, false)
// Tools button
var toolsButton = cardFooter.appendChild(dom.createElement('button'))
toolsButton.setAttribute('type', 'button')
toolsButton.innerHTML = 'Tools'
toolsButton.addEventListener('click', function (e) {
cardMain.innerHTML = ''
cardMain.appendChild(toolsPane(selectAllGroups, selectedGroups,
groupsMainTable, book, dom, me))
})
} // if book
cardFooter.appendChild(newAddressBookButton(book))
// })
div.appendChild(dom.createElement('hr'))
// div.appendChild(newAddressBookButton(book)) // later
// end of AddressBook instance
} // renderThreeColumnBrowser
// ///////////////////////////////////////////////////////////////////////////////////
// Render Individual card
var renderIndividual = function (subject) {
var mugshotDiv
// //////////////////// DRAG and Drop for mugshot image
var card = subject
var handleDroppedThing = function (thing) {
kb.fetcher.nowOrWhenFetched(thing.doc(), function (ok, mess) {
if (!ok) {
console.log('Error looking up dropped thing ' + thing + ': ' + mess)
} else {
var types = kb.findTypeURIs(thing)
for (var ty in types) {
console.log(' drop object type includes: ' + ty) // @@ Allow email addresses and phone numbers to be dropped?
}
console.log('Default: assume web page ' + thing) // icon was: UI.icons.iconBase + 'noun_25830.svg'
kb.add(card, ns.wf('attachment'), thing, card.doc())
/*
var b = kb.bnode()
kb.add(card, UI.ns.vcard('url'), b, card.doc())
kb.add(b, UI.ns.vcard('value'), kb.sym(u), card.doc())
*/
// @@ refresh UI
}
})
}
var uploadFileToContact = function (filename, contentType, data) {
// var fileExtension = filename.split('.').pop() // .toLowerCase()
var extension = mime.extension(contentType)
if (contentType !== mime.lookup(filename)) {
filename += '_.' + extension
console.log('MIME TYPE MISMATCH -- adding extension: ' + filename)
}
let prefix, predicate, isImage
if (contentType.startsWith('image')) {
prefix = 'image_'
predicate = ns.vcard('hasPhoto')
isImage = true
} else {
prefix = 'attachment_'
predicate = ns.wf('attachment')
isImage = false
}
var n, pic
for (n = 0; ; n++) { // Check filename is not used or invent new one
pic = kb.sym(card.dir().uri + filename)
if (!kb.holds(subject, ns.vcard('hasPhoto'), pic)) {
break
}
filename = prefix + n + '.' + extension
}
kb.add(subject, predicate, pic, subject.doc())
console.log('Putting ' + data.length + ' bytes of ' + contentType + ' to ' + pic)
kb.fetcher.webOperation('PUT', pic, {data: data, contentType: contentType})
.then(function (response) {
if (!response.ok) {
complain('Error uploading ' + pic + ':' + response.status)
return
}
console.log(' Upload: put OK: ' + pic)
return kb.fetcher.putBack(subject.doc())
})
.then(function () {
if (isImage) {
mugshotDiv.refresh()
}
})
.catch(function (status) {
console.log(' Upload: FAIL ' + pic + ', Error: ' + status)
})
}
// When a set of URIs are dropped on
var handleURIsDroppedOnMugshot = function (uris) {
uris.map(function (u) {
var thing = $rdf.sym(u) // Attachment needs text label to disinguish I think not icon.
console.log('Dropped on mugshot thing ' + thing) // icon was: UI.icons.iconBase + 'noun_25830.svg'
if (u.startsWith('http') && u.indexOf('#') < 0) { // Plain document
// Take a copy of a photo on the web:
kb.fetcher.webOperation('GET', thing.uri).then(result => {
let contentType = result.headers.get('Content-Type')
// let data = result.responseText
let pathEnd = thing.uri.split('/').slice(-1)[0] // last segment as putative filename
pathEnd = pathEnd.split('?')[0] // chop off any query params
result.arrayBuffer().then(function (data) { // read text stream
if (!result.ok) {
complain('Error downloading ' + thing + ':' + result.status)
return
}
uploadFileToContact(pathEnd, contentType, data)
})
})
return
} else {
console.log('Not a web document URI, cannot copy as picture: ' + thing)
}
handleDroppedThing(thing)
})
}
// Drop an image file to set up the mugshot
var droppedFileHandler = function (files) {
for (var i = 0; i < files.length; i++) {
let f = files[i]
console.log(' meeting: Filename: ' + f.name + ', type: ' + (f.type || 'n/a') +
' size: ' + f.size + ' bytes, last modified: ' +
(f.lastModifiedDate ? f.lastModifiedDate.toLocaleDateString() : 'n/a')
) // See e.g. https://www.html5rocks.com/en/tutorials/file/dndfiles/
// @@ Add: progress bar(s)
var reader = new FileReader()
reader.onload = (function (theFile) {
return function (e) {
var data = e.target.result
console.log(' File read byteLength : ' + data.byteLength)
var filename = encodeURIComponent(theFile.name)
var contentType = theFile.type
uploadFileToContact(filename, contentType, data)
}
})(f)
reader.readAsArrayBuffer(f)
}
}
// //////// End of drag and drop
// Background metadata for this pane we bundle with the JS
var individualForm = kb.sym('https://linkeddata.github.io/solid-app-set/contact/individualForm.ttl#form1')
var individualFormDoc = individualForm.doc()
if (!kb.holds(undefined, undefined, undefined, individualFormDoc)) { // If not loaded already
var individualFormText = require('./individualForm.js')
$rdf.parse(individualFormText, kb, individualFormDoc.uri, 'text/turtle') // Load form directly
}
var vcardOnt = UI.ns.vcard('Type').doc()
if (!kb.holds(undefined, undefined, undefined, vcardOnt)) { // If not loaded already
$rdf.parse(require('./vcard.js'), kb, vcardOnt.uri, 'text/turtle') // Load ontology directly
}
var toBeFetched = [ subject.doc() ] // was: individualFormDoc, UI.ns.vcard('Type').doc()
UI.store.fetcher.load(toBeFetched)
.catch(function (e) {
console.log('Error: Failed to load form or ontology: ' + e)
}) // load.then
.then(function (xhrs) {
var setPaneStyle = function () {
var types = kb.findTypeURIs(subject)
var mystyle = 'padding: 0.5em 1.5em 1em 1.5em; '
var backgroundColor = null
for (var uri in types) {
backgroundColor = kb.any(kb.sym(uri), kb.sym('http://www.w3.org/ns/ui#backgroundColor'))
if (backgroundColor) break
}
backgroundColor = backgroundColor ? backgroundColor.value : '#fff' // default white
mystyle += 'background-color: ' + backgroundColor + '; '
div.setAttribute('style', mystyle)
}
setPaneStyle()
UI.authn.checkUser() // kick off async operation
mugshotDiv = div.appendChild(dom.createElement('div'))
function mugshot (image) {
let img = div.appendChild(dom.createElement('img'))
img.setAttribute('style', 'max-height: 10em; border-radius: 1em; margin: 0.7em;')
UI.widgets.makeDropTarget(img, handleURIsDroppedOnMugshot, droppedFileHandler)
if (image) img.setAttribute('src', image.uri)
return img
}
function syncMugshots () {
mugshotDiv.innerHTML = '' // @@ don't clear, later: sync
let images = kb.each(subject, ns.vcard('hasPhoto')) // Priviledge vcard ones
images.sort()
images = images.slice(0, 5) // max number for the space
images.forEach(mugshot)
if (images.length === 0) {
UI.widgets.setImage(mugshot(), subject) // Fallback icon or get from web
}
}
syncMugshots()
mugshotDiv.refresh = syncMugshots
UI.widgets.appendForm(dom, div, {}, subject, individualForm, cardDoc, complainIfBad)
// Comment/discussion area
/*
var messageStore = kb.any(tracker, ns.wf('messageStore'))
if (!messageStore) messageStore = kb.any(tracker, ns.wf('doc'))
div.appendChild(UI.messageArea(dom, kb, subject, messageStore))
donePredicate(ns.wf('message'))
*/
div.appendChild(dom.createElement('tr'))
.setAttribute('style', 'height: 1em') // spacer
var lookUpId = function (dom, container, x) {
var tr = table.appendChild(dom.createElement('tr'))
tr.setAttribute('style', 'margin-top: 0.1em solid #ccc;')
var nameTD = tr.appendChild(dom.createElement('td'))
var formTD = tr.appendChild(dom.createElement('td'))
nameTD.textContent = x.uri.split('/')[2]
kb.fetcher.load(x)
.then(function (xhr) {
nameTD.textContent = x.uri.split('/')[2] + ' (' +
kb.statementsMatching(undefined, undefined, undefined, x.doc()).length + ')'
})
.catch(function (e) {
formTD.appendChild(UI.widgets.errorMessageBlock(dom, e, 'pink'))
})
formTD.appendChild(UI.widgets.linkIcon(dom, x))
}
var table = div.appendChild(dom.createElement('table'))
var aliases = kb.allAliases(subject)
if (aliases.length > 1) {
for (var i = 0; i < aliases.length; i++) {
var x = aliases[i]
if (!x.sameTerm(subject)) {
lookUpId(dom, table, x)
// UI.widgets.appendForm(dom, formTD, {}, x, individualForm, x.doc(), complainIfBad)
}
}
}
UI.widgets.attachmentList(dom, subject, div, {
// promptIcon: UI.icons.iconBase + 'noun_681601.svg',
predicate: UI.ns.vcard('url') // @@@@@@@@@ ,--- no, the vcard ontology structure uses a bnode.
})
div.appendChild(dom.createElement('hr'))
var pages = kb.each(subject, ns.vcard('url')) // vcard:url [ a vcard:HomePage; vcard:value <http://www.w3.org/People/Berners-Lee>],
pages.forEach(function (p) {
var cla = kb.any(p, ns.