src/widgets/buttons.js
/* UI Widgets such as buttons
*/
/* global alert */
const $rdf = require('rdflib')
module.exports = {}
var buttons = {}
var UI = {
icons: require('../iconBase'),
log: require('../log'),
ns: require('../ns'),
store: require('../store'),
style: require('../style')
// widgets: widgets // @@
}
const utils = require('../utils')
const error = require('./error')
const dragAndDrop = require('./dragAndDrop')
const cancelIconURI = UI.icons.iconBase + 'noun_1180156.svg' // black X
const checkIconURI = UI.icons.iconBase + 'noun_1180158.svg' // green checkmark; Continue
function getStatusArea (context) {
var box = context.statusArea || context.div || null
if (box) return box
let dom = context.dom
if (!dom && typeof document !== 'undefined') {
dom = document
}
if (dom) {
var body = dom.getElementsByTagName('body')[0]
box = dom.createEvent('div')
body.insertBefore(box, body.firstElementChild)
context.statusArea = box
return box
}
return null
}
function complain (context, err) {
if (!err) return // only if error
var ele = context.statusArea || context.div || getStatusArea(context)
console.log('Complaint: ' + err)
if (ele) ele.appendChild(error.errorMessageBlock(context.dom, err))
else alert(err)
}
buttons.complain = complain
// var UI.ns = require('./ns.js')
// var utilsModule = require('./utils')
// var aclControlModule = require('./acl-control')
// paneUtils = {}
buttons.clearElement = function (ele) {
while (ele.firstChild) {
ele.removeChild(ele.firstChild)
}
return ele
}
buttons.refreshTree = function (root) {
if (root.refresh) {
root.refresh()
return
}
for (var i = 0; i < root.children.length; i++) {
buttons.refreshTree(root.children[i])
}
}
// To figure out the log URI from the full URI used to invoke the reasoner
buttons.extractLogURI = function (fullURI) {
var logPos = fullURI.search(/logFile=/)
var rulPos = fullURI.search(/&rulesFile=/)
return fullURI.substring(logPos + 8, rulPos)
}
// @@@ This needs to be changed to local timee
// noTime - only give date, no time,
buttons.shortDate = function (str, noTime) {
if (!str) return '???'
var month = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
try {
var nowZ = new Date().toISOString()
// var nowZ = $rdf.term(now).value
// var n = now.getTimezoneOffset() // Minutes
if (str.slice(0, 10) === nowZ.slice(0, 10) && !noTime) {
return str.slice(11, 16)
}
if (str.slice(0, 4) === nowZ.slice(0, 4)) {
return (
month[parseInt(str.slice(5, 7), 10) - 1] +
' ' +
parseInt(str.slice(8, 10), 10)
)
}
return str.slice(0, 10)
} catch (e) {
return 'shortdate:' + e
}
}
buttons.formatDateTime = function (date, format) {
return format
.split('{')
.map(function (s) {
var k = s.split('}')[0]
var width = { Milliseconds: 3, FullYear: 4 }
var d = { Month: 1 }
return s
? ('000' + (date['get' + k]() + (d[k] || 0))).slice(-(width[k] || 2)) + s.split('}')[1]
: ''
})
.join('')
}
buttons.timestamp = function () {
return buttons.formatDateTime(
new Date(),
'{FullYear}-{Month}-{Date}T{Hours}:{Minutes}:{Seconds}.{Milliseconds}'
)
}
buttons.shortTime = function () {
return buttons.formatDateTime(
new Date(),
'{Hours}:{Minutes}:{Seconds}.{Milliseconds}'
)
}
// ///////////////////// Handy UX widgets
// Sets the best name we have and looks up a better one
//
buttons.setName = function (element, x) {
var kb = UI.store
var ns = UI.ns
var findName = 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 : null
}
var name = x.sameTerm(ns.foaf('Agent')) ? 'Everyone' : findName(x)
element.textContent = name || utils.label(x)
if (!name && x.uri) {
// Note this is only a fetch, not a lookUP of all sameAs etc
kb.fetcher.nowOrWhenFetched(x.doc(), undefined, function (_ok) {
element.textContent = findName(x) || utils.label(x) // had: (ok ? '' : '? ') +
})
}
}
// Set of suitable images
buttons.imagesOf = function (x, kb) {
var ns = UI.ns
return kb
.each(x, ns.sioc('avatar'))
.concat(kb.each(x, ns.foaf('img')))
.concat(kb.each(x, ns.vcard('logo')))
.concat(kb.each(x, ns.vcard('photo')))
.concat(kb.each(x, ns.foaf('depiction')))
}
// Best logo or avater or photo etc to represent someone or some group etc
//
buttons.iconForClass = {
// Potentially extendable by other apps, panes, etc
// Relative URIs to the iconBase
'solid:AppProviderClass': 'noun_144.svg', // @@ classs name should not contain 'Class'
'solid:AppProvider': 'noun_15177.svg', // @@
'solid:Pod': 'noun_Cabinet_1434380.svg',
'vcard:Group': 'noun_339237.svg',
'vcard:Organization': 'noun_143899.svg',
'vcard:Individual': 'noun_15059.svg',
'schema:Person': 'noun_15059.svg',
'foaf:Person': 'noun_15059.svg',
'foaf:Agent': 'noun_98053.svg',
'acl:AuthenticatedAgent': 'noun_99101.svg',
'prov:SoftwareAgent': 'noun_Robot_849764.svg', // Bot
'vcard:AddressBook': 'noun_15695.svg',
'trip:Trip': 'noun_581629.svg',
'meeting:LongChat': 'noun_1689339.svg',
'meeting:Meeting': 'noun_66617.svg',
'meeting:Project': 'noun_1036577.svg',
'ui:Form': 'noun_122196.svg',
'rdfs:Class': 'class-rectangle.svg', // For RDF developers
'rdf:Property': 'property-diamond.svg',
'owl:Ontology': 'noun_classification_1479198.svg'
}
var tempSite = function (x) {
// use only while one in rdflib fails with origins 2019
var str = x.uri.split('#')[0]
var p = str.indexOf('//')
if (p < 0) throw new Error('This URI does not have a web site part (origin)')
var q = str.indexOf('/', p + 2)
if (q < 0) {
// no third slash?
return str.slice(0) + '/' // Add slash to a bare origin
} else {
return str.slice(0, q + 1)
}
}
/**
* Find an image for this thing as a class
*/
buttons.findImageFromURI = function findImageFromURI (x) {
const iconDir = UI.icons.iconBase
// Special cases from URI scheme:
if (x.uri) {
if (
x.uri.split('/').length === 4 &&
!x.uri.split('/')[1] &&
!x.uri.split('/')[3]
) {
return iconDir + 'noun_15177.svg' // App -- this is an origin
}
// Non-HTTP URI types imply types
if (x.uri.startsWith('message:') || x.uri.startsWith('mid:')) {
// message: is aapple bug-- should be mid:
return iconDir + 'noun_480183.svg' // envelope noun_567486
}
if (x.uri.startsWith('mailto:')) {
return iconDir + 'noun_567486.svg' // mailbox - an email desitination
}
// For HTTP(s) documents, we could look at the MIME type if we know it.
if (x.uri.startsWith('https:') && x.uri.indexOf('#') < 0) {
return tempSite(x) + 'favicon.ico' // was x.site().uri + ...
// Todo: make the docuent icon a fallback for if the favicon does not exist
// todo: pick up a possible favicon for the web page istelf from a link
// was: return iconDir + 'noun_681601.svg' // document - under solid assumptions
}
return null
}
return iconDir + 'noun_10636_grey.svg' // Grey Circle - some thing
}
/* Find something we have as explict image data for the thing
*/
buttons.findImage = thing => {
const kb = UI.store
const ns = UI.ns
const iconDir = UI.icons.iconBase
if (thing.sameTerm(ns.foaf('Agent')) || thing.sameTerm(ns.rdf('Resource'))) {
return iconDir + 'noun_98053.svg' // Globe
}
const image =
kb.any(thing, ns.sioc('avatar')) ||
kb.any(thing, ns.foaf('img')) ||
kb.any(thing, ns.vcard('logo')) ||
kb.any(thing, ns.vcard('hasPhoto')) ||
kb.any(thing, ns.vcard('photo')) ||
kb.any(thing, ns.foaf('depiction'))
return image ? image.uri : null
}
/**
* Do the best you can with the data available
*
* @return {Boolean} Are we happy with this icon?
* Sets src AND STYLE of the image.
*/
buttons._trySetImage = function _trySetImage (element, thing, iconForClassMap) {
const kb = UI.store
const explitImage = buttons.findImage(thing)
if (explitImage) {
element.setAttribute('src', explitImage)
return true
}
// This is one of the classes we know about - the class itself?
const typeIcon = iconForClassMap[thing.uri]
if (typeIcon) {
element.setAttribute('src', typeIcon)
element.style = UI.style.classIconStyle
// element.style.border = '0.1em solid green;'
// element.style.backgroundColor = '#eeffee' // pale green
return true
}
const schemeIcon = buttons.findImageFromURI(thing)
if (schemeIcon) {
element.setAttribute('src', schemeIcon)
return true // happy with this -- don't look it up
}
// Do we have a generic icon for something in any class its in?
const types = kb.findTypeURIs(thing)
for (var typeURI in types) {
if (iconForClassMap[typeURI]) {
element.setAttribute('src', iconForClassMap[typeURI])
return false // maybe we can do better
}
}
element.setAttribute('src', UI.icons.iconBase + 'noun_10636_grey.svg') // Grey Circle - some thing
return false // we can do better
}
// ToDo: Also add icons for *properties* like home, work, email, range, domain, comment,
//
buttons.setImage = function (element, thing) { // 20191230a
const kb = UI.store
const ns = UI.ns
var iconForClassMap = {}
for (var k in buttons.iconForClass) {
const pref = k.split(':')[0]
const id = k.split(':')[1]
const klass = ns[pref](id)
iconForClassMap[klass.uri] = $rdf.uri.join(buttons.iconForClass[k], UI.icons.iconBase)
}
const happy = buttons._trySetImage(element, thing, iconForClassMap)
if (!happy && thing.uri) {
kb.fetcher.nowOrWhenFetched(thing.doc(), undefined, (ok) => {
if (ok) {
buttons._trySetImage(element, thing, iconForClassMap)
}
})
}
}
// If a web page then a favicon with a fallback to
// See eg http://stackoverflow.com/questions/980855/inputting-a-default-image
var faviconOrDefault = function (dom, x) {
var image = dom.createElement('img')
image.style = UI.style.iconStyle
var isOrigin = function (x) {
if (!x.uri) return false
var parts = x.uri.split('/')
return parts.length === 3 || (parts.length === 4 && parts[3] === '')
}
image.setAttribute(
'src',
UI.icons.iconBase + (isOrigin(x) ? 'noun_15177.svg' : 'noun_681601.svg') // App symbol vs document
)
if (x.uri && x.uri.startsWith('https:') && x.uri.indexOf('#') < 0) {
var res = dom.createElement('object') // favico with a fallback of a default image if no favicon
res.setAttribute('data', tempSite(x) + 'favicon.ico')
res.setAttribute('type', 'image/x-icon')
res.appendChild(image) // fallback
return res
} else {
buttons.setImage(image, x)
return image
}
}
// Delete button with a check you really mean it
//
// @@ Supress check if command key held down?
//
buttons.deleteButtonWithCheck = function (
dom,
container,
noun,
deleteFunction
) {
var minusIconURI = UI.icons.iconBase + 'noun_2188_red.svg' // white minus in red #cc0000 circle
// var delButton = dom.createElement('button')
var img = dom.createElement('img')
img.setAttribute('src', minusIconURI) // plus sign
img.setAttribute('style', 'margin: 0.2em; width: 1em; height:1em')
img.title = 'Remove this ' + noun
var delButton = img
container.appendChild(delButton)
container.setAttribute('class', 'hoverControl') // See tabbedtab.css (sigh global CSS)
delButton.setAttribute('class', 'hoverControlHide')
// delButton.setAttribute('style', 'color: red; margin-right: 0.3em; foat:right; text-align:right')
delButton.addEventListener(
'click',
function (_event) {
container.removeChild(delButton) // Ask -- are you sure?
var cancelButton = dom.createElement('button')
// cancelButton.textContent = 'cancel'
cancelButton.setAttribute('style', UI.style.buttonStyle)
var img = cancelButton.appendChild(dom.createElement('img'))
img.setAttribute('src', cancelIconURI)
img.setAttribute('style', UI.style.buttonStyle)
container.appendChild(cancelButton).addEventListener(
'click',
function (_event) {
container.removeChild(sureButton)
container.removeChild(cancelButton)
container.appendChild(delButton)
},
false
)
var sureButton = dom.createElement('button')
sureButton.textContent = 'Delete ' + noun
sureButton.setAttribute('style', UI.style.buttonStyle)
container.appendChild(sureButton).addEventListener(
'click',
function (_event) {
container.removeChild(sureButton)
container.removeChild(cancelButton)
deleteFunction()
},
false
)
},
false
)
return delButton
}
/* Make a button
*
* @param dom - the DOM document object
* @Param iconURI - the URI of theb icon to use
* @param text - the tooltip text or possibly button contents text
* @param handler <function> - A handler to called when button is clicked
*
* @returns <dDomElement> - the button
*/
buttons.button = function (dom, iconURI, text, handler) {
var button = dom.createElement('button')
button.setAttribute('type', 'button')
button.setAttribute('style', UI.style.buttonStyle)
// button.innerHTML = text // later, user preferences may make text preferred for some
var img = button.appendChild(dom.createElement('img'))
img.setAttribute('src', iconURI)
img.setAttribute('style', 'width: 2em; height: 2em;') // trial and error. 2em disappears
img.title = text
if (handler) {
button.addEventListener('click', handler, false)
}
return button
}
buttons.cancelButton = function (dom, handler) {
return buttons.button(dom, cancelIconURI, 'Cancel', handler)
}
buttons.continueButton = function (dom, handler) {
return buttons.button(dom, checkIconURI, 'Continue', handler)
}
/* Grab a name for a new thing
*
* Form to get the name of a new thing before we create it
* @returns: a promise of (a name or null if cancelled)
*/
buttons.askName = function (dom, kb, container, predicate, klass, noun) {
// eslint-disable-next-line promise/param-names
return new Promise(function (resolve, _reject) {
var form = dom.createElement('div') // form is broken as HTML behaviour can resurface on js error
// classLabel = utils.label(ns.vcard('Individual'))
predicate = predicate || UI.ns.foaf('name') // eg 'name' in user's language
noun = noun || (klass ? utils.label(klass) : ' ') // eg 'folder' in users's language
var prompt = noun + ' ' + utils.label(predicate) + ': '
form.appendChild(dom.createElement('p')).textContent = prompt
var namefield = dom.createElement('input')
namefield.setAttribute('type', 'text')
namefield.setAttribute('size', '100')
namefield.setAttribute('maxLength', '2048') // No arbitrary limits
namefield.setAttribute('style', UI.style.textInputStyle)
namefield.select() // focus next user input
form.appendChild(namefield)
container.appendChild(form)
// namefield.focus()
function gotName () {
form.parentNode.removeChild(form)
resolve(namefield.value.trim())
}
namefield.addEventListener('keyup', function (e) {
if (e.keyCode === 13) {
gotName()
}
}, false)
form.appendChild(dom.createElement('br'))
const cancel = form.appendChild(buttons.cancelButton(dom))
cancel.addEventListener('click', function (_event) {
form.parentNode.removeChild(form)
resolve(null)
}, false)
const continueButton = form.appendChild(buttons.continueButton(dom))
continueButton.addEventListener('click', function (_event) {
gotName()
}, false)
namefield.focus()
}) // Promise
}
// ////////////////////////////////////////////////////////////////
// A little link icon
//
//
buttons.linkIcon = function (dom, subject, iconURI) {
var anchor = dom.createElement('a')
anchor.setAttribute('href', subject.uri)
if (subject.uri.startsWith('http')) {
// If diff web page
anchor.setAttribute('target', '_blank') // open in a new tab or window
} // as mailboxes and mail messages do not need new browser window
var img = anchor.appendChild(dom.createElement('img'))
img.setAttribute(
'src',
iconURI || UI.icons.originalIconBase + 'go-to-this.png'
)
img.setAttribute('style', 'margin: 0.3em;')
return anchor
}
// A TR to repreent a draggable person, etc in a list
//
// pred is unused param at the moment
//
buttons.personTR = function (dom, pred, obj, options) {
var tr = dom.createElement('tr')
options = options || {}
// tr.predObj = [pred.uri, obj.uri] moved to acl-control
var td1 = tr.appendChild(dom.createElement('td'))
var td2 = tr.appendChild(dom.createElement('td'))
var td3 = tr.appendChild(dom.createElement('td'))
// var image = td1.appendChild(dom.createElement('img'))
var image = faviconOrDefault(dom, obj)
td1.setAttribute('style', 'vertical-align: middle; width:2.5em; padding:0.5em; height: 2.5em;')
td2.setAttribute('style', 'vertical-align: middle; text-align:left;')
td3.setAttribute('style', 'vertical-align: middle; width:2em; padding:0.5em; height: 4em;')
td1.appendChild(image)
buttons.setName(td2, obj)
if (options.deleteFunction) {
buttons.deleteButtonWithCheck(dom, td3, options.noun || 'one', options.deleteFunction)
}
if (obj.uri) {
// blank nodes need not apply
if (options.link !== false) {
var anchor = td3.appendChild(buttons.linkIcon(dom, obj))
anchor.classList.add('HoverControlHide')
td3.appendChild(dom.createElement('br'))
}
if (options.draggable !== false) {
// default is on
image.setAttribute('draggable', 'false') // Stop the image being dragged instead - just the TR
dragAndDrop.makeDraggable(tr, obj)
}
}
tr.subject = obj
return tr
}
// Refresh a DOM tree recursively
buttons.refreshTree = function refreshTree (root) {
if (root.refresh) {
root.refresh()
return
}
for (var i = 0; i < root.children.length; i++) {
refreshTree(root.children[i])
}
}
// List of attachments accepting drop
buttons.attachmentList = function (dom, subject, div, options) {
options = options || {}
var doc = options.doc || subject.doc()
if (options.modify === undefined) options.modify = true
var modify = options.modify
var promptIcon = options.promptIcon || UI.icons.iconBase + 'noun_748003.svg' // target
// var promptIcon = options.promptIcon || (UI.icons.iconBase + 'noun_25830.svg') // paperclip
var predicate = options.predicate || UI.ns.wf('attachment')
var noun = options.noun || 'attachment'
var kb = UI.store
var attachmentOuter = div.appendChild(dom.createElement('table'))
attachmentOuter.setAttribute('style', 'margin-top: 1em; margin-bottom: 1em;')
var attachmentOne = attachmentOuter.appendChild(dom.createElement('tr'))
var attachmentLeft = attachmentOne.appendChild(dom.createElement('td'))
var attachmentRight = attachmentOne.appendChild(dom.createElement('td'))
var attachmentTable = attachmentRight.appendChild(dom.createElement('table'))
attachmentTable.appendChild(dom.createElement('tr')) // attachmentTableTop
var deleteAttachment = function (target) {
kb.updater.update($rdf.st(subject, predicate, target, doc), [], function (
uri,
ok,
errorBody,
_xhr
) {
if (ok) {
refresh()
} else {
complain('Error deleting one: ' + errorBody)
}
})
}
var createNewRow = function (target) {
var theTarget = target
var opt = { noun: noun }
if (modify) {
opt.deleteFunction = function () {
deleteAttachment(theTarget)
}
}
return buttons.personTR(dom, predicate, target, opt)
}
var refresh = (attachmentTable.refresh = function () {
var things = kb.each(subject, predicate)
things.sort()
utils.syncTableToArray(attachmentTable, things, createNewRow)
})
attachmentOuter.refresh = refresh // Participate in downstream changes
refresh()
var droppedURIHandler = function (uris) {
var ins = []
uris.map(function (u) {
var target = $rdf.sym(u) // Attachment needs text label to disinguish I think not icon.
console.log('Dropped on attachemnt ' + u) // icon was: UI.icons.iconBase + 'noun_25830.svg'
ins.push($rdf.st(subject, predicate, target, doc))
})
kb.updater.update([], ins, function (uri, ok, errorBody, _xhr) {
if (ok) {
refresh()
} else {
complain('Error adding one: ' + errorBody)
}
})
}
if (modify) {
var paperclip = attachmentLeft.appendChild(dom.createElement('img'))
paperclip.setAttribute('src', promptIcon)
paperclip.setAttribute('style', 'width; 2em; height: 2em; margin: 0.5em;')
paperclip.setAttribute('draggable', 'false')
dragAndDrop.makeDropTarget(attachmentLeft, droppedURIHandler)
}
return attachmentOuter
}
// /////////////////////////////////////////////////////////////////////////////
// Event Handler for links within solid apps.
//
// Note that native links have consraints in Firefox, they
// don't work with local files for instance (2011)
//
buttons.openHrefInOutlineMode = function (e) {
e.preventDefault()
e.stopPropagation()
var target = utils.getTarget(e)
var uri = target.getAttribute('href')
if (!uri) return console.log('openHrefInOutlineMode: No href found!\n')
const dom = window.document
if (dom.outlineManager) {
// @@ TODO Remove the use of document as a global object
dom.outlineManager.GotoSubject(UI.store.sym(uri), true, undefined, true, undefined)
} else if (window && window.panes && window.panes.getOutliner) {
// @@ TODO Remove the use of window as a global object
window.panes
.getOutliner()
.GotoSubject(UI.store.sym(uri), true, undefined, true, undefined)
} else {
console.log('ERROR: Can\'t access outline manager in this config')
}
// dom.outlineManager.GotoSubject(UI.store.sym(uri), true, undefined, true, undefined)
}
// We make a URI in the annotation store out of the URI of the thing to be annotated.
//
// @@ Todo: make it a personal preference.
//
buttons.defaultAnnotationStore = function (subject) {
if (subject.uri === undefined) return undefined
var s = subject.uri
if (s.slice(0, 7) !== 'http://') return undefined
s = s.slice(7) // Remove
var hash = s.indexOf('#')
if (hash >= 0) s = s.slice(0, hash)
// Strip trailing
else {
var slash = s.lastIndexOf('/')
if (slash < 0) return undefined
s = s.slice(0, slash)
}
return UI.store.sym('http://tabulator.org/wiki/annnotation/' + s)
}
buttons.allClassURIs = function () {
var set = {}
UI.store
.statementsMatching(undefined, UI.ns.rdf('type'), undefined)
.map(function (st) {
if (st.object.uri) set[st.object.uri] = true
})
UI.store
.statementsMatching(undefined, UI.ns.rdfs('subClassOf'), undefined)
.map(function (st) {
if (st.object.uri) set[st.object.uri] = true
if (st.subject.uri) set[st.subject.uri] = true
})
UI.store
.each(undefined, UI.ns.rdf('type'), UI.ns.rdfs('Class'))
.map(function (c) {
if (c.uri) set[c.uri] = true
})
return set
}
/**
* 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 dp = {}
var op = {}
var no = 0
var nd = 0
var nu = 0
var pi = kb.predicateIndex // One entry for each pred
for (var p in pi) {
var object = pi[p][0].object
if (object.termType === 'Literal') {
dp[p] = true
nd++
} else {
op[p] = true
no++
}
} // If nothing discovered, then could be either:
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)
if (!op[p] && !dp[p]) {
dp[p] = true
op[p] = true
nu++
}
}
possibleProperties.op = op
possibleProperties.dp = dp
UI.log.info(`propertyTriage: ${no} non-lit, ${nd} literal. ${nu} unknown.`)
return possibleProperties
}
/**
* General purpose widgets
*/
// A button for jumping
//
buttons.linkButton = function (dom, object) {
var b = dom.createElement('button')
b.setAttribute('type', 'button')
b.textContent = 'Goto ' + utils.label(object)
b.addEventListener('click', function (_event) {
// b.parentNode.removeChild(b)
dom.outlineManager.GotoSubject(object, true, undefined, true, undefined)
}, true)
return b
}
buttons.removeButton = function (dom, element) {
var b = dom.createElement('button')
b.setAttribute('type', 'button')
b.textContent = '✕' // MULTIPLICATION X
b.addEventListener('click', function (_event) {
element.parentNode.removeChild(element)
}, true)
return b
}
// Description text area
//
// Make a box to demand a description or display existing one
//
// @param dom - the document DOM for the user interface
// @param kb - the graph which is the knowledge base we are working with
// @param subject - a term, the subject of the statement(s) being edited.
// @param predicate - a term, the predicate of the statement(s) being edited
// @param store - The web document being edited
// @param callbackFunction - takes (boolean ok, string errorBody)
// /////////////////////////////////////// Random I/O widgets /////////////
// //// Column Header Buttons
//
// These are for selecting different modes, sources,styles, etc.
//
/*
buttons.headerButtons = function (dom, kb, name, words) {
var box = dom.createElement('table')
var i, word, s = '<tr>'
box.setAttribute('style', 'width: 90%; height: 1.5em')
for (i=0; i<words.length; i++) {
s += '<td><input type="radio" name="' + name + '" id="' + words[i] + '" value='
}
box.innerHTML = s + '</tr>'
}
*/
// ////////////////////////////////////////////////////////////
//
// selectorPanel
//
// A vertical panel for selecting connections to left or right.
//
// @param inverse means this is the object rather than the subject
//
buttons.selectorPanel = function (
dom,
kb,
type,
predicate,
inverse,
possible,
options,
callbackFunction,
linkCallback
) {
return buttons.selectorPanelRefresh(
dom.createElement('div'),
dom,
kb,
type,
predicate,
inverse,
possible,
options,
callbackFunction,
linkCallback
)
}
buttons.selectorPanelRefresh = function (
list,
dom,
kb,
type,
predicate,
inverse,
possible,
options,
callbackFunction,
linkCallback
) {
var style0 =
'border: 0.1em solid #ddd; border-bottom: none; width: 95%; height: 2em; padding: 0.5em;'
var selected = null
list.innerHTML = ''
var refreshItem = function (box, x) {
// Scope to hold item and x
var item, image
var setStyle = function () {
var already = inverse
? kb.each(undefined, predicate, x)
: kb.each(x, predicate)
iconDiv.setAttribute('class', already.length === 0 ? 'hideTillHover' : '') // See tabbedtab.css
image.setAttribute(
'src',
options.connectIcon || UI.icons.iconBase + 'noun_25830.svg'
)
image.setAttribute('title', already.length ? already.length : 'attach')
}
var f = buttons.index.twoLine.widgetForClass(type)
item = f(dom, x)
item.setAttribute('style', style0)
var nav = dom.createElement('div')
nav.setAttribute('class', 'hideTillHover') // See tabbedtab.css
nav.setAttribute('style', 'float:right; width:10%')
var a = dom.createElement('a')
a.setAttribute('href', x.uri)
a.setAttribute('style', 'float:right')
nav.appendChild(a).textContent = '>'
box.appendChild(nav)
var iconDiv = dom.createElement('div')
iconDiv.setAttribute(
'style',
(inverse ? 'float:left;' : 'float:right;') + ' width:30px;'
)
image = dom.createElement('img')
setStyle()
iconDiv.appendChild(image)
box.appendChild(iconDiv)
item.addEventListener('click', function (event) {
if (selected === item) {
// deselect
item.setAttribute('style', style0)
selected = null
} else {
if (selected) selected.setAttribute('style', style0)
item.setAttribute(
'style',
style0 + 'background-color: #ccc; color:black;'
)
selected = item
}
callbackFunction(x, event, selected === item)
setStyle()
}, false)
image.addEventListener('click', function (event) {
linkCallback(x, event, inverse, setStyle)
}, false)
box.appendChild(item)
return box
}
for (var i = 0; i < possible.length; i++) {
var box = dom.createElement('div')
list.appendChild(box)
refreshItem(box, possible[i])
}
return list
}
// ###########################################################################
//
// Small compact views of things
//
buttons.index = {}
buttons.index.line = {} // Approx 80em
buttons.index.twoLine = {} // Approx 40em * 2.4em
// ///////////////////////////////////////////////////////////////////////////
// We need these for anything which is a subject of an attachment.
//
// These should be moved to type-dependeent UI code. Related panes maybe
buttons.index.twoLine[''] = function (dom, x) {
// Default
var box = dom.createElement('div')
box.textContent = utils.label(x)
return box
}
buttons.index.twoLine.widgetForClass = function (c) {
var widget = buttons.index.twoLine[c.uri]
var kb = UI.store
if (widget) return widget
var sup = kb.findSuperClassesNT(c)
for (var cl in sup) {
widget = buttons.index.twoLine[kb.fromNT(cl).uri]
if (widget) return widget
}
return buttons.index.twoLine['']
}
buttons.index.twoLine['http://www.w3.org/2000/10/swap/pim/qif#Transaction'] = function (dom, x) {
var failed = ''
var enc = function (p) {
var y = UI.store.any(x, UI.ns.qu(p))
if (!y) failed += '@@ No value for ' + p + '! '
return y ? utils.escapeForXML(y.value) : '?' // @@@@
}
var box = dom.createElement('table')
box.innerHTML = `
<tr>
<td colspan="2">${enc('payee')}</td>
</tr>
<tr>
<td>${enc('date').slice(0, 10)}</td>
<td style="text-align: right;">${enc('amount')}</td>
</tr>`
if (failed) {
box.innerHTML = `
<tr>
<td><a href="${utils.escapeForXML(x.uri)}">${utils.escapeForXML(failed)}</a></td>
</tr>`
}
return box
}
buttons.index.twoLine['http://www.w3.org/ns/pim/trip#Trip'] = function (
dom,
x
) {
var enc = function (p) {
var y = UI.store.any(x, p)
return y ? utils.escapeForXML(y.value) : '?'
}
var box = dom.createElement('table')
box.innerHTML = `
<tr>
<td colspan="2">${enc(UI.ns.dc('title'))}</td>
</tr>
<tr style="color: #777">
<td>${enc(UI.ns.cal('dtstart'))}</td>
<td>${enc(UI.ns.cal('dtend'))}</td>
</tr>`
return box
}
// Stick a stylesheet link the document if not already there
buttons.addStyleSheet = function (dom, href) {
var links = dom.querySelectorAll('link')
for (var i = 0; i < links.length; i++) {
if (
(links[i].getAttribute('rel') || '') === 'stylesheet' &&
(links[i].getAttribute('href') || '') === href
) {
return
}
}
var link = dom.createElement('link')
link.setAttribute('rel', 'stylesheet')
link.setAttribute('type', 'text/css')
link.setAttribute('href', href)
dom.getElementsByTagName('head')[0].appendChild(link)
}
// Figure (or guess) whether this is an image, etc
//
buttons.isAudio = function (file) {
return buttons.isImage(file, 'audio')
}
buttons.isVideo = function (file) {
return buttons.isImage(file, 'video')
}
buttons.isImage = function (file, kind) {
var dcCLasses = {
audio: 'http://purl.org/dc/dcmitype/Sound',
image: 'http://purl.org/dc/dcmitype/Image',
video: 'http://purl.org/dc/dcmitype/MovingImage'
}
var what = kind || 'image'
var typeURIs = UI.store.findTypeURIs(file)
var prefix = $rdf.Util.mediaTypeClass(what + '/*').uri.split('*')[0]
for (var t in typeURIs) {
if (t.startsWith(prefix)) return true
}
if (dcCLasses[what] in typeURIs) return true
return false
}
/**
* File upload button
* @param dom The DOM aka document
* @param display:none - Same handler function as drop, takes array of file objects
* @returns {Element} - a div with a button and a inout in it
* The input is hidden, as it is uglky - the user clicks on the nice icons and fires the input.
*/
// See https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications
buttons.fileUploadButtonDiv = function fileUploadButtonDiv (
dom,
droppedFileHandler
) {
const div = dom.createElement('div')
const input = div.appendChild(dom.createElement('input'))
input.setAttribute('type', 'file')
input.setAttribute('multiple', 'true')
input.addEventListener(
'change',
event => {
console.log('File drop event: ', event)
if (event.files) {
droppedFileHandler(event.files)
} else if (event.target && event.target.files) {
droppedFileHandler(event.target.files)
} else {
alert('Sorry no files .. internal error?')
}
},
false
)
input.style = 'display:none'
const button = div.appendChild(
buttons.button(
dom,
UI.icons.iconBase + 'noun_Upload_76574_000000.svg',
'Upload files',
_event => {
input.click()
}
)
)
dragAndDrop.makeDropTarget(button, null, droppedFileHandler) // Can also just drop on button
return div
}
module.exports = buttons