src/utils.js
// Solid-UI general Utilities
// ==========================
//
// This must load AFTER the rdflib.js and log-ext.js (or log.js).
//
module.exports = {
addLoadEvent, // not used anywhere
AJARImage,
ancestor,
beep,
clearVariableNames,
emptyNode,
escapeForXML,
findPos,
genUuid,
getAbout,
getEyeFocus,
getTarget,
getTerm,
hashColor,
include,
label,
labelForXML,
labelWithOntology,
newVariableName,
ontologyLabel,
predicateLabelForXML,
predParentOf,
RDFComparePredicateObject,
RDFComparePredicateSubject,
shortName,
stackString,
syncTableToArray,
syncTableToArrayReOrdered
}
var UI = {
log: require('./log'),
ns: require('./ns'),
rdf: require('rdflib'),
store: require('./store')
}
var nextVariable = 0
function newVariableName () {
return 'v' + nextVariable++
}
function clearVariableNames () {
nextVariable = 0
}
// http://stackoverflow.com/questions/879152/how-do-i-make-javascript-beep
// http://www.tsheffler.com/blog/2013/05/14/audiocontext-noteonnoteoff-and-time-units/
var audioContext
if (typeof AudioContext !== 'undefined') {
audioContext = AudioContext
} else if (typeof window !== 'undefined') {
audioContext = window.AudioContext || window.webkitAudioContext
}
function beep () {
if (!audioContext) {
return
} // Safari 2015
const ContextClass = audioContext
const ctx = new ContextClass()
return function (duration, frequency, type, finishedCallback) {
duration = +(duration || 0.3)
// Only 0-4 are valid types.
type = type || 'sine' // sine, square, sawtooth, triangle
if (typeof finishedCallback !== 'function') {
finishedCallback = function () {}
}
var osc = ctx.createOscillator()
osc.type = type
osc.frequency.value = frequency || 256
osc.connect(ctx.destination)
osc.start(0)
osc.stop(duration)
}
}
// Make pseudorandom color from a uri
// NOT USED ANYWHERE
function hashColor (who) {
who = who.uri || who
var hash = function (x) {
return x.split('').reduce(function (a, b) {
a = (a << 5) - a + b.charCodeAt(0)
return a & a
}, 0)
}
return '#' + ((hash(who) & 0xffffff) | 0xc0c0c0).toString(16) // c0c0c0 or 808080 forces pale
}
function genUuid () {
// http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0
var v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
/** Sync a DOM table with an array of things
*
* @param {DomElement} table - will have a tr for each thing
* @param {Array<NamedNode>} things - ORDERED array of NamedNode objects
* @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
let row
let i
for (i = 0; i < table.children.length; i++) {
row = table.children[i]
row.trashMe = true
}
for (let g = 0; g < things.length; g++) {
var thing = things[g]
foundOne = false
for (i = 0; i < table.children.length; i++) {
row = table.children[i]
if (row.subject && row.subject.sameTerm(thing)) {
row.trashMe = false
foundOne = true
break
}
}
if (!foundOne) {
const newRow = createNewRow(thing)
// Insert new row in position g in the table to match array
if (g >= table.children.length) {
table.appendChild(newRow)
} else {
const ele = table.children[g]
table.insertBefore(newRow, ele)
}
newRow.subject = thing
} // if not foundOne
} // loop g
for (i = 0; i < table.children.length; i++) {
row = table.children[i]
if (row.trashMe) {
table.removeChild(row)
}
}
} // syncTableToArray
/** Sync a DOM table with an array of things
*
* @param {DomElement} table - will have a tr for each thing
* @param {Array<NamedNode>} 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
*/
function stackString (e) {
let str = '' + e + '\n'
let i
if (!e.stack) {
return str + 'No stack available.\n'
}
const lines = e.stack.toString().split('\n')
const toPrint = []
for (i = 0; i < lines.length; i++) {
let line = lines[i]
if (line.indexOf('ecmaunit.js') > -1) {
// remove useless bit of traceback
break
}
if (line.charAt(0) === '(') {
line = 'function' + line
}
const chunks = line.split('@')
toPrint.push(chunks)
}
// toPrint.reverse(); No - I prefer the latest at the top by the error message -tbl
for (i = 0; i < toPrint.length; i++) {
str += ' ' + toPrint[i][1] + '\n ' + toPrint[i][0]
}
return str
}
function emptyNode (node) {
const nodes = node.childNodes
const len = nodes.length
for (let i = len - 1; i >= 0; i--) node.removeChild(nodes[i])
return node
}
function getTarget (e) {
var target
e = e || window.event
if (e.target) target = e.target
else if (e.srcElement) target = e.srcElement
if (target.nodeType === 3) {
// defeat Safari bug [sic]
target = target.parentNode
}
// UI.log.debug("Click on: " + target.tagName)
return target
}
function ancestor (target, tagName) {
var level
for (level = target; level; level = level.parentNode) {
// UI.log.debug("looking for "+tagName+" Level: "+level+" "+level.tagName)
try {
if (level.tagName === tagName) return level
} catch (e) {
// can hit "TypeError: can't access dead object" in ffox
return undefined
}
}
return undefined
}
function getAbout (kb, target) {
var level, aa
for (
level = target;
level && level.nodeType === 1;
level = level.parentNode
) {
// UI.log.debug("Level "+level + ' '+level.nodeType + ': '+level.tagName)
aa = level.getAttribute('about')
if (aa) {
// UI.log.debug("kb.fromNT(aa) = " + kb.fromNT(aa))
return kb.fromNT(aa)
// } else {
// if (level.tagName=='TR') return undefined//this is to prevent literals passing through
}
}
UI.log.debug('getAbout: No about found')
return undefined
}
function getTerm (target) {
var statementTr = target.parentNode
var st = statementTr ? statementTr.AJAR_statement : undefined
var className = st ? target.className : '' // if no st then it's necessary to use getAbout
switch (className) {
case 'pred':
case 'pred selected':
return st.predicate
case 'obj':
case 'obj selected':
if (!statementTr.AJAR_inverse) {
return st.object
} else {
return st.subject
}
case '':
case 'selected': // header TD
return getAbout(UI.store, target) // kb to be changed
case 'undetermined selected':
return target.nextSibling
? st.predicate
: !statementTr.AJAR_inverse
? st.object
: st.subject
}
}
function include (document, linkstr) {
var lnk = document.createElement('script')
lnk.setAttribute('type', 'text/javascript')
lnk.setAttribute('src', linkstr)
// TODO:This needs to be fixed or no longer used.
// document.getElementsByTagName('head')[0].appendChild(lnk)
return lnk
}
function addLoadEvent (func) {
var oldonload = window.onload
if (typeof window.onload !== 'function') {
window.onload = func
} else {
window.onload = function () {
oldonload()
func()
}
}
} // addLoadEvent
// Find the position of an object relative to the window
function findPos (obj) {
// C&P from http://www.quirksmode.org/js/findpos.html
var myDocument = obj.ownerDocument
var DocBox = myDocument.documentElement.getBoundingClientRect()
var box = obj.getBoundingClientRect()
return [box.left - DocBox.left, box.top - DocBox.top]
}
function getEyeFocus (element, instantly, isBottom, myWindow) {
if (!myWindow) myWindow = window
var elementPosY = findPos(element)[1]
var totalScroll = elementPosY - 52 - myWindow.scrollY // magic number 52 for web-based version
if (instantly) {
if (isBottom) {
myWindow.scrollBy(
0,
elementPosY +
element.clientHeight -
(myWindow.scrollY + myWindow.innerHeight)
)
return
}
myWindow.scrollBy(0, totalScroll)
return
}
var id = myWindow.setInterval(scrollAmount, 50)
var times = 0
function scrollAmount () {
myWindow.scrollBy(0, totalScroll / 10)
times++
if (times === 10) {
myWindow.clearInterval(id)
}
}
}
function AJARImage (src, alt, tt, doc) {
if (!doc) {
doc = document
}
var image = doc.createElement('img')
image.setAttribute('src', src)
image.addEventListener('copy', function (e) {
e.clipboardData.setData('text/plain', '')
e.clipboardData.setData('text/html', '')
e.preventDefault() // We want no title data to be written to the clipboard
})
// if (typeof alt != 'undefined') // Messes up cut-and-paste of text
// image.setAttribute('alt', alt)
if (typeof tt !== 'undefined') {
image.setAttribute('title', tt)
}
return image
}
// Make short name for ontology
function shortName (uri) {
let p = uri
if ('#/'.indexOf(p[p.length - 1]) >= 0) p = p.slice(0, -1)
const namespaces = []
for (const ns in this.prefixes) {
namespaces[this.prefixes[ns]] = ns // reverse index
}
let pok
const canUse = function canUse (pp) {
// if (!__Serializer.prototype.validPrefix.test(pp)) return false; // bad format
if (pp === 'ns') return false // boring
// if (pp in this.namespaces) return false; // already used
// this.prefixes[uri] = pp;
// this.namespaces[pp] = uri;
pok = pp
return true
}
let i
const hash = p.lastIndexOf('#')
if (hash >= 0) p = p.slice(hash - 1) // lop off localid
for (;;) {
const slash = p.lastIndexOf('/')
if (slash >= 0) p = p.slice(slash + 1)
i = 0
while (i < p.length) {
if (this.prefixchars.indexOf(p[i])) i++
else break
}
p = p.slice(0, i)
if (p.length < 6 && canUse(p)) return pok // exact i sbest
if (canUse(p.slice(0, 3))) return pok
if (canUse(p.slice(0, 2))) return pok
if (canUse(p.slice(0, 4))) return pok
if (canUse(p.slice(0, 1))) return pok
if (canUse(p.slice(0, 5))) return pok
for (i = 0; ; i++) if (canUse(p.slice(0, 3) + i)) return pok
}
}
// Short name for an ontology
function ontologyLabel (term) {
if (term.uri === undefined) return '??'
var s = term.uri
var namespaces = []
var i = s.lastIndexOf('#')
var part
if (i >= 0) {
s = s.slice(0, i + 1)
} else {
i = s.lastIndexOf('/')
if (i >= 0) {
s = s.slice(0, i + 1)
} else {
return term.uri + '?!' // strange should have # or /
}
}
for (const ns in UI.ns) {
namespaces[UI.ns[ns]] = ns // reverse index
}
try {
return namespaces[s]
} catch (e) {}
s = s.slice(0, -1) // Chop off delimiter ... now have just
while (s) {
i = s.lastIndexOf('/')
if (i >= 0) {
part = s.slice(i + 1)
s = s.slice(0, i)
if (part !== 'ns' && '0123456789'.indexOf(part[0]) < 0) {
return part
}
} else {
return term.uri + '!?' // strange should have a nice part
}
}
}
function labelWithOntology (x, initialCap) {
const t = UI.store.findTypeURIs(x)
if (t[UI.ns.rdf('Predicate').uri] || t[UI.ns.rdfs('Class').uri]) {
return label(x, initialCap) + ' (' + ontologyLabel(x) + ')'
}
return label(x, initialCap)
}
// This ubiquitous function returns the best label for a thing
//
// The hacks in this code make a major difference to the usability
//
// @returns string
//
function label (x, initialCap) {
// x is an object
function doCap (s) {
// s = s.toString()
if (initialCap) return s.slice(0, 1).toUpperCase() + s.slice(1)
return s
}
function cleanUp (s1) {
var s2 = ''
if (s1.slice(-1) === '/') s1 = s1.slice(0, -1) // chop trailing slash
for (var i = 0; i < s1.length; i++) {
if (s1[i] === '_' || s1[i] === '-') {
s2 += ' '
continue
}
s2 += s1[i]
if (
i + 1 < s1.length &&
s1[i].toUpperCase() !== s1[i] &&
s1[i + 1].toLowerCase() !== s1[i + 1]
) {
s2 += ' '
}
}
if (s2.slice(0, 4) === 'has ') s2 = s2.slice(4)
return doCap(s2)
}
// 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)
if (lab) {
return doCap(lab.value)
}
}
*/
// Hard coded known label predicates
// @@ TBD: Add subproperties of rdfs:label
var kb = UI.store
var lab1 =
kb.any(x, UI.ns.link('message')) ||
kb.any(x, UI.ns.vcard('fn')) ||
kb.any(x, UI.ns.foaf('name')) ||
kb.any(x, UI.ns.dct('title')) ||
kb.any(x, UI.ns.dc('title')) ||
kb.any(x, UI.ns.rss('title')) ||
kb.any(x, UI.ns.contact('fullName')) ||
kb.any(x, kb.sym('http://www.w3.org/2001/04/roadmap/org#name')) ||
kb.any(x, UI.ns.cal('summary')) ||
kb.any(x, UI.ns.foaf('nick')) ||
kb.any(x, UI.ns.rdfs('label'))
if (lab1) {
return doCap(lab1.value)
}
// Default to label just generated from the URI
if (x.termType === 'BlankNode') {
return '...'
}
if (x.termType === 'Collection') {
return '(' + x.elements.length + ')'
}
var s = x.uri
if (typeof s === 'undefined') return x.toString() // can't be a symbol
// s = decodeURI(s) // This can crash is random valid @ signs are presentation
// The idea was to clean up eg URIs encoded in query strings
// Also encoded character in what was filenames like @ [] {}
try {
s = s
.split('/')
.map(decodeURIComponent)
.join('/') // If it is properly encoded
} catch (e) {
// try individual decoding of ASCII code points
for (var i = s.length - 3; i > 0; i--) {
const hex = '0123456789abcefABCDEF' // The while upacks multiple layers of encoding
while (
s[i] === '%' &&
hex.indexOf(s[i + 1]) >= 0 &&
hex.indexOf(s[i + 2]) >= 0
) {
s =
s.slice(0, i) +
String.fromCharCode(parseInt(s.slice(i + 1, i + 3), 16)) +
s.slice(i + 3)
}
}
}
if (s.slice(-5) === '#this') s = s.slice(0, -5)
else if (s.slice(-3) === '#me') s = s.slice(0, -3)
var hash = s.indexOf('#')
if (hash >= 0) return cleanUp(s.slice(hash + 1))
if (s.slice(-9) === '/foaf.rdf') s = s.slice(0, -9)
else if (s.slice(-5) === '/foaf') s = s.slice(0, -5)
// Eh? Why not do this? e.g. dc:title needs it only trim URIs, not rdfs:labels
var slash = s.lastIndexOf('/', s.length - 2) // (len-2) excludes trailing slash
if (slash >= 0 && slash < x.uri.length) return cleanUp(s.slice(slash + 1))
return doCap(decodeURIComponent(x.uri))
}
function escapeForXML (str) {
return str.replace(/&/g, '&').replace(/</g, '<')
}
// As above but escaped for XML and chopped of contains a slash
function labelForXML (x) {
return escapeForXML(label(x))
}
// As above but for predicate, possibly inverse
function predicateLabelForXML (p, inverse) {
var lab
if (inverse) {
// If we know an inverse predicate, use its label
var ip = UI.store.any(p, UI.ns.owl('inverseOf'))
if (!ip) ip = UI.store.any(undefined, UI.ns.owl('inverseOf'), p)
if (ip) return labelForXML(ip)
}
lab = labelForXML(p)
if (inverse) {
if (lab === 'type') return '...' // Not "is type of"
return 'is ' + lab + ' of'
}
return lab
}
// Not a method. For use in sorts
function RDFComparePredicateObject (self, other) {
var x = self.predicate.compareTerm(other.predicate)
if (x !== 0) return x
return self.object.compareTerm(other.object)
}
function RDFComparePredicateSubject (self, other) {
var x = self.predicate.compareTerm(other.predicate)
if (x !== 0) return x
return self.subject.compareTerm(other.subject)
}
// ends
function predParentOf (node) {
var n = node
while (true) {
if (n.getAttribute('predTR')) {
return n
} else if (n.previousSibling && n.previousSibling.nodeName === 'TR') {
n = n.previousSibling
} else {
UI.log.error('Could not find predParent')
return node
}
}
}
// makeQueryRow moved to outline mode