// 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} 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) } // 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(/