UNPKG

solid-ui

Version:

UI library for writing Solid read-write-web applications

1,594 lines (1,228 loc) • 48.9 kB
"use strict"; var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.renderTableViewPane = renderTableViewPane; var _index2 = require("./authn/index"); var debug = _interopRequireWildcard(require("./debug")); var _iconBase = require("./iconBase"); var _logic = require("./logic"); var log = _interopRequireWildcard(require("./log")); var ns = _interopRequireWildcard(require("./ns")); var rdf = _interopRequireWildcard(require("rdflib")); var style = _interopRequireWildcard(require("./style")); var utils = _interopRequireWildcard(require("./utils")); var widgets = _interopRequireWildcard(require("./widgets")); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } // Table Widget: Format an array of RDF statements as an HTML table. // // This can operate in one of three modes: when the class of object is given // or when the source document from whuch data is taken is given, // or if a prepared query object is given. // (In principle it could operate with neither class nor document // given but typically // there would be too much data.) // When the tableClass is not given, it looks for common classes in the data, // and gives the user the option. // // 2008 Written, Ilaria Liccardi as the tableViewPane.js // 2014 Core table widget moved into common/table.js - timbl // // pull in first avoid cross-refs var UI = { icons: _iconBase.icons, log: log, ns: ns, store: _logic.store, utils: utils, widgets: widgets }; // UI.widgets.renderTableViewPane function renderTableViewPane(doc, options) { var sourceDocument = options.sourceDocument; var tableClass = options.tableClass; var givenQuery = options.query; var RDFS_LITERAL = 'http://www.w3.org/2000/01/rdf-schema#Literal'; var ns = UI.ns; var kb = UI.store; var rowsLookup = {}; // Persistent mapping of subject URI to dom TR // Predicates that are never made into columns: var FORBIDDEN_COLUMNS = { 'http://www.w3.org/2002/07/owl#sameAs': true, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type': true }; // Number types defined in the XML schema: var XSD_NUMBER_TYPES = { 'http://www.w3.org/2001/XMLSchema#decimal': true, 'http://www.w3.org/2001/XMLSchema#float': true, 'http://www.w3.org/2001/XMLSchema#double': true, 'http://www.w3.org/2001/XMLSchema#integer': true, 'http://www.w3.org/2001/XMLSchema#nonNegativeInteger': true, 'http://www.w3.org/2001/XMLSchema#positiveInteger': true, 'http://www.w3.org/2001/XMLSchema#nonPositiveInteger': true, 'http://www.w3.org/2001/XMLSchema#negativeInteger': true, 'http://www.w3.org/2001/XMLSchema#long': true, 'http://www.w3.org/2001/XMLSchema#int': true, 'http://www.w3.org/2001/XMLSchema#short': true, 'http://www.w3.org/2001/XMLSchema#byte': true, 'http://www.w3.org/2001/XMLSchema#unsignedLong': true, 'http://www.w3.org/2001/XMLSchema#unsignedInt': true, 'http://www.w3.org/2001/XMLSchema#unsignedShort': true, 'http://www.w3.org/2001/XMLSchema#unsignedByte': true }; var XSD_DATE_TYPES = { 'http://www.w3.org/2001/XMLSchema#dateTime': true, 'http://www.w3.org/2001/XMLSchema#date': true }; // Classes that indicate an image: var IMAGE_TYPES = { 'http://xmlns.com/foaf/0.1/Image': true, 'http://purl.org/dc/terms/Image': true }; // Name of the column used as a "key" value to look up the row. // This is necessary because in the normal view, the columns are // all "optional" values, meaning that we will get a result set // for every individual value that is found. The row key acts // as an anchor that can be used to combine this information // back into the same row. var keyVariable = options.keyVariable || '?_row'; var subjectIdCounter = 0; var allType, types; var typeSelectorDiv, addColumnDiv; // The last SPARQL query used: var lastQuery = null; var mostCommonType = null; var resultDiv = doc.createElement('div'); resultDiv.className = 'tableViewPane'; resultDiv.appendChild(generateControlBar()); // sets typeSelectorDiv var tableDiv = doc.createElement('div'); resultDiv.appendChild(tableDiv); // Save a refresh function for use by caller resultDiv.refresh = function () { runQuery(table.query, table.logicalRows, table.columns, table); // updateTable(givenQuery, mostCommonType) // This could be a lot more incremental and efficient }; // A specifically asked-for query if (givenQuery) { var table = renderTableForQuery(givenQuery); // lastQuery = givenQuery tableDiv.appendChild(table); } else { // Find the most common type and select it by default var s = calculateTable(); allType = s[0]; types = s[1]; if (!tableClass) { typeSelectorDiv.appendChild(generateTypeSelector(allType, types)); } mostCommonType = getMostCommonType(types); if (mostCommonType) { buildFilteredTable(mostCommonType); } else { buildFilteredTable(allType); } } return resultDiv; // ///////////////////////////////////////////////////////////////// /* function closeDialog (dialog) { dialog.parentNode.removeChild(dialog) } function createActionButton (label, callback) { var button = doc.createElement('input') button.setAttribute('type', 'submit') button.setAttribute('value', label) button.addEventListener('click', callback, false) return button } // @@ Tdo: put these buttonsback in, // to allow user to see and edit and save the sparql query for the table they are looking at // function createSparqlWindow () { var dialog = doc.createElement('div') dialog.setAttribute('class', 'sparqlDialog') var title = doc.createElement('h3') title.appendChild(doc.createTextNode('Edit SPARQL query')) var inputbox = doc.createElement('textarea') inputbox.value = rdf.queryToSPARQL(lastQuery) dialog.appendChild(title) dialog.appendChild(inputbox) dialog.appendChild(createActionButton('Query', function () { var query = rdf.SPARQLToQuery(inputbox.value) updateTable(query) closeDialog(dialog) })) dialog.appendChild(createActionButton('Close', function () { closeDialog(dialog) })) return dialog } function sparqlButtonPressed () { var dialog = createSparqlWindow() resultDiv.appendChild(dialog) } function generateSparqlButton () { var image = doc.createElement('img') image.setAttribute('class', 'sparqlButton') image.setAttribute('src', UI.iconBase + 'icons/1pt5a.gif') image.setAttribute('alt', 'Edit SPARQL query') image.addEventListener('click', sparqlButtonPressed, false) return image } */ // Generate the control bar displayed at the top of the screen. function generateControlBar() { var result = doc.createElement('table'); result.setAttribute('class', 'toolbar'); var tr = doc.createElement('tr'); /* @@ Add in later -- not debugged yet var sparqlButtonDiv = doc.createElement("td") sparqlButtonDiv.appendChild(generateSparqlButton()) tr.appendChild(sparqlButtonDiv) */ typeSelectorDiv = doc.createElement('td'); tr.appendChild(typeSelectorDiv); addColumnDiv = doc.createElement('td'); tr.appendChild(addColumnDiv); result.appendChild(tr); return result; } // Add the SELECT details to the query being built. function addSelectToQuery(query, type) { var selectedColumns = type.getColumns(); for (var i = 0; i < selectedColumns.length; ++i) { // TODO: autogenerate nicer names for variables // variables have to be unambiguous var variable = kb.variable('_col' + i); query.vars.push(variable); selectedColumns[i].setVariable(variable); } } // Add WHERE details to the query being built. function addWhereToQuery(query, rowVar, type) { var queryType = type.type; if (!queryType) { queryType = kb.variable('_any'); } // _row a type query.pat.add(rowVar, UI.ns.rdf('type'), queryType); } // Generate OPTIONAL column selectors. function addColumnsToQuery(query, rowVar, type) { var selectedColumns = type.getColumns(); for (var i = 0; i < selectedColumns.length; ++i) { var column = selectedColumns[i]; var formula = kb.formula(); formula.add(rowVar, column.predicate, column.getVariable()); query.pat.optional.push(formula); } } // Generate a query object from the currently-selected type // object. function generateQuery(type) { var query = new rdf.Query(); var rowVar = kb.variable(keyVariable.slice(1)); // don't pass '?' addSelectToQuery(query, type); addWhereToQuery(query, rowVar, type); addColumnsToQuery(query, rowVar, type); return query; } // Build the contents of the tableDiv element, filtered according // to the specified type. function buildFilteredTable(type) { // Generate "add column" cell. clearElement(addColumnDiv); addColumnDiv.appendChild(generateColumnAddDropdown(type)); var query = generateQuery(type); updateTable(query, type); } function updateTable(query, type) { // Stop the previous query from doing any updates. if (lastQuery) { lastQuery.running = false; } // Render the HTML table. var htmlTable = renderTableForQuery(query, type); // Clear the tableDiv element, and replace with the new table. clearElement(tableDiv); tableDiv.appendChild(htmlTable); // Save the query for the edit dialog. lastQuery = query; } // Remove all subelements of the specified element. function clearElement(element) { while (element.childNodes.length > 0) { element.removeChild(element.childNodes[0]); } } // A SubjectType is created for each rdf:type discovered. function SubjectType(type) { this.type = type; this.columns = null; this.allColumns = []; this.useCount = 0; // Get a list of all columns used by this type. this.getAllColumns = function () { return this.allColumns; }; // Get a list of the current columns used by this type // (subset of allColumns) this.getColumns = function () { // The first time through, get a list of all the columns // and select only the six most popular columns. if (!this.columns) { var allColumns = this.getAllColumns(); this.columns = allColumns.slice(0, 7); } return this.columns; }; // Get a list of unused columns this.getUnusedColumns = function () { var allColumns = this.getAllColumns(); var columns = this.getColumns(); var result = []; for (var i = 0; i < allColumns.length; ++i) { if (columns.indexOf(allColumns[i]) === -1) { result.push(allColumns[i]); } } return result; }; this.addColumn = function (column) { this.columns.push(column); }; this.removeColumn = function (column) { this.columns = this.columns.filter(function (x) { return x !== column; }); }; this.getLabel = function () { return utils.label(this.type); }; this.addUse = function () { this.useCount += 1; }; } // Class representing a column in the table. function Column() { this.useCount = 0; // Have we checked any values for this column yet? this.checkedAnyValues = false; // If the range is unknown, but we just get literals in this // column, then we can generate a literal selector. this.possiblyLiteral = true; // If the range is unknown, but we just get literals and they // match the regular expression for numbers, we can generate // a number selector. this.possiblyNumber = true; // We accumulate classes which things in the column must be a member of this.constraints = []; // Check values as they are read. If we don't know what the // range is, we might be able to infer that it is a literal // if all of the values are literals. Similarly, we might // be able to determine if the literal values are actually // numbers (using regexps). this.checkValue = function (term) { var termType = term.termType; if (this.possiblyLiteral && termType !== 'Literal' && termType !== 'NamedNode') { this.possiblyNumber = false; this.possiblyLiteral = false; } else if (this.possiblyNumber) { if (termType !== 'Literal') { this.possiblyNumber = false; } else { var literalValue = term.value; if (!literalValue.match(/^-?\d+(\.\d*)?$/)) { this.possiblyNumber = false; } } } this.checkedAnyValues = true; }; this.getVariable = function () { return this.variable; }; this.setVariable = function (variable) { this.variable = variable; }; this.getKey = function () { return this.variable.toString(); }; this.addUse = function () { this.useCount += 1; }; this.getHints = function () { if (options && options.hints && this.variable && options.hints[this.variable.toNT()]) { return options.hints[this.variable.toNT()]; } return {}; }; this.getLabel = function () { if (this.getHints().label) { return this.getHints().label; } if (this.predicate) { if (this.predicate.sameTerm(ns.rdf('type')) && this.superClass) { return utils.label(this.superClass, true); // do initial cap } return utils.label(this.predicate); } else if (this.variable) { return this.variable.toString(); } else { return 'unlabeled column?'; } }; this.setPredicate = function (predicate, inverse, other) { if (inverse) { // variable is in the subject pos this.inverse = predicate; this.constraints = this.constraints.concat(kb.each(predicate, UI.ns.rdfs('domain'))); if (predicate.sameTerm(ns.rdfs('subClassOf')) && other.termType === 'NamedNode') { this.superClass = other; this.alternatives = kb.each(undefined, ns.rdfs('subClassOf'), other); } } else { // variable is the object this.predicate = predicate; this.constraints = this.constraints.concat(kb.each(predicate, UI.ns.rdfs('range'))); } }; this.getConstraints = function () { return this.constraints; }; this.filterFunction = function () { return true; }; this.sortKey = function () { return this.getLabel().toLowerCase(); }; this.isImageColumn = function () { for (var i = 0; i < this.constraints.length; i++) { if (this.constraints[i].uri in IMAGE_TYPES) return true; } return false; }; } // Convert an object to an array. function objectToArray(obj, filter) { var result = []; for (var property in obj) { // @@@ have to guard against methods var value = obj[property]; if (!filter || filter(property, value)) { result.push(value); } } return result; } // Generate an <option> in a drop-down list. function optionElement(label, value) { var result = doc.createElement('option'); result.setAttribute('value', value); result.appendChild(doc.createTextNode(label)); return result; } // Generate drop-down list box for choosing type of data displayed function generateTypeSelector(allType, types) { var resultDiv = doc.createElement('div'); resultDiv.appendChild(doc.createTextNode('Select type: ')); var dropdown = doc.createElement('select'); dropdown.appendChild(optionElement('All types', 'null')); for (var uri in types) { dropdown.appendChild(optionElement(types[uri].getLabel(), uri)); } dropdown.addEventListener('click', function () { var type; if (dropdown.value === 'null') { type = allType; } else { type = types[dropdown.value]; } typeSelectorChanged(type); }, false); resultDiv.appendChild(dropdown); return resultDiv; } // Callback invoked when the type selector drop-down list is changed. function typeSelectorChanged(selectedType) { buildFilteredTable(selectedType); } // Build drop-down list to add a new column function generateColumnAddDropdown(type) { var resultDiv = doc.createElement('div'); var unusedColumns = type.getUnusedColumns(); unusedColumns.sort(function (a, b) { var aLabel = a.sortKey(); var bLabel = b.sortKey(); return (aLabel > bLabel) - (aLabel < bLabel); }); // If there are no unused columns, the div is empty. if (unusedColumns.length > 0) { resultDiv.appendChild(doc.createTextNode('Add column: ')); // Build dropdown list of unused columns. var dropdown = doc.createElement('select'); dropdown.appendChild(optionElement('', '-1')); for (var i = 0; i < unusedColumns.length; ++i) { var column = unusedColumns[i]; dropdown.appendChild(optionElement(column.getLabel(), '' + i)); } resultDiv.appendChild(dropdown); // Invoke callback when the dropdown is changed, to add // the column and reload the table. dropdown.addEventListener('click', function () { var columnIndex = Number(dropdown.value); if (columnIndex >= 0) { type.addColumn(unusedColumns[columnIndex]); buildFilteredTable(type); } }, false); } return resultDiv; } // Find the column for a given variable function getColumnForVariable(columns, variableNT) { for (var predicateUri in columns) { var column = columns[predicateUri]; if (column.variable.toNT() === variableNT) { return column; } } throw new Error("getColumnForVariable: no column for variable ".concat(variableNT)); // return null } // Find the column for a given predicate, creating a new column object // if necessary. function getColumnForPredicate(columns, predicate) { var column; if (predicate.uri in columns) { column = columns[predicate.uri]; } else { column = new Column(); column.setPredicate(predicate); columns[predicate.uri] = column; } return column; } // Find a type by its URI, creating a new SubjectType object if // necessary. function getTypeForObject(types, type) { var subjectType; if (type.uri in types) { subjectType = types[type.uri]; } else { subjectType = new SubjectType(type); types[type.uri] = subjectType; } return subjectType; } // Discover types and subjects for search. function discoverTypes() { // rdf:type properties of subjects, indexed by URI for the type. var types = {}; // Get a list of statements that match: ? rdfs:type ? // From this we can get a list of subjects and types. var subjectList = kb.statementsMatching(undefined, UI.ns.rdf('type'), tableClass, // can be undefined OR sourceDocument); // can be undefined // Subjects for later lookup. This is a mapping of type URIs to // lists of subjects (it is necessary to record the type of // a subject). var subjects = {}; for (var i = 0; i < subjectList.length; ++i) { var type = subjectList[i].object; if (type.termType !== 'NamedNode') { // @@ no bnodes? continue; } var typeObj = getTypeForObject(types, type); if (!(type.uri in subjects)) { subjects[type.uri] = []; } subjects[type.uri].push(subjectList[i].subject); typeObj.addUse(); } return [subjects, types]; } // Get columns for the given subject. function getSubjectProperties(subject, columns) { // Get a list of properties of this subject. var properties = kb.statementsMatching(subject, undefined, undefined, sourceDocument); var result = {}; for (var j = 0; j < properties.length; ++j) { var predicate = properties[j].predicate; if (predicate.uri in FORBIDDEN_COLUMNS) { continue; } // Find/create a column for this predicate. var column = getColumnForPredicate(columns, predicate); column.checkValue(properties[j].object); result[predicate.uri] = column; } return result; } // Identify the columns associated with a type. function identifyColumnsForType(type, subjects) { var allColumns = {}; // Process each subject of this type to build up the // column list. for (var i = 0; i < subjects.length; ++i) { var columns = getSubjectProperties(subjects[i], allColumns); for (var predicateUri in columns) { var column = columns[predicateUri]; column.addUse(); } } // Generate the columns list var allColumnsList = objectToArray(allColumns); sortColumns(allColumnsList); type.allColumns = allColumnsList; } // Build table information from parsing RDF statements. function calculateTable() { // Find the types that we will display in the dropdown // list box, and associated objects of those types. var subjects, types; var s = discoverTypes(); // eslint-disable-next-line prefer-const subjects = s[0]; // eslint-disable-next-line prefer-const types = s[1]; // no [ ] on LHS for (var typeUrl in subjects) { var subjectList = subjects[typeUrl]; var type = types[typeUrl]; identifyColumnsForType(type, subjectList); } // TODO: Special type that captures all rows. // Combine columns from all types var allType = new SubjectType(null); return [allType, objectToArray(types)]; } // Sort the list of columns by the most common columns. function sortColumns(columns) { function sortFunction(a, b) { return (a.useCount < b.useCount) - (a.useCount > b.useCount); } columns.sort(sortFunction); } // Create the delete button for a column. function renderColumnDeleteButton(type, column) { var button = doc.createElement('a'); button.appendChild(doc.createTextNode('[x]')); button.addEventListener('click', function () { type.removeColumn(column); buildFilteredTable(type); }, false); return button; } // Render the table header for the HTML table. function renderTableHeader(columns, type) { var tr = doc.createElement('tr'); /* Empty header for link column */ var linkTd = doc.createElement('th'); tr.appendChild(linkTd); for (var i = 0; i < columns.length; ++i) { var th = doc.createElement('th'); var column = columns[i]; th.appendChild(doc.createTextNode(column.getLabel())); // We can only add a delete button if we are using the // proper interface and have a type to delete from: if (type) { th.appendChild(renderColumnDeleteButton(type, column)); } tr.appendChild(th); } return tr; } // Sort the rows in the rendered table by data from a specific // column, using the provided sort function to compare values. function applyColumnSort(rows, column, sortFunction, reverse) { var columnKey = column.getKey(); // Sort the rows array. rows.sort(function (row1, row2) { var row1Value = null; var row2Value = null; if (columnKey in row1.values) { row1Value = row1.values[columnKey][0]; } if (columnKey in row2.values) { row2Value = row2.values[columnKey][0]; } var result = sortFunction(row1Value, row2Value); if (reverse) { return -result; } else { return result; } }); // Remove all rows from the table: if (rows.length) { var parentTable = rows[0]._htmlRow.parentNode; for (var i = 0; i < rows.length; ++i) { parentTable.removeChild(rows[i]._htmlRow); } // Add back the rows in the new sorted order: for (var _i = 0; _i < rows.length; ++_i) { parentTable.appendChild(rows[_i]._htmlRow); } } } // Filter the list of rows based on the selectors for the // columns. function applyColumnFiltersToRow(row, columns) { var rowDisplayed = true; // Check the filter functions for every column. // The row should only be displayed if the filter functions // for all of the columns return true. for (var c = 0; c < columns.length; ++c) { var column = columns[c]; var columnKey = column.getKey(); var columnValue = null; if (columnKey in row.values) { columnValue = row.values[columnKey][0]; } if (!column.filterFunction(columnValue)) { rowDisplayed = false; break; } } // Show or hide the HTML row according to the result // from the filter function. var htmlRow = row._htmlRow; if (rowDisplayed) { htmlRow.style.display = ''; } else { htmlRow.style.display = 'none'; } } // Filter the list of rows based on the selectors for the // columns. function applyColumnFilters(rows, columns) { // Apply filterFunction to each row. for (var r = 0; r < rows.length; ++r) { var row = rows[r]; applyColumnFiltersToRow(row, columns); } } // /////////////////////////////////// Literal column handling // Sort by literal value function literalSort(rows, column, reverse) { function literalToString(colValue) { if (colValue) { if (colValue.termType === 'Literal') { return colValue.value.toLowerCase(); } else if (colValue.termType === 'NamedNode') { return utils.label(colValue).toLowerCase(); } return colValue.value.toLowerCase(); } else { return ''; } } function literalCompare(value1, value2) { var strValue1 = literalToString(value1); var strValue2 = literalToString(value2); if (strValue1 < strValue2) { return -1; } else if (strValue1 > strValue2) { return 1; } else { return 0; } } applyColumnSort(rows, column, literalCompare, reverse); } // Generates a selector for an RDF literal column. function renderLiteralSelector(rows, columns, column) { var result = doc.createElement('div'); var textBox = doc.createElement('input'); textBox.setAttribute('type', 'text'); textBox.style.width = '70%'; result.appendChild(textBox); var sort1 = doc.createElement('span'); sort1.appendChild(doc.createTextNode("\u25BC")); sort1.addEventListener('click', function () { literalSort(rows, column, false); }, false); result.appendChild(sort1); var sort2 = doc.createElement('span'); sort2.appendChild(doc.createTextNode("\u25B2")); sort2.addEventListener('click', function () { literalSort(rows, column, true); }, false); result.appendChild(sort2); var substring = null; // Filter the table to show only rows that have a particular // substring in the specified column. column.filterFunction = function (colValue) { if (!substring) { return true; } else if (!colValue) { return false; } else { var literalValue; if (colValue.termType === 'Literal') { literalValue = colValue.value; } else if (colValue.termType === 'NamedNode') { literalValue = utils.label(colValue); } else { literalValue = ''; } return literalValue.toLowerCase().indexOf(substring) >= 0; } }; textBox.addEventListener('keyup', function () { if (textBox.value !== '') { substring = textBox.value.toLowerCase(); } else { substring = null; } applyColumnFilters(rows, columns); }, false); return result; } // /////////////////////////////////// Enumeration // Generates a dropdown selector for enumeration types include // // @param rows, // @param columns, the mapping of predictae URIs to columns // @param column, // @param list, List of alternative terms // function renderEnumSelector(rows, columns, column, list) { var doMultiple = true; var result = doc.createElement('div'); var dropdown = doc.createElement('select'); var searchValue = {}; // Defualt to all enabled for (var i = 0; i < list.length; ++i) { var value = list[i]; searchValue[value.uri] = true; } var initialSelection = getHints(column).initialSelection; if (initialSelection) searchValue = initialSelection; if (doMultiple) dropdown.setAttribute('multiple', 'true');else dropdown.appendChild(optionElement('(All)', '-1')); for (var _i2 = 0; _i2 < list.length; ++_i2) { var _value = list[_i2]; var ele = optionElement(utils.label(_value), _i2); if (searchValue[_value.uri]) ele.selected = true; dropdown.appendChild(ele); } result.appendChild(dropdown); // Select based on an enum value. column.filterFunction = function (colValue) { return !searchValue || colValue && searchValue[colValue.uri]; }; dropdown.addEventListener('click', function () { if (doMultiple) { searchValue = {}; var opt = dropdown.options; for (var _i3 = 0; _i3 < opt.length; _i3++) { var option = opt[_i3]; var index = Number(option.value); if (opt[_i3].selected) searchValue[list[index].uri] = true; } } else { var _index = Number(dropdown.value); // adjusted in Standard tweaks 2018-01 if (_index < 0) { searchValue = null; } else { searchValue = {}; searchValue[list[_index].uri] = true; } } applyColumnFilters(rows, columns); }, true); return result; } // //////////////////////////////////// Numeric // // Selector for XSD number types. function renderNumberSelector(rows, columns, column) { var result = doc.createElement('div'); var minSelector = doc.createElement('input'); minSelector.setAttribute('type', 'text'); minSelector.style.width = '40px'; result.appendChild(minSelector); var maxSelector = doc.createElement('input'); maxSelector.setAttribute('type', 'text'); maxSelector.style.width = '40px'; result.appendChild(maxSelector); // Select based on minimum/maximum limits. var min = null; var max = null; column.filterFunction = function (colValue) { if (colValue) { colValue = Number(colValue); } if (min && (!colValue || colValue < min)) { return false; } if (max && (!colValue || colValue > max)) { return false; } return true; }; // When the values in the boxes are changed, update the // displayed columns. function eventListener() { if (minSelector.value === '') { min = null; } else { min = Number(minSelector.value); } if (maxSelector.value === '') { max = null; } else { max = Number(maxSelector.value); } applyColumnFilters(rows, columns); } minSelector.addEventListener('keyup', eventListener, false); maxSelector.addEventListener('keyup', eventListener, false); return result; } // ///////////////////////////////////////////////////////////////// // Fallback attempts at generating a selector if other attempts fail. function fallbackRenderTableSelector(rows, columns, column) { // Have all values matched as numbers? if (column.checkedAnyValues && column.possiblyNumber) { return renderNumberSelector(rows, columns, column); } // Have all values been literals? if (column.possiblyLiteral) { return renderLiteralSelector(rows, columns, column); } return null; } // Render a selector for a given row. function renderTableSelector(rows, columns, column) { // What type of data is in this column? Check the constraints for // this predicate. // If this is a class which can be one of various sibling classes? if (column.superClass && column.alternatives.length > 0) { return renderEnumSelector(rows, columns, column, column.alternatives); } var cs = column.getConstraints(); var range; for (var i = 0; i < cs.length; i++) { range = cs[i]; // Is this a number type? // Alternatively, is this an rdf:Literal type where all of // the values match as numbers? if (column.checkedAnyValues && column.possiblyNumber || range.uri in XSD_NUMBER_TYPES) { return renderNumberSelector(rows, columns, column); } // rdf:Literal? Assume a string at this point if (range.uri === RDFS_LITERAL) { return renderLiteralSelector(rows, columns, column); } // Is this an enumeration type? // Also ToDo: @@@ Handle membership of classes whcih are disjointUnions var choices = kb.each(range, UI.ns.owl('oneOf')); if (choices.length > 0) { return renderEnumSelector(rows, columns, column, choices.elements); } } return fallbackRenderTableSelector(rows, columns, column); } // Generate the search selectors for the table columns. function renderTableSelectors(rows, columns) { var tr = doc.createElement('tr'); tr.className = 'selectors'; // Empty link column tr.appendChild(doc.createElement('td')); // Generate selectors. for (var i = 0; i < columns.length; ++i) { var td = doc.createElement('td'); var selector = renderTableSelector(rows, columns, columns[i]); if (selector) { td.appendChild(selector); } /* // Useful debug: display URI of predicate in column header if (columns[i].predicate.uri) { td.appendChild(document.createTextNode(columns[i].predicate.uri)) } */ tr.appendChild(td); } return tr; } function linkTo(uri, linkText, hints) { hints = hints || {}; var result = doc.createElement('a'); var linkFunction = hints.linkFunction; result.setAttribute('href', uri); result.appendChild(doc.createTextNode(linkText)); if (!linkFunction) { result.addEventListener('click', UI.widgets.openHrefInOutlineMode, true); } else { result.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); var target = utils.getTarget(e); var uri = target.getAttribute('href'); if (!uri) debug.log('No href found \n'); linkFunction(uri); }, true); } return result; } function linkToObject(obj, hints) { var match = false; if (obj.uri) { match = obj.uri.match(/^mailto:(.*)/); } if (match) { return linkTo(obj.uri, match[1], hints); } else { return linkTo(obj.uri, utils.label(obj), hints); } } // Render an image function renderImage(obj) { var result = doc.createElement('img'); result.setAttribute('src', obj.uri); // Set the height, so it appears as a thumbnail. result.style.height = '40px'; return result; } // Render an individual RDF object to an HTML object displayed // in a table cell. function getHints(column) { if (options && options.hints && column.variable && options.hints[column.variable.toNT()]) { return options.hints[column.variable.toNT()]; } return {}; } function renderValue(obj, column) { // hint var hints = getHints(column); var cellFormat = hints.cellFormat; if (cellFormat) { switch (cellFormat) { case 'shortDate': return doc.createTextNode(UI.widgets.shortDate(obj.value)); // break default: // drop through } } else { if (obj.termType === 'Literal') { if (obj.datatype) { if (XSD_DATE_TYPES[obj.datatype.uri]) { return doc.createTextNode(UI.widgets.shortDate(obj.value)); } else if (XSD_NUMBER_TYPES[obj.datatype.uri]) { var span = doc.createElement('span'); span.textContent = obj.value; span.setAttribute('style', 'text-align: right'); return span; } } return doc.createTextNode(obj.value); } else if (obj.termType === 'NamedNode' && column.isImageColumn()) { return renderImage(obj); } else if (obj.termType === 'NamedNode' || obj.termType === 'BlankNode') { return linkToObject(obj, hints); } else if (obj.termType === 'Collection') { var _span = doc.createElement('span'); _span.appendChild(doc.createTextNode('[')); obj.elements.forEach(function (x) { _span.appendChild(renderValue(x, column)); _span.appendChild(doc.createTextNode(', ')); }); _span.removeChild(_span.lastChild); _span.appendChild(doc.createTextNode(']')); return _span; } else { return doc.createTextNode("unknown termtype '" + obj.termType + "'!"); } } } // Render a row of the HTML table, from the given row structure. // Note that unlike other functions, this renders into a provided // row (<tr>) element. function renderTableRowInto(tr, row, columns, _downstream) { /* Link column, for linking to this subject. */ var linkTd = doc.createElement('td'); if (row._subject && 'uri' in row._subject) { linkTd.appendChild(linkTo(row._subject.uri, "\u2192")); } tr.appendChild(linkTd); // Create a <td> for each column (whether the row has data for that // column or not). for (var i = 0; i < columns.length; ++i) { var column = columns[i]; var td = doc.createElement('td'); var orig; var columnKey = column.getKey(); if (columnKey in row.values) { var objects = row.values[columnKey]; var different = false; if (row.originalValues && row.originalValues[columnKey]) { if (objects.length !== row.originalValues[columnKey].length) { different = true; } } for (var j = 0; j < objects.length; ++j) { var obj = objects[j]; if (row.originalValues && row.originalValues[columnKey] && row.originalValues[columnKey].length > j) { orig = row.originalValues[columnKey][j]; if (obj.toString() !== orig.toString()) { different = true; } } td.appendChild(renderValue(obj, column)); if (j !== objects.length - 1) { td.appendChild(doc.createTextNode(',\n')); } if (different) { td.style.background = '#efe'; // green = new changed } } } tr.appendChild(td); } // Save a reference to the HTML row in the row object. row._htmlRow = tr; return tr; } // Check if a value is already stored in the list of values for // a cell (the query can sometimes find it multiple times) function valueInList(value, list) { var key = null; if (value.termType === 'Literal') { key = 'value'; } else if (value.termType === 'NamedNode') { key = 'uri'; } else { return list.indexOf(value) >= 0; } // Check the list and compare keys: var i; for (i = 0; i < list.length; ++i) { if (list[i].termType === value.termType && list[i][key] === value[key]) { return true; } } // Not found? return false; } // Update a row, add new values, and regenerate the HTML element // containing the values. function updateRow(row, columns, values) { var key; var needUpdate = false; for (key in values) { var value = values[key]; // If this key is not already in the row, create a new entry // for it: if (!(key in row.values)) { row.values[key] = []; } // Possibly add this new value to the list, but don't // add it if we have already added it: if (!valueInList(value, row.values[key])) { row.values[key].push(value); needUpdate = true; } } // Regenerate the HTML row? if (needUpdate) { clearElement(row._htmlRow); renderTableRowInto(row._htmlRow, row, columns); } applyColumnFiltersToRow(row, columns); // Hide immediately if nec } // Get a unique ID for the given subject. This is normally the // URI; if the subject has no URI, a unique ID is assigned. function getSubjectId(subject) { if ('uri' in subject) { return subject.uri; } else if ('_subject_id' in subject) { return subject._subject_id; } else { var result = '' + subjectIdCounter; subject._subject_id = result; ++subjectIdCounter; return result; } } // Run a query and populate the table. // Populates also an array of logical rows. This will be empty when the function // first returns (as the query is performed in the background) function runQuery(query, rows, columns, table) { query.running = true; var startTime = Date.now(); var progressMessage = doc.createElement('tr'); table.appendChild(progressMessage); progressMessage.textContent = 'Loading ...'; for (var i = 0; i < rows.length; i++) { rows[i].original = true; if (!rows[i].originalValues) { // remember first set rows[i].originalValues = rows[i].values; } rows[i].values = {}; // oldStyle = rows[i]._htmlRow.getAttribute('style') || '' // rows[i]._htmlRow.style.background = '#ffe'; //setAttribute('style', ' background-color: #ffe;')// yellow } var onResult = function onResult(values) { if (!query.running) { return; } progressMessage.textContent += '.'; // give a progress bar var row = null; var rowKey = null; var rowKeyId; // If the query has a row key, use it to look up the row. if (keyVariable in values) { rowKey = values[keyVariable]; rowKeyId = getSubjectId(rowKey); // Do we have a row for this already? // If so, reuse it; otherwise, we must create a new row. if (rowKeyId in rowsLookup) { row = rowsLookup[rowKeyId]; } } // Create a new row? if (!row) { var tr = doc.createElement('tr'); table.appendChild(tr); row = { _htmlRow: tr, _subject: rowKey, values: {} }; rows.push(row); if (rowKey) { rowsLookup[rowKeyId] = row; } } // Add the new values to this row. delete row.original; // This is included in the new data updateRow(row, columns, values); }; var onDone = function onDone() { if (progressMessage && progressMessage.parentNode && progressMessage.parentNode.removeChild) { progressMessage.parentNode.removeChild(progressMessage); progressMessage = null; } var elapsedTimeMS = Date.now() - startTime; debug.log('Query done: ' + rows.length + ' rows, ' + elapsedTimeMS + 'ms'); // Delete rows which were from old values not new for (var _i4 = rows.length - 1; _i4 >= 0; _i4--) { // backwards if (rows[_i4].original) { debug.log(' deleting row ' + rows[_i4]._subject); var tr = rows[_i4]._htmlRow; tr.parentNode.removeChild(tr); delete rowsLookup[getSubjectId(rows[_i4]._subject)]; rows.splice(_i4, 1); } } if (options.sortBy) { // @@ for each column check needs sorting var column = getColumnForVariable(columns, options.sortBy); literalSort(rows, column, options.sortReverse); } if (options.onDone) options.onDone(resultDiv); // return div makes testing easier }; kb.query(query, onResult, undefined, onDone); } // Given the formula object which is the query pattern, // deduce from where the variable occurs constraints on // what values it can take. function inferColumnsFromFormula(columns, formula) { UI.log.debug('>> processing formula'); for (var i = 0; i < formula.statements.length; ++i) { var statement = formula.statements[i]; // UI.log.debug("processing statement " + i) // Does it match this?: // <something> <predicate> ?var // If so, we can use the predicate as the predicate for the // column used for the specified variable. if (statement.predicate.termType === 'NamedNode' && statement.object.termType === 'Variable') { var variable = statement.object.toString(); if (variable in columns) { var column = columns[variable]; column.setPredicate(statement.predicate, false, statement.subject); } } if (statement.predicate.termType === 'NamedNode' && statement.subject.termType === 'Variable') { var _variable = statement.subject.toString(); if (_variable in columns) { var _column = columns[_variable]; _column.setPredicate(statement.predicate, true, statement.object); } } } // Apply to OPTIONAL formulas: for (var _i5 = 0; _i5 < formula.optional.length; ++_i5) { UI.log.debug('recurse to optional subformula ' + _i5); inferColumnsFromFormula(columns, formula.optional[_i5]); } UI.log.debug('<< finished processing formula'); } // Generate a list of column structures and infer details about the // predicates based on the contents of the query function inferColumns(query) { // Generate the columns list: var result = []; var columns = {}; for (var i = 0; i < query.vars.length; ++i) { var column = new Column(); var queryVar = query.vars[i]; UI.log.debug('column ' + i + ' : ' + queryVar); column.setVariable(queryVar); columns[queryVar] = column; result.push(column); } inferColumnsFromFormula(columns, query.pat); return result; } // Generate a table from a query. function renderTableForQuery(query, type) { // infer columns from query, to allow generic queries var columns; if (!givenQuery) { columns = type.getColumns(); } else { columns = inferColumns(query); } // Start with an empty list of rows; this will be populated // by the query. var rows = []; // Create table element and header. var table = doc.createElement('table'); table.appendChild(renderTableHeader(columns, type)); table.appendChild(renderTableSelectors(rows, columns)); // Run query. Note that this is perform asynchronously; the // query runs in the background and this call does not block. table.logicalRows = rows; // Save for refresh table.columns = columns; table.query = query; runQuery(query, rows, columns, table); return table; } // Find the most common type of row function getMostCommonType(types) { var bestCount = -1; var best = null; var typeUri; for (typeUri in types) { var type = types[typeUri]; if (type.useCount > bestCount) { best = type; bestCount = type.useCount; } } return best; } // Filter list of columns to only those columns used in the // specified rows. /* function filterColumns (columns, rows) { var filteredColumns = {} // Copy columns from "columns" -> "filteredColumns", but only // those columns that are used in the list of rows specified. for (let columnUri in columns) { for (let i = 0; i < rows.length; ++i) { if (columnUri in rows[i]) { filteredColumns[columnUri] = columns[columnUri] break } } } return filteredColumns } */ } // /////////////////////////////////////////////////////////////////// // ENDS //# sourceMappingURL=table.js.map