// 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, predicateLabel, 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} 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} 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) } // Hard coded known label predicates // @@ TBD: Add subproperties of rdfs:label var kb = UI.store var lab1 = kb.any(x, UI.ns.ui('label')) || // Prioritize ui:label 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(/