solid-panes
Version:
Solid-compatible Panes: applets and views for the mashlib and databrowser
1,481 lines (1,364 loc) • 80.1 kB
JavaScript
/* istanbul ignore file */
/* -*- coding: utf-8-dos -*-
Outline Mode Manager
*/
import * as paneRegistry from 'pane-registry'
import * as $rdf from 'rdflib'
import * as UI from 'solid-ui'
import { authn, authSession, store } from 'solid-logic'
import { propertyViews } from './propertyViews'
import { outlineIcons } from './outlineIcons.js' // @@ chec
import { UserInput } from './userInput.js'
import * as queryByExample from './queryByExample.js'
/* global alert XPathResult sourceWidget */
// XPathResult?
// const iconHeight = '24px'
export default function (context) {
const dom = context.dom
this.document = context.dom
this.outlineIcons = outlineIcons
this.labeller = this.labeller || {}
this.labeller.LanguagePreference = '' // for now
const outline = this // Kenny: do we need this?
const thisOutline = this
let selection = []
this.selection = selection
this.ancestor = UI.utils.ancestor // make available as outline.ancestor in callbacks
this.sparql = UI.rdf.UpdateManager
this.kb = store
const kb = store
const sf = 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)
const outlineElement = this.outlineElement
this.init = function () {
const table = getOutlineContainer()
table.outline = this
}
/** benchmark a function **/
benchmark.lastkbsize = 0
function benchmark (f) {
const args = []
for (let i = arguments.length - 1; i > 0; i--) args[i - 1] = arguments[i]
// UI.log.debug('BENCHMARK: args=' + args.join());
const begin = new Date().getTime()
const returnValue = f.apply(f, args)
const 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) {
const 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
const uris = kb.uris(obj)
uris.sort()
let last = null
for (let 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 ''
const docuri = UI.rdf.uri.docpart(uri)
if (docuri.slice(0, 5) !== 'http:') return ''
const state = sf.getState(docuri)
let 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
const 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
/** 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
) {
const td = dom.createElement('td')
td.setAttribute(
'style',
'margin: 0.2em; border: none; padding: 0; vertical-align: top;'
)
td.setAttribute('notSelectable', 'false')
let theClass = 'obj'
// 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)
}
// set DOM methods
td.tabulatorSelect = function () {
setSelected(this, true)
}
td.tabulatorDeselect = function () {
setSelected(this, false)
}
td.addEventListener('click', selectableTDClickListener)
return td
} // outlineObjectTD
this.outlinePredicateTD = function outlinePredicateTD (
predicate,
newTr,
inverse,
internal
) {
const 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';
const 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 (const 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])
}
}
// 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 = 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 = paneRegistry.byName(item.paneName) // 20190701
containerDiv.innerHTML = ''
const table = containerDiv.appendChild(dom.createElement('table'))
const me = 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 = authn.currentUser()
if (!me) return []
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: 'profile',
label: 'Your Profile',
icon: UI.icons.iconBase + 'noun_15059.svg'
},
{
paneName: 'editProfile',
label: 'Edit your Profile',
icon: UI.icons.iconBase + 'noun_492246.svg'
}
]
.concat(books)
.concat(pods)
async function getPods () {
async function addPodStorage (pod) { // namedNode
await loadContainerRepresentation(pod)
if (kb.holds(pod, ns.rdf('type'), ns.space('Storage'), pod.doc())) {
pods.push(pod)
return true
}
return false
}
async function addPodStorageFromUrl (url) {
const podStorage = new URL(url)
// check for predicate pim:Storage in containers up the path tree
let pathStorage = podStorage.pathname
while (pathStorage.length) {
pathStorage = pathStorage.substring(0, pathStorage.lastIndexOf('/'))
if (await addPodStorage(kb.sym(`${podStorage.origin}${pathStorage}/`))) return
}
// TODO should url.origin be added to pods list when there are no pim:Storage ???
}
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 []
}
// load pod's storages from profile
let pods = kb.each(me, ns.space('storage'), null, me.doc())
pods.map(async (pod) => {
// TODO use addPodStorageFromUrl(pod.uri) to check for pim:Storage ???
await loadContainerRepresentation(pod)
})
try {
// if uri then SolidOS is a browse.html web app
const uri = (new URL(window.location.href)).searchParams.get('uri')
const podUrl = uri || window.location.href
await addPodStorageFromUrl(podUrl)
} catch (err) {
console.error('cannot load container', err)
}
// remove namedNodes duplicates
function uniques (nodes) {
const uniqueNodes = []
nodes.forEach(node => {
if (!uniqueNodes.find(uniqueNode => uniqueNode.equals(node))) uniqueNodes.push(node)
})
return uniqueNodes
}
pods = uniques(pods)
if (!pods.length) return []
return pods.map((pod, index) => {
function split (item) { return item.uri.split('//')[1].slice(0, -1) }
const label = split(me).startsWith(split(pod)) ? 'Your storage' : split(pod)
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.login.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
authSession.onLogout(closeDashboard)
// finally - switch to showing dashboard
outlineContainer.style.display = 'none'
dashboardContainer.appendChild(dashboard)
const tab = dashboardContainer.querySelector(
`[data-global-pane-name="${options.pane}"]`
)
if (tab) {
tab.click()
}
function closeDashboard () {
dashboardContainer.style.display = 'none'
outlineContainer.style.display = 'inherit'
}
}
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 loadContainerRepresentation (subject) {
// force reload for index.html with RDFa
if (!kb.any(subject, ns.ldp('contains'), undefined, subject.doc())) {
const response = await kb.fetcher.webOperation('GET', subject.uri, kb.fetcher.initFetchOptions(subject.uri, { headers: { accept: 'text/turtle' } }))
const containerTurtle = response.responseText
$rdf.parse(containerTurtle, kb, subject.uri, 'text/turtle')
}
}
async function getRelevantPanes (subject, context) {
// make sure container representation is loaded (when server returns index.html)
if (subject.uri.endsWith('/')) { await loadContainerRepresentation(subject) }
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.login.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
const listen = function (ico, pane) {
// Freeze scope for event time
ico.addEventListener(
'click',
function (event) {
let containingTable
// Find the containing table for this subject
for (containingTable = td; containingTable.parentNode; containingTable = containingTable.parentNode) {
if (containingTable.nodeName === 'TABLE') break
}
if (containingTable.nodeName !== 'TABLE') {
throw new Error('outline: internal error.')
}
const removePanes = function (specific) {
for (let d = containingTable.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 &&
containingTable.parentNode.className /* outer table */ &&
numberOfPanesRequiringQueryButton === 1 &&
dom.getElementById('queryButton')
) {
dom
.getElementById('queryButton')
.setAttribute('style', 'display:none;')
}
}
}
}
}
const renderPane = function (pane) {
let paneDiv
UI.log.info('outline: Rendering pane (2): ' + pane.name)
try {
paneDiv = pane.render(subject, context, options)
} catch (e) {
// Easier debugging for pane developers
paneDiv = dom.createElement('div')
paneDiv.setAttribute('class', 'exceptionPane')
const 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')
}
const second = containingTable.firstChild.nextSibling
const row = dom.createElement('tr')
const cell = row.appendChild(dom.createElement('td'))
cell.appendChild(paneDiv)
if (second) containingTable.insertBefore(row, second)
else containingTable.appendChild(row)
row.pane = pane
row.paneButton = ico
}
const 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
}
let numberOfPanesRequiringQueryButton = 0
for (let d = containingTable.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
const tr = dom.createElement('tr')
if (options.hover) {
// By default no hide till hover as community deems it confusing
tr.setAttribute('class', 'hoverControl')
}
const 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) {
const icon = header.appendChild(
UI.utils.AJARImage(
UI.icons.originalIconBase + 'tbl-collapse.png',
'collapse',
undefined,
dom
)
)
icon.addEventListener('click', collapseMouseDownListener)
const 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) {
const table = d.parentNode
const par = table.parentNode
const placeholder = dom.createElement('table')
placeholder.setAttribute('style', 'width: 100%;')
par.replaceChild(placeholder, table)
table.removeChild(d)
par.replaceChild(table, placeholder) // Attempt to
}
const 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) {
let 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')
const pre = dom.createElement('pre')
paneDiv.appendChild(pre)
pre.appendChild(dom.createTextNode(UI.utils.stackString(e)))
}
const row = dom.createElement('tr')
const 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) {
const 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')
const 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 ''
let 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)
}
const max = plist.length
for (j = 0; j < max; j++) {
// squishing together equivalent properties I think
let s = plist[j]
// if (s.object == parentSubject) continue; // that we knew
// Avoid predicates from other panes
if (predicateFilter && !predicateFilter(s.predicate, inverse)) continue
const tr = propertyTR(dom, s, inverse)
parent.appendChild(tr)
const predicateTD = tr.firstChild // we need to kludge the rowspan later
let defaultpropview = views.defaults[s.predicate.uri]
// LANGUAGE PREFERENCES WAS AVAILABLE WITH FF EXTENSION - get from elsewhere?
let dups = 0 // How many rows have the same predicate, -1?
let langTagged = 0 // how many objects have language tags?
let 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) {
const predDups = k - dups
const show = 2 * n < predDups ? n : predDups
const showLaterArray = []
if (predDups !== 1) {
predicateTD.setAttribute(
'rowspan',
show === predDups ? predDups : n + 1
)
let l
if (show < predDups && show === 1) {
// what case is this...
predicateTD.setAttribute('rowspan', 2)
}
let 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]
const 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
const moreTR = dom.createElement('tr')
const 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??
const small = dom.createElement('a')
moreTD.appendChild(small)
const 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 (let i = 0; i < showLaterArray.length; i++) {
const trObj = showLaterArray[i]
trObj.style.display = display
}
}
}) // ???
let current = 'none'
const 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
**
*/
const termWidget = {} // @@@@@@ global
global.termWidget = termWidget
termWidget.construct = function (dom) {
dom = dom || document
const 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) {
const iconTD = td.childNodes[1]
if (!iconTD) return
let width = iconTD.style.width
const 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) {
const iconTD = td.childNodes[1]
let baseURI
if (!iconTD) return
let width = iconTD.style.width
width = parseInt(width)
width = width - icon.width
iconTD.style.width = width + 'px'
for (let x = 0; x < iconTD.childNodes.length; x++) {
const elt = iconTD.childNodes[x]
const 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 = ''
}
const 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 = []
}
const 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')
const 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) {
const 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?
const target = thisOutline.targetOf(e)
const 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?
const target = thisOutline.targetOf(e)
const p = target.parentNode
termWidget.replaceIcon(
p.parentNode,
outlineIcons.termWidgets.optOff,
outlineIcons.termWidgets.optOn,
optOnIconMouseDownListener
)
p.parentNode.parentNode.setAttribute('optional', 'true')
}
function setSelectedParent (node, inc) {
const onIcon = outlineIcons.termWidgets.optOn
const offIcon = outlineIcons.termWidgets.optOff
for (let n = node; n.parentNode; n = n.parentNode) {
while (true) {
if (n.getAttribute('predTR')) {
let 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) {
const target = UI.utils.getTarget(event)
if (target.label) {
window.content.location = target.label
// The following alternative does not work in the extension.
// var s = 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 (const uri in sourceWidget.sources) {
sourceWidget.sources[uri].setAttribute('class', '')
} // .class doesn't work. Be careful!
for (let i = 0; i < selection.length; i++) {
if (!selection[i].parentNode) {
console.log('showSource: EH? no parentNode? ' + selection[i] + '\n')
continue
}
const st = selection[i].parentNode.AJAR_statement
if (!st) continue // for root TD
const 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')
let 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)
const about = UI.utils.getTerm(node) // show uri for a newly selectedTd
thisOutline.showURI(about)
let st = node.AJAR_statement // show blue cross when the why of that triple is editable
if (typeof st === 'undefined' && node.parentNode) 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
const source = st.why
// var target = st.why
const editable = 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 () {
const 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) {
let 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) {
const selectedTd = inputTd || selection[0]
let 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'
) {
setSelect