solid-panes
Version:
Solid-compatible Panes: applets and views for the mashlib and databrowser
1,500 lines (1,383 loc) • 81.3 kB
JavaScript
/* -*- coding: utf-8-dos -*-
Outline Mode Manager
*/
var panes = require('pane-registry')
const $rdf = require('rdflib')
var YAHOO = require('./dragDrop.js')
var outlineIcons = require('./outlineIcons.js')
var UserInput = require('./userInput.js')
var UI = require('solid-ui')
var queryByExample = require('./queryByExample.js')
/* global alert XPathResult sourceWidget */
// XPathResult?
// const iconHeight = '24px'
module.exports = function (context) {
const dom = context.dom
this.document = context.dom
this.outlineIcons = outlineIcons
this.labeller = this.labeller || {}
this.labeller.LanguagePreference = '' // for now
var outline = this // Kenny: do we need this?
var thisOutline = this
var selection = []
this.selection = selection
this.ancestor = UI.utils.ancestor // make available as outline.ancestor in callbacks
this.sparql = UI.rdf.UpdateManager
this.kb = UI.store
var kb = UI.store
var sf = UI.store.fetcher
dom.outline = this
this.qs = new queryByExample.QuerySource() // Track queries in queryByExample
// var selection = [] // Array of statements which have been selected
// this.focusTd // the <td> that is being observed
this.UserInput = new UserInput(this)
this.clipboardAddress = 'tabulator:clipboard' // Weird
this.UserInput.clipboardInit(this.clipboardAddress)
var outlineElement = this.outlineElement
this.init = function () {
var table = getOutlineContainer()
table.outline = this
}
/** benchmark a function **/
benchmark.lastkbsize = 0
function benchmark (f) {
var args = []
for (var i = arguments.length - 1; i > 0; i--) args[i - 1] = arguments[i]
// UI.log.debug('BENCHMARK: args=' + args.join());
var begin = new Date().getTime()
var returnValue = f.apply(f, args)
var end = new Date().getTime()
UI.log.info(
'BENCHMARK: kb delta: ' +
(kb.statements.length - benchmark.lastkbsize) +
', time elapsed for ' +
f +
' was ' +
(end - begin) +
'ms'
)
benchmark.lastkbsize = kb.statements.length
return returnValue
} // benchmark
// / ////////////////////// Representing data
// Represent an object in summary form as a table cell
function appendRemoveIcon (node, subject, removeNode) {
var image = UI.utils.AJARImage(
outlineIcons.src.icon_remove_node,
'remove',
undefined,
dom
)
image.addEventListener('click', removeNodeIconMouseDownListener)
// image.setAttribute('align', 'right') Causes icon to be moved down
image.node = removeNode
image.setAttribute('about', subject.toNT())
image.style.marginLeft = '5px'
image.style.marginRight = '10px'
// image.style.border='solid #777 1px';
node.appendChild(image)
return image
}
this.appendAccessIcons = function (kb, node, obj) {
if (obj.termType !== 'NamedNode') return
var uris = kb.uris(obj)
uris.sort()
var last = null
for (var i = 0; i < uris.length; i++) {
if (uris[i] === last) continue
last = uris[i]
thisOutline.appendAccessIcon(node, last)
}
}
this.appendAccessIcon = function (node, uri) {
if (!uri) return ''
var docuri = UI.rdf.uri.docpart(uri)
if (docuri.slice(0, 5) !== 'http:') return ''
var state = sf.getState(docuri)
var icon, alt, listener
switch (state) {
case 'unrequested':
icon = outlineIcons.src.icon_unrequested
alt = 'fetch'
listener = unrequestedIconMouseDownListener
break
case 'requested':
icon = outlineIcons.src.icon_requested
alt = 'fetching'
listener = failedIconMouseDownListener // new: can retry yello blob
break
case 'fetched':
icon = outlineIcons.src.icon_fetched
listener = fetchedIconMouseDownListener
alt = 'loaded'
break
case 'failed':
icon = outlineIcons.src.icon_failed
alt = 'failed'
listener = failedIconMouseDownListener
break
case 'unpermitted':
icon = outlineIcons.src.icon_failed
listener = failedIconMouseDownListener
alt = 'no perm'
break
case 'unfetchable':
icon = outlineIcons.src.icon_failed
listener = failedIconMouseDownListener
alt = 'cannot fetch'
break
default:
UI.log.error('?? state = ' + state)
break
} // switch
var img = UI.utils.AJARImage(
icon,
alt,
outlineIcons.tooltips[icon].replace(/[Tt]his resource/, docuri),
dom
)
img.setAttribute('uri', uri)
img.addEventListener('click', listener) // @@ seemed to be missing 2017-08
addButtonCallbacks(img, docuri)
node.appendChild(img)
return img
} // appendAccessIcon
// Six different Creative Commons Licenses:
// 1. http://creativecommons.org/licenses/by-nc-nd/3.0/
// 2. http://creativecommons.org/licenses/by-nc-sa/3.0/
// 3. http://creativecommons.org/licenses/by-nc/3.0/
// 4. http://creativecommons.org/licenses/by-nd/3.0/
// 5. http://creativecommons.org/licenses/by-sa/3.0/
// 6. http://creativecommons.org/licenses/by/3.0/
/** make the td for an object (grammatical object)
* @param obj - an RDF term
* @param view - a VIEW function (rather than a bool asImage)
**/
this.outlineObjectTD = function outlineObjectTD (
obj,
view,
deleteNode,
statement
) {
var td = dom.createElement('td')
td.setAttribute(
'style',
'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
)
td.setAttribute('notSelectable', 'false')
var theClass = 'obj'
// check the IPR on the data. Ok if there is any checked license which is one the document has.
if (statement && statement.why) {
if (UI.licenceOptions && UI.licenceOptions.checkLicence()) {
theClass += ' licOkay' // flag as light green etc .licOkay {background-color: #dfd}
}
}
// set about and put 'expand' icon
if (
obj.termType === 'NamedNode' ||
obj.termType === 'BlankNode' ||
(obj.termType === 'Literal' &&
obj.value.slice &&
(obj.value.slice(0, 6) === 'ftp://' ||
obj.value.slice(0, 8) === 'https://' ||
obj.value.slice(0, 7) === 'http://'))
) {
td.setAttribute('about', obj.toNT())
td.appendChild(
UI.utils.AJARImage(
UI.icons.originalIconBase + 'tbl-expand-trans.png',
'expand',
undefined,
dom
)
).addEventListener('click', expandMouseDownListener)
}
td.setAttribute('class', theClass) // this is how you find an object
// @@ TAKE CSS OUT OF STYLE SHEET
if (kb.whether(obj, UI.ns.rdf('type'), UI.ns.link('Request'))) {
td.className = 'undetermined'
} // @@? why-timbl
if (!view) {
// view should be a function pointer
view = viewAsBoringDefault
}
td.appendChild(view(obj))
if (deleteNode) {
appendRemoveIcon(td, obj, deleteNode)
}
try {
// new YAHOO.util.DDExternalProxy(td)
} catch (e) {
UI.log.error('YAHOO Drag and drop not supported:\n' + e)
}
// set DOM methods
td.tabulatorSelect = function () {
setSelected(this, true)
}
td.tabulatorDeselect = function () {
setSelected(this, false)
}
// td.appendChild( iconBox.construct(document.createTextNode('bla')) );
// Create an inquiry icon if there is proof about this triple
if (statement) {
var oneStatementFormula = new UI.rdf.IndexedFormula()
oneStatementFormula.statements.push(statement) // st.asFormula()
// The following works because Formula.hashString works fine for
// one statement formula
var reasons = kb.each(
oneStatementFormula,
kb.sym('http://dig.csail.mit.edu/TAMI/2007/amord/tms#justification')
)
if (reasons.length) {
var inquirySpan = dom.createElement('span')
if (reasons.length > 1) {
inquirySpan.innerHTML = ' × ' + reasons.length
}
inquirySpan.setAttribute('class', 'inquiry')
inquirySpan.insertBefore(
UI.utils.AJARImage(
outlineIcons.src.icon_display_reasons,
'explain',
undefined,
dom
),
inquirySpan.firstChild
)
td.appendChild(inquirySpan)
}
}
td.addEventListener('click', selectableTDClickListener)
return td
} // outlineObjectTD
this.outlinePredicateTD = function outlinePredicateTD (
predicate,
newTr,
inverse,
internal
) {
var predicateTD = dom.createElement('TD')
predicateTD.setAttribute('about', predicate.toNT())
predicateTD.setAttribute('class', internal ? 'pred internal' : 'pred')
let lab
switch (predicate.termType) {
case 'BlankNode': // TBD
predicateTD.className = 'undetermined'
break
case 'NamedNode':
lab = UI.utils.predicateLabelForXML(predicate, inverse)
break
case 'Collection': // some choices of predicate
lab = UI.utils.predicateLabelForXML(predicate.elements[0], inverse)
}
lab = lab.slice(0, 1).toUpperCase() + lab.slice(1)
// if (kb.statementsMatching(predicate,rdf('type'), UI.ns.link('Request')).length) predicateTD.className='undetermined';
var labelTD = dom.createElement('TD')
labelTD.setAttribute(
'style',
'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
)
labelTD.setAttribute('notSelectable', 'true')
labelTD.appendChild(dom.createTextNode(lab))
predicateTD.appendChild(labelTD)
labelTD.style.width = '100%'
predicateTD.appendChild(termWidget.construct(dom)) // termWidget is global???
for (var w in outlineIcons.termWidgets) {
if (!newTr || !newTr.AJAR_statement) break // case for TBD as predicate
// alert(Icon.termWidgets[w]+' '+Icon.termWidgets[w].filter)
if (
outlineIcons.termWidgets[w].filter &&
outlineIcons.termWidgets[w].filter(
newTr.AJAR_statement,
'pred',
inverse
)
) {
termWidget.addIcon(predicateTD, outlineIcons.termWidgets[w])
}
}
try {
// new YAHOO.util.DDExternalProxy(predicateTD)
} catch (e) {
UI.log.error('drag and drop not supported')
}
// set DOM methods
predicateTD.tabulatorSelect = function () {
setSelected(this, true)
}
predicateTD.tabulatorDeselect = function () {
setSelected(this, false)
}
predicateTD.addEventListener('click', selectableTDClickListener)
return predicateTD
} // outlinePredicateTD
/**
* Render Tabbed set of home app panes
*
* @param {Object} [options] A set of options you can provide
* @param {string} [options.selectedTab] To open a specific dashboard pane
* @param {Function} [options.onClose] If given, will present an X for the dashboard, and call this method when clicked
* @returns Promise<{Element}> - the div that holds the dashboard
*/
async function globalAppTabs (options = {}) {
console.log('globalAppTabs @@')
const div = dom.createElement('div')
const me = UI.authn.currentUser()
if (!me) {
alert('Must be logged in for this')
throw new Error('Not logged in')
}
const items = await getDashboardItems()
function renderTab (div, item) {
div.dataset.globalPaneName = item.tabName || item.paneName
div.textContent = item.label
}
function renderMain (containerDiv, item) {
// Items are pane names
const pane = panes.byName(item.paneName) // 20190701
containerDiv.innerHTML = ''
const table = containerDiv.appendChild(dom.createElement('table'))
const me = UI.authn.currentUser()
thisOutline.GotoSubject(
item.subject || me,
true,
pane,
false,
undefined,
table
)
}
div.appendChild(
UI.tabs.tabWidget({
dom,
subject: me,
items,
renderMain,
renderTab,
ordered: true,
orientation: 0,
backgroundColor: '#eeeeee', // black?
selectedTab: options.selectedTab,
onClose: options.onClose
})
)
return div
}
this.getDashboard = globalAppTabs
async function getDashboardItems () {
const me = UI.authn.currentUser()
const div = dom.createElement('div')
const [books, pods] = await Promise.all([getAddressBooks(), getPods()])
return [
{
paneName: 'home',
label: 'Your stuff',
icon: UI.icons.iconBase + 'noun_547570.svg'
},
{
paneName: 'basicPreferences',
label: 'Preferences',
icon: UI.icons.iconBase + 'noun_Sliders_341315_00000.svg'
},
{
paneName: 'editProfile',
label: 'Edit your profile',
icon: UI.icons.iconBase + 'noun_492246.svg'
}
]
.concat(books)
.concat(pods)
async function getPods () {
try {
// need to make sure that profile is loaded
await kb.fetcher.load(me.doc())
} catch (err) {
console.error('Unable to load profile', err)
return []
}
const pods = kb.each(me, ns.space('storage'), null, me.doc())
return pods.map((pod, index) => {
const label =
pods.length > 1 ? pod.uri.split('//')[1].slice(0, -1) : 'Your storage'
return {
paneName: 'folder',
tabName: `folder-${index}`,
label,
subject: pod,
icon: UI.icons.iconBase + 'noun_Cabinet_251723.svg'
}
})
}
async function getAddressBooks () {
try {
const context = await UI.authn.findAppInstances(
{ me, div, dom },
ns.vcard('AddressBook')
)
return (context.instances || []).map((book, index) => ({
paneName: 'contact',
tabName: `contact-${index}`,
label: 'Contacts',
subject: book,
icon: UI.icons.iconBase + 'noun_15695.svg'
}))
} catch (err) {
console.error('oops in globalAppTabs AddressBook')
}
return []
}
}
this.getDashboardItems = getDashboardItems
/**
* Call this method to show the global dashboard.
*
* @param {Object} [options] A set of options that can be passed
* @param {string} [options.pane] To open a specific dashboard pane
* @returns {Promise<void>}
*/
async function showDashboard (options = {}) {
const dashboardContainer = getDashboardContainer()
const outlineContainer = getOutlineContainer()
// reuse dashboard if already children already is inserted
if (dashboardContainer.childNodes.length > 0 && options.pane) {
outlineContainer.style.display = 'none'
dashboardContainer.style.display = 'inherit'
const tab = dashboardContainer.querySelector(
`[data-global-pane-name="${options.pane}"]`
)
if (tab) {
tab.click()
return
}
console.warn(
'Did not find the referred tab in global dashboard, will open first one'
)
}
// create a new dashboard if not already present
const dashboard = await globalAppTabs({
selectedTab: options.pane,
onClose: closeDashboard
})
// close the dashboard if user log out
UI.authn.solidAuthClient.trackSession(closeDashboardIfLoggedOut)
// finally - switch to showing dashboard
outlineContainer.style.display = 'none'
dashboardContainer.appendChild(dashboard)
function closeDashboard () {
dashboardContainer.style.display = 'none'
outlineContainer.style.display = 'inherit'
}
function closeDashboardIfLoggedOut (session) {
if (session) {
return
}
closeDashboard()
}
}
this.showDashboard = showDashboard
function getDashboardContainer () {
return getOrCreateContainer('GlobalDashboard')
}
function getOutlineContainer () {
return getOrCreateContainer('outline')
}
/**
* Get element with id or create a new on the fly with that id
*
* @param {string} id The ID of the element you want to get or create
* @returns {HTMLElement}
*/
function getOrCreateContainer (id) {
return (
document.getElementById(id) ||
(() => {
const dashboardContainer = document.createElement('div')
dashboardContainer.id = id
const mainContainer =
document.querySelector('[role="main"]') || document.body
return mainContainer.appendChild(dashboardContainer)
})()
)
}
async function getRelevantPanes (subject, context) {
const panes = context.session.paneRegistry
const relevantPanes = panes.list.filter(
pane => pane.label(subject, context) && !pane.global
)
if (relevantPanes.length === 0) {
// there are no relevant panes, simply return default pane (which ironically is internalPane)
return [panes.byName('internal')]
}
const filteredPanes = await UI.authn.filterAvailablePanes(relevantPanes)
if (filteredPanes.length === 0) {
// if no relevant panes are available panes because of user role, we still allow for the most relevant pane to be viewed
return [relevantPanes[0]]
}
const firstRelevantPaneIndex = panes.list.indexOf(relevantPanes[0])
const firstFilteredPaneIndex = panes.list.indexOf(filteredPanes[0])
// if the first relevant pane is loaded before the panes available wrt role, we still want to offer the most relevant pane
return firstRelevantPaneIndex < firstFilteredPaneIndex
? [relevantPanes[0]].concat(filteredPanes)
: filteredPanes
}
function getPane (relevantPanes, subject) {
return (
relevantPanes.find(
pane => pane.shouldGetFocus && pane.shouldGetFocus(subject)
) || relevantPanes[0]
)
}
async function expandedHeaderTR (subject, requiredPane, options) {
async function renderPaneIconTray (td, options = {}) {
const paneShownStyle =
'width: 24px; border-radius: 0.5em; border-top: solid #222 1px; border-left: solid #222 0.1em; border-bottom: solid #eee 0.1em; border-right: solid #eee 0.1em; margin-left: 1em; padding: 3px; background-color: #ffd;'
const paneHiddenStyle =
'width: 24px; border-radius: 0.5em; margin-left: 1em; padding: 3px'
const paneIconTray = td.appendChild(dom.createElement('nav'))
paneIconTray.style =
'display:flex; justify-content: flex-start; align-items: center;'
const relevantPanes = options.hideList
? []
: await getRelevantPanes(subject, context)
tr.firstPane = requiredPane || getPane(relevantPanes, subject)
const paneNumber = relevantPanes.indexOf(tr.firstPane)
if (relevantPanes.length !== 1) {
// if only one, simplify interface
relevantPanes.forEach((pane, index) => {
const label = pane.label(subject, context)
const ico = UI.utils.AJARImage(pane.icon, label, label, dom)
ico.style = pane === tr.firstPane ? paneShownStyle : paneHiddenStyle // init to something at least
// ico.setAttribute('align','right'); @@ Should be better, but ffox bug pushes them down
// ico.style.width = iconHeight
// ico.style.height = iconHeight
var listen = function (ico, pane) {
// Freeze scope for event time
ico.addEventListener(
'click',
function (event) {
// Find the containing table for this subject
for (var t = td; t.parentNode; t = t.parentNode) {
if (t.nodeName === 'TABLE') break
}
if (t.nodeName !== 'TABLE') {
throw new Error('outline: internal error.')
}
var removePanes = function (specific) {
for (var d = t.firstChild; d; d = d.nextSibling) {
if (typeof d.pane !== 'undefined') {
if (!specific || d.pane === specific) {
if (d.paneButton) {
d.paneButton.setAttribute('class', 'paneHidden')
d.paneButton.style = paneHiddenStyle
}
removeAndRefresh(d)
// If we just delete the node d, ffox doesn't refresh the display properly.
// state = 'paneHidden';
if (
d.pane.requireQueryButton &&
t.parentNode.className /* outer table */ &&
numberOfPanesRequiringQueryButton === 1 &&
dom.getElementById('queryButton')
) {
dom
.getElementById('queryButton')
.setAttribute('style', 'display:none;')
}
}
}
}
}
var renderPane = function (pane) {
var paneDiv
UI.log.info('outline: Rendering pane (2): ' + pane.name)
if (UI.no_catch_pane_errors) {
// for debugging
paneDiv = pane.render(subject, context, options)
} else {
try {
paneDiv = pane.render(subject, context, options)
} catch (e) {
// Easier debugging for pane developers
paneDiv = dom.createElement('div')
paneDiv.setAttribute('class', 'exceptionPane')
var pre = dom.createElement('pre')
paneDiv.appendChild(pre)
pre.appendChild(
dom.createTextNode(UI.utils.stackString(e))
)
}
}
if (
pane.requireQueryButton &&
dom.getElementById('queryButton')
) {
dom.getElementById('queryButton').removeAttribute('style')
}
var second = t.firstChild.nextSibling
var row = dom.createElement('tr')
var cell = row.appendChild(dom.createElement('td'))
cell.appendChild(paneDiv)
if (second) t.insertBefore(row, second)
else t.appendChild(row)
row.pane = pane
row.paneButton = ico
}
var state = ico.getAttribute('class')
if (state === 'paneHidden') {
if (!event.shiftKey) {
// shift means multiple select
removePanes()
}
renderPane(pane)
ico.setAttribute('class', 'paneShown')
ico.style = paneShownStyle
} else {
removePanes(pane)
ico.setAttribute('class', 'paneHidden')
ico.style = paneHiddenStyle
}
var numberOfPanesRequiringQueryButton = 0
for (var d = t.firstChild; d; d = d.nextSibling) {
if (d.pane && d.pane.requireQueryButton) {
numberOfPanesRequiringQueryButton++
}
}
},
false
)
} // listen
listen(ico, pane)
ico.setAttribute(
'class',
index !== paneNumber ? 'paneHidden' : 'paneShown'
)
if (index === paneNumber) tr.paneButton = ico
paneIconTray.appendChild(ico)
})
}
return paneIconTray
} // renderPaneIconTray
// Body of expandedHeaderTR
var tr = dom.createElement('tr')
if (options.hover) {
// By default no hide till hover as community deems it confusing
tr.setAttribute('class', 'hoverControl')
}
var td = tr.appendChild(dom.createElement('td'))
td.setAttribute(
'style',
'margin: 0.2em; border: none; padding: 0; vertical-align: top;' +
'display:flex; justify-content: space-between; flex-direction: row;'
)
td.setAttribute('notSelectable', 'true')
td.setAttribute('about', subject.toNT())
td.setAttribute('colspan', '2')
// Stuff at the right about the subject
const header = td.appendChild(dom.createElement('div'))
header.style =
'display:flex; justify-content: flex-start; align-items: center; flex-wrap: wrap;'
const showHeader = !!requiredPane
if (!options.solo && !showHeader) {
var icon = header.appendChild(
UI.utils.AJARImage(
UI.icons.originalIconBase + 'tbl-collapse.png',
'collapse',
undefined,
dom
)
)
icon.addEventListener('click', collapseMouseDownListener)
var strong = header.appendChild(dom.createElement('h1'))
strong.appendChild(dom.createTextNode(UI.utils.label(subject)))
strong.style =
'font-size: 150%; margin: 0 0.6em 0 0; padding: 0.1em 0.4em;'
UI.widgets.makeDraggable(strong, subject)
}
header.appendChild(
await renderPaneIconTray(td, {
hideList: showHeader
})
)
// set DOM methods
tr.firstChild.tabulatorSelect = function () {
setSelected(this, true)
}
tr.firstChild.tabulatorDeselect = function () {
setSelected(this, false)
}
return tr
} // expandedHeaderTR
// / //////////////////////////////////////////////////////////////////////////
/* PANES
**
** Panes are regions of the outline view in which a particular subject is
** displayed in a particular way. They are like views but views are for query results.
** subject panes are currently stacked vertically.
*/
// / //////////////////// Specific panes are in panes/*.js
//
// The defaultPane is the first one registered for which the label method exists
// Those registered first take priority as a default pane.
// That is, those earlier in this file
/**
* Pane registration
*/
// the second argument indicates whether the query button is required
// / ///////////////////////////////////////////////////////////////////////////
// Remove a node from the DOM so that Firefox refreshes the screen OK
// Just deleting it cause whitespace to accumulate.
function removeAndRefresh (d) {
var table = d.parentNode
var par = table.parentNode
var placeholder = dom.createElement('table')
placeholder.setAttribute('style', 'width: 100%;')
par.replaceChild(placeholder, table)
table.removeChild(d)
par.replaceChild(table, placeholder) // Attempt to
}
var propertyTable = (this.propertyTable = function propertyTable (
subject,
table,
pane,
options
) {
UI.log.debug('Property table for: ' + subject)
subject = kb.canon(subject)
// if (!pane) pane = panes.defaultPane;
if (!table) {
// Create a new property table
table = dom.createElement('table')
table.setAttribute('style', 'width: 100%;')
expandedHeaderTR(subject, pane, options).then(tr1 => {
table.appendChild(tr1)
if (tr1.firstPane) {
var paneDiv
try {
UI.log.info('outline: Rendering pane (1): ' + tr1.firstPane.name)
paneDiv = tr1.firstPane.render(subject, context, options)
} catch (e) {
// Easier debugging for pane developers
paneDiv = dom.createElement('div')
paneDiv.setAttribute('class', 'exceptionPane')
var pre = dom.createElement('pre')
paneDiv.appendChild(pre)
pre.appendChild(dom.createTextNode(UI.utils.stackString(e)))
}
var row = dom.createElement('tr')
var cell = row.appendChild(dom.createElement('td'))
cell.appendChild(paneDiv)
if (
tr1.firstPane.requireQueryButton &&
dom.getElementById('queryButton')
) {
dom.getElementById('queryButton').removeAttribute('style')
}
table.appendChild(row)
row.pane = tr1.firstPane
row.paneButton = tr1.paneButton
}
})
return table
} else {
// New display of existing table, keeping expanded bits
UI.log.info('Re-expand: ' + table)
// do some other stuff here
return table
}
}) /* propertyTable */
function propertyTR (doc, st, inverse) {
var tr = doc.createElement('TR')
tr.AJAR_statement = st
tr.AJAR_inverse = inverse
// tr.AJAR_variable = null; // @@ ?? was just 'tr.AJAR_variable'
tr.setAttribute('predTR', 'true')
var predicateTD = thisOutline.outlinePredicateTD(st.predicate, tr, inverse)
tr.appendChild(predicateTD) // @@ add 'internal' to predicateTD's class for style? mno
return tr
}
this.propertyTR = propertyTR
// / ////////// Property list
function appendPropertyTRs (parent, plist, inverse, predicateFilter) {
// UI.log.info('@appendPropertyTRs, 'this' is %s, dom is %s, '+ // Gives 'can't access dead object'
// 'thisOutline.document is %s', this, dom.location, thisOutline.document.location);
// UI.log.info('@appendPropertyTRs, dom is now ' + this.document.location);
// UI.log.info('@appendPropertyTRs, dom is now ' + thisOutline.document.location);
UI.log.debug('Property list length = ' + plist.length)
if (plist.length === 0) return ''
var sel, j, k
if (inverse) {
sel = function (x) {
return x.subject
}
plist = plist.sort(UI.utils.RDFComparePredicateSubject)
} else {
sel = function (x) {
return x.object
}
plist = plist.sort(UI.utils.RDFComparePredicateObject)
}
var max = plist.length
for (j = 0; j < max; j++) {
// squishing together equivalent properties I think
var s = plist[j]
// if (s.object == parentSubject) continue; // that we knew
// Avoid predicates from other panes
if (predicateFilter && !predicateFilter(s.predicate, inverse)) continue
var tr = propertyTR(dom, s, inverse)
parent.appendChild(tr)
var predicateTD = tr.firstChild // we need to kludge the rowspan later
var defaultpropview = views.defaults[s.predicate.uri]
// LANGUAGE PREFERENCES WAS AVAILABLE WITH FF EXTENSION - get from elsewhere?
var dups = 0 // How many rows have the same predicate, -1?
var langTagged = 0 // how many objects have language tags?
var myLang = 0 // Is there one I like?
for (
k = 0;
k + j < max && plist[j + k].predicate.sameTerm(s.predicate);
k++
) {
if (k > 0 && sel(plist[j + k]).sameTerm(sel(plist[j + k - 1]))) dups++
if (sel(plist[j + k]).lang && outline.labeller.LanguagePreference) {
langTagged += 1
if (
sel(plist[j + k]).lang.indexOf(
outline.labeller.LanguagePreference
) >= 0
) {
myLang++
}
}
}
/* Display only the one in the preferred language
ONLY in the case (currently) when all the values are tagged.
Then we treat them as alternatives. */
if (myLang > 0 && langTagged === dups + 1) {
for (let k = j; k <= j + dups; k++) {
if (
outline.labeller.LanguagePreference &&
sel(plist[k]).lang.indexOf(outline.labeller.LanguagePreference) >= 0
) {
tr.appendChild(
thisOutline.outlineObjectTD(
sel(plist[k]),
defaultpropview,
undefined,
s
)
)
break
}
}
j += dups // extra push
continue
}
tr.appendChild(
thisOutline.outlineObjectTD(sel(s), defaultpropview, undefined, s)
)
/* Note: showNobj shows between n to 2n objects.
* This is to prevent the case where you have a long list of objects
* shown, and dangling at the end is '1 more' (which is easily ignored)
* Therefore more objects are shown than hidden.
*/
tr.showNobj = function (n) {
var predDups = k - dups
var show = 2 * n < predDups ? n : predDups
var showLaterArray = []
if (predDups !== 1) {
predicateTD.setAttribute(
'rowspan',
show === predDups ? predDups : n + 1
)
var l
if (show < predDups && show === 1) {
// what case is this...
predicateTD.setAttribute('rowspan', 2)
}
var displayed = 0 // The number of cells generated-1,
// all duplicate thing removed
for (l = 1; l < k; l++) {
// This detects the same things
if (
!kb
.canon(sel(plist[j + l]))
.sameTerm(kb.canon(sel(plist[j + l - 1])))
) {
displayed++
s = plist[j + l]
defaultpropview = views.defaults[s.predicate.uri]
var trObj = dom.createElement('tr')
trObj.style.colspan = '1'
trObj.appendChild(
thisOutline.outlineObjectTD(
sel(plist[j + l]),
defaultpropview,
undefined,
s
)
)
trObj.AJAR_statement = s
trObj.AJAR_inverse = inverse
parent.appendChild(trObj)
if (displayed >= show) {
trObj.style.display = 'none'
showLaterArray.push(trObj)
}
} else {
// ToDo: show all the data sources of this statement
UI.log.info('there are duplicates here: %s', plist[j + l - 1])
}
}
// @@a quick fix on the messing problem.
if (show === predDups) {
predicateTD.setAttribute('rowspan', displayed + 1)
}
} // end of if (predDups!==1)
if (show < predDups) {
// Add the x more <TR> here
var moreTR = dom.createElement('tr')
var moreTD = moreTR.appendChild(dom.createElement('td'))
moreTD.setAttribute(
'style',
'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
)
moreTD.setAttribute('notSelectable', 'false')
if (predDups > n) {
// what is this for??
var small = dom.createElement('a')
moreTD.appendChild(small)
var predToggle = (function (f) {
return f(predicateTD, k, dups, n)
})(function (predicateTD, k, dups, n) {
return function (display) {
small.innerHTML = ''
if (display === 'none') {
small.appendChild(
UI.utils.AJARImage(
UI.icons.originalIconBase + 'tbl-more-trans.png',
'more',
'See all',
dom
)
)
small.appendChild(
dom.createTextNode(predDups - n + ' more...')
)
predicateTD.setAttribute('rowspan', n + 1)
} else {
small.appendChild(
UI.utils.AJARImage(
UI.icons.originalIconBase + 'tbl-shrink.png',
'(less)',
undefined,
dom
)
)
predicateTD.setAttribute('rowspan', predDups + 1)
}
for (var i = 0; i < showLaterArray.length; i++) {
var trObj = showLaterArray[i]
trObj.style.display = display
}
}
}) // ???
var current = 'none'
var toggleObj = function (event) {
predToggle(current)
current = current === 'none' ? '' : 'none'
if (event) event.stopPropagation()
return false // what is this for?
}
toggleObj()
small.addEventListener('click', toggleObj, false)
} // if(predDups>n)
parent.appendChild(moreTR)
} // if
} // tr.showNobj
tr.showAllobj = function () {
tr.showNobj(k - dups)
}
tr.showNobj(10)
j += k - 1 // extra push
}
} // appendPropertyTRs
this.appendPropertyTRs = appendPropertyTRs
/* termWidget
**
*/
var termWidget = {} // @@@@@@ global
global.termWidget = termWidget
termWidget.construct = function (dom) {
dom = dom || document
var td = dom.createElement('TD')
td.setAttribute(
'style',
'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
)
td.setAttribute('class', 'iconTD')
td.setAttribute('notSelectable', 'true')
td.style.width = '0px'
return td
}
termWidget.addIcon = function (td, icon, listener) {
var iconTD = td.childNodes[1]
if (!iconTD) return
var width = iconTD.style.width
var img = UI.utils.AJARImage(icon.src, icon.alt, icon.tooltip, dom)
width = parseInt(width)
width = width + icon.width
iconTD.style.width = width + 'px'
iconTD.appendChild(img)
if (listener) {
img.addEventListener('click', listener)
}
}
termWidget.removeIcon = function (td, icon) {
var iconTD = td.childNodes[1]
var baseURI
if (!iconTD) return
var width = iconTD.style.width
width = parseInt(width)
width = width - icon.width
iconTD.style.width = width + 'px'
for (var x = 0; x < iconTD.childNodes.length; x++) {
var elt = iconTD.childNodes[x]
var eltSrc = elt.src
// ignore first '?' and everything after it //Kenny doesn't know what this is for
try {
baseURI = dom.location.href.split('?')[0]
} catch (e) {
console.log(e)
baseURI = ''
}
var relativeIconSrc = UI.rdf.uri.join(icon.src, baseURI)
if (eltSrc === relativeIconSrc) {
iconTD.removeChild(elt)
}
}
}
termWidget.replaceIcon = function (td, oldIcon, newIcon, listener) {
termWidget.removeIcon(td, oldIcon)
termWidget.addIcon(td, newIcon, listener)
}
// / /////////////////////////////////////////////////// VALUE BROWSER VIEW
// / /////////////////////////////////////////////////////// TABLE VIEW
// Summarize a thing as a table cell
/**********************
query global vars
***********************/
// const doesn't work in Opera
// const BLANK_QUERY = { pat: kb.formula(), vars: [], orderBy: [] };
// @ pat: the query pattern in an RDFIndexedFormula. Statements are in pat.statements
// @ vars: the free variables in the query
// @ orderBy: the variables to order the table
function QueryObj () {
this.pat = kb.formula()
this.vars = []
// this.orderBy = []
}
var queries = []
queries[0] = new QueryObj()
/*
function querySave () {
queries.push(queries[0])
var choices = dom.getElementById('queryChoices')
var next = dom.createElement('option')
var box = dom.createElement('input')
var index = queries.length - 1
box.setAttribute('type', 'checkBox')
box.setAttribute('value', index)
choices.appendChild(box)
choices.appendChild(dom.createTextNode('Saved query #' + index))
choices.appendChild(dom.createElement('br'))
next.setAttribute('value', index)
next.appendChild(dom.createTextNode('Saved query #' + index))
dom.getElementById('queryJump').appendChild(next)
}
*/
/*
function resetQuery () {
function resetOutliner (pat) {
var n = pat.statements.length
var pattern, tr
for (let i = 0; i < n; i++) {
pattern = pat.statements[i]
tr = pattern.tr
// UI.log.debug('tr: ' + tr.AJAR_statement);
if (typeof tr !== 'undefined') {
delete tr.AJAR_pattern
delete tr.AJAR_variable
}
}
for (let x in pat.optional) { resetOutliner(pat.optional[x]) }
}
resetOutliner(myQuery.pat)
UI.utils.clearVariableNames()
queries[0] = myQuery = new QueryObj()
}
*/
function addButtonCallbacks (target, fireOn) {
UI.log.debug('Button callbacks for ' + fireOn + ' added')
var makeIconCallback = function (icon) {
return function IconCallback (req) {
if (req.indexOf('#') >= 0) {
console.log(
'@@ makeIconCallback: Not expecting # in URI whose state changed: ' +
req
)
// alert('Should have no hash in '+req)
}
if (!target) {
return false
}
if (!outline.ancestor(target, 'DIV')) return false
// if (term.termType != 'symbol') { return true } // should always ve
if (req === fireOn) {
target.src = icon
target.title = outlineIcons.tooltips[icon]
}
return true
}
}
sf.addCallback('request', makeIconCallback(outlineIcons.src.icon_requested))
sf.addCallback('done', makeIconCallback(outlineIcons.src.icon_fetched))
sf.addCallback('fail', makeIconCallback(outlineIcons.src.icon_failed))
}
// Selection support
function selected (node) {
var a = node.getAttribute('class')
if (a && a.indexOf('selected') >= 0) return true
return false
}
// These woulkd be simpler using closer variables below
function optOnIconMouseDownListener (e) {
// outlineIcons.src.icon_opton needed?
var target = thisOutline.targetOf(e)
var p = target.parentNode
termWidget.replaceIcon(
p.parentNode,
outlineIcons.termWidgets.optOn,
outlineIcons.termWidgets.optOff,
optOffIconMouseDownListener
)
p.parentNode.parentNode.removeAttribute('optional')
}
function optOffIconMouseDownListener (e) {
// outlineIcons.src.icon_optoff needed?
var target = thisOutline.targetOf(e)
var p = target.parentNode
termWidget.replaceIcon(
p.parentNode,
outlineIcons.termWidgets.optOff,
outlineIcons.termWidgets.optOn,
optOnIconMouseDownListener
)
p.parentNode.parentNode.setAttribute('optional', 'true')
}
function setSelectedParent (node, inc) {
var onIcon = outlineIcons.termWidgets.optOn
var offIcon = outlineIcons.termWidgets.optOff
for (var n = node; n.parentNode; n = n.parentNode) {
while (true) {
if (n.getAttribute('predTR')) {
var num = n.getAttribute('parentOfSelected')
if (!num) num = 0
else num = parseInt(num)
if (num === 0 && inc > 0) {
termWidget.addIcon(
n.childNodes[0],
n.getAttribute('optional') ? onIcon : offIcon,
n.getAttribute('optional')
? optOnIconMouseDownListener
: optOffIconMouseDownListener
)
}
num = num + inc
n.setAttribute('parentOfSelected', num)
if (num === 0) {
n.removeAttribute('parentOfSelected')
termWidget.removeIcon(
n.childNodes[0],
n.getAttribute('optional') ? onIcon : offIcon
)
}
break
} else if (n.previousSibling && n.previousSibling.nodeName === 'TR') {
n = n.previousSibling
} else break
}
}
}
this.statusBarClick = function (event) {
var target = UI.utils.getTarget(event)
if (target.label) {
window.content.location = target.label
// The following alternative does not work in the extension.
// var s = UI.store.sym(target.label);
// outline.GotoSubject(s, true);
}
}
this.showURI = function showURI (about) {
if (about && dom.getElementById('UserURI')) {
dom.getElementById('UserURI').value =
about.termType === 'NamedNode' ? about.uri : '' // blank if no URI
}
}
this.showSource = function showSource () {
if (typeof sourceWidget === 'undefined') return
// deselect all before going on, this is necessary because you would switch tab,
// close tab or so on...
for (var uri in sourceWidget.sources) {
sourceWidget.sources[uri].setAttribute('class', '')
} // .class doesn't work. Be careful!
for (var i = 0; i < selection.length; i++) {
if (!selection[i].parentNode) {
console.log('showSource: EH? no parentNode? ' + selection[i] + '\n')
continue
}
var st = selection[i].parentNode.AJAR_statement
if (!st) continue // for root TD
var source = st.why
if (source && source.uri) {
sourceWidget.highlight(source, true)
}
}
}
this.getSelection = function getSelection () {
return selection
}
function setSelected (node, newValue) {
// UI.log.info('selection has ' +selection.map(function(item){return item.textContent;}).join(', '));
// UI.log.debug('@outline setSelected, intended to '+(newValue?'select ':'deselect ')+node+node.textContent);
// if (newValue === selected(node)) return; //we might not need this anymore...
if (node.nodeName !== 'TD') {
UI.log.debug('down' + node.nodeName)
throw new Error(
'Expected TD in setSelected: ' +
node.nodeName +
' : ' +
node.textContent
)
}
UI.log.debug('pass')
var cla = node.getAttribute('class')
if (!cla) cla = ''
if (newValue) {
cla += ' selected'
if (cla.indexOf('pred') >= 0 || cla.indexOf('obj') >= 0) {
setSelectedParent(node, 1)
}
selection.push(node)
// UI.log.info('Selecting '+node.textContent)
var about = UI.utils.getTerm(node) // show uri for a newly selectedTd
thisOutline.showURI(about)
var st = node.AJAR_statement // show blue cross when the why of that triple is editable
if (typeof st === 'undefined') st = node.parentNode.AJAR_statement
// if (typeof st === 'undefined') return; // @@ Kludge? Click in the middle of nowhere
if (st) {
// don't do these for headers or base nodes
var source = st.why
// var target = st.why
var editable = UI.store.updater.editable(source.uri, kb)
if (!editable) {
// let target = node.parentNode.AJAR_inverse ? st.object : st.subject
} // left hand side
// think about this later. Because we update to the why for now.
// alert('Target='+target+', editable='+editable+'\nselected statement:' + st)
if (editable && cla.indexOf('pred') >= 0) {
termWidget.addIcon(node, outlineIcons.termWidgets.addTri)
} // Add blue plus
}
} else {
UI.log.debug('cla=$' + cla + '$')
if (cla === 'selected') cla = '' // for header <TD>
cla = cla.replace(' selected', '')
if (cla.indexOf('pred') >= 0 || cla.indexOf('obj') >= 0) {
setSelectedParent(node, -1)
}
if (cla.indexOf('pred') >= 0) {
termWidget.removeIcon(node, outlineIcons.termWidgets.addTri)
}
selection = selection.filter(function (x) {
return x === node
})
UI.log.info('Deselecting ' + node.textContent)
}
if (typeof sourceWidget !== 'undefined') thisOutline.showSource() // Update the data sources display
// UI.log.info('selection becomes [' +selection.map(function(item){return item.textContent;}).join(', ')+']');
// UI.log.info('Setting className ' + cla);
node.setAttribute('class', cla)
}
function deselectAll () {
var n = selection.length
for (let i = n - 1; i >= 0; i--) setSelected(selection[i], false)
selection = []
}
/** Get the target of an event **/
this.targetOf = function (e) {
var target
if (!e) e = window.event
if (e.target) {
target = e.target
} else if (e.srcElement) {
target = e.srcElement
} else {
UI.log.error("can't get target for event " + e)
return false
} // fail
if (target.nodeType === 3) {
// defeat Safari bug [sic]
target = target.parentNode
}
return target
} // targetOf
this.walk = function walk (directionCode, inputTd) {
var selectedTd = inputTd || selection[0]
var newSelTd
switch (directionCode) {
case 'down':
try {
newSelTd = selectedTd.parentNode.nextSibling.lastChild
} catch (e) {
this.walk('up')
return
} // end
deselectAll()
setSelected(newSelTd, true)
break
case 'up':
try {
newSelTd = selectedTd.parentNode.previousSibling.lastChild
} catch (e) {
return
} // top
deselectAll()
setSelected(newSelTd, true)
break
case 'right':
deselectAll()
if (
selectedTd.nextSibling ||
selectedTd.lastChild.tagName === 'strong'
) {
setSelected(selectedTd.nextSibling, true)
} else {
var newSelected = dom.evaluate(
'table/div/tr/td[2]',
selectedTd,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue
setSelected(newSelected, true)
}
break
case 'left':
deselectAll()
if (
selectedTd.previousSibling &&
selectedTd.previousSibling.className === 'undetermined'
) {
setSelected(selectedTd.previousSibling, true)
return true // do not shrink signal
} else {
setSelected(UI.utils.ancestor(selectedTd.parentNode, 'TD'), true)
}