UNPKG

solid-ui

Version:

UI library for Solid applications

1,280 lines (1,278 loc) 47 kB
/* Buttons */ import { st, sym, uri, Util } from 'rdflib'; import { icons } from '../iconBase'; import ns from '../ns'; import { style } from '../style'; import * as debug from '../debug'; import { info } from '../log'; import { uploadFiles, makeDraggable, makeDropTarget } from './dragAndDrop'; import { store } from 'solid-logic'; import * as utils from '../utils'; import { errorMessageBlock } from './error'; import { addClickListenerToElement, createImageDiv, wrapDivInATR } from './widgetHelpers'; import { linkIcon, createLinkForURI } from './buttons/iconLinks'; /** * UI Widgets such as buttons * @packageDocumentation */ /* global alert */ const { iconBase } = icons; const cancelIconURI = iconBase + 'noun_1180156.svg'; // black X const checkIconURI = iconBase + 'noun_1180158.svg'; // green checkmark; Continue function getStatusArea(context) { let box = (context && context.statusArea) || (context && context.div) || null; if (box) return box; let dom = context && context.dom; if (!dom && typeof document !== 'undefined') { dom = document; } if (dom) { const body = dom.getElementsByTagName('body')[0]; box = dom.createElement('div'); body.insertBefore(box, body.firstElementChild); if (context) { context.statusArea = box; } return box; } return null; } /** * Display an error message block */ export function complain(context, err) { if (!err) return; // only if error const ele = getStatusArea(context); debug.log('Complaint: ' + err); if (ele) ele.appendChild(errorMessageBlock((context && context.dom) || document, err)); else alert(err); } /** * Remove all the children of an HTML element */ export function clearElement(ele) { while (ele.firstChild) { ele.removeChild(ele.firstChild); } return ele; } /** * To figure out the log URI from the full URI used to invoke the reasoner */ export function extractLogURI(fullURI) { const logPos = fullURI.search(/logFile=/); const rulPos = fullURI.search(/&rulesFile=/); return fullURI.substring(logPos + 8, rulPos); } /** * By default, converts e.g. '2020-02-19T19:35:28.557Z' to '19:35' * if today is 19 Feb 2020, and to 'Feb 19' if not. * @@@ TODO This needs to be changed to local time * @param noTime Return a string like 'Feb 19' even if it's today. */ export function shortDate(str, noTime) { if (!str) return '???'; const month = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; try { const nowZ = new Date().toISOString(); // var nowZ = $rdf.term(now).value // var n = now.getTimezoneOffset() // Minutes if (str.slice(0, 10) === nowZ.slice(0, 10) && !noTime) { return str.slice(11, 16); } if (str.slice(0, 4) === nowZ.slice(0, 4)) { return (month[parseInt(str.slice(5, 7), 10) - 1] + ' ' + parseInt(str.slice(8, 10), 10)); } return str.slice(0, 10); } catch (e) { return 'shortdate:' + e; } } /** * Format a date and time * @param date for instance `new Date()` * @param format for instance '{FullYear}-{Month}-{Date}T{Hours}:{Minutes}:{Seconds}.{Milliseconds}' * @returns for instance '2000-01-15T23:14:23.002' */ export function formatDateTime(date, format) { return format .split('{') .map(function (s) { const k = s.split('}')[0]; const width = { Milliseconds: 3, FullYear: 4 }; const d = { Month: 1 }; return s ? ('000' + (date['get' + k]() + (d[k] || 0))).slice(-(width[k] || 2)) + s.split('}')[1] : ''; }) .join(''); } /** * Get a string representation of the current time * @returns for instance '2000-01-15T23:14:23.002' */ export function timestamp() { return formatDateTime(new Date(), '{FullYear}-{Month}-{Date}T{Hours}:{Minutes}:{Seconds}.{Milliseconds}'); } /** * Get a short string representation of the current time * @returns for instance '23:14:23.002' */ export function shortTime() { return formatDateTime(new Date(), '{Hours}:{Minutes}:{Seconds}.{Milliseconds}'); } // ///////////////////// Handy UX widgets /** * Sets the best name we have and looks up a better one */ export function setName(element, x) { const kb = store; const findName = function (x) { const name = kb.any(x, ns.vcard('fn')) || kb.any(x, ns.foaf('name')) || kb.any(x, ns.vcard('organization-name')); return name ? name.value : null; }; const name = x.sameTerm(ns.foaf('Agent')) ? 'Everyone' : findName(x); element.textContent = name || utils.label(x); if (!name && x.uri) { if (!kb.fetcher) { throw new Error('kb has no fetcher'); } // Note this is only a fetch, not a lookUP of all sameAs etc kb.fetcher.nowOrWhenFetched(x.doc(), undefined, function (_ok) { element.textContent = findName(x) || utils.label(x); // had: (ok ? '' : '? ') + }); } } /** * Set of suitable images * See also [[findImage]] * @param x The thing for which we want to find an image * @param kb The RDF store to look in * @returns It goes looking for triples in `kb`, * `(subject: x), (predicate: see list below) (object: image-url)` * to find any image linked from the thing with one of the following * predicates (in order): * * ns.sioc('avatar') * * ns.foaf('img') * * ns.vcard('logo') * * ns.vcard('hasPhoto') * * ns.vcard('photo') * * ns.foaf('depiction') */ export function imagesOf(x, kb) { return kb .each(x, ns.sioc('avatar')) .concat(kb.each(x, ns.foaf('img'))) .concat(kb.each(x, ns.vcard('logo'))) .concat(kb.each(x, ns.vcard('hasPhoto'))) .concat(kb.each(x, ns.vcard('photo'))) .concat(kb.each(x, ns.foaf('depiction'))); } /** * Best logo or avatar or photo etc to represent someone or some group etc */ export const iconForClass = { // Potentially extendable by other apps, panes, etc // Relative URIs to the iconBase 'solid:AppProviderClass': 'noun_144.svg', // @@ classs name should not contain 'Class' 'solid:AppProvider': 'noun_15177.svg', // @@ 'solid:Pod': 'noun_Cabinet_1434380.svg', 'vcard:Group': 'noun_339237.svg', 'vcard:Organization': 'noun_143899.svg', 'vcard:Individual': 'noun_15059.svg', 'schema:Person': 'noun_15059.svg', 'foaf:Person': 'noun_15059.svg', 'foaf:Agent': 'noun_98053.svg', 'acl:AuthenticatedAgent': 'noun_99101.svg', 'prov:SoftwareAgent': 'noun_Robot_849764.svg', // Bot 'vcard:AddressBook': 'noun_15695.svg', 'trip:Trip': 'noun_581629.svg', 'meeting:LongChat': 'noun_1689339.svg', 'meeting:Meeting': 'noun_66617.svg', 'meeting:Project': 'noun_1036577.svg', 'ui:Form': 'noun_122196.svg', 'rdfs:Class': 'class-rectangle.svg', // For RDF developers 'rdf:Property': 'property-diamond.svg', 'owl:Ontology': 'noun_classification_1479198.svg', 'wf:Tracker': 'noun_122196.svg', 'wf:Task': 'noun_17020_gray-tick.svg', 'wf:Open': 'noun_17020_sans-tick.svg', 'wf:Closed': 'noun_17020.svg' }; /** * Returns the origin of the URI of a NamedNode */ function tempSite(x) { // use only while one in rdflib fails with origins 2019 const str = x.uri.split('#')[0]; const p = str.indexOf('//'); if (p < 0) throw new Error('This URI does not have a web site part (origin)'); const q = str.indexOf('/', p + 2); if (q < 0) { // no third slash? return str.slice(0) + '/'; // Add slash to a bare origin } else { return str.slice(0, q + 1); } } /** * Find an image for this thing as a class */ export function findImageFromURI(x) { const iconDir = iconBase; // Special cases from URI scheme: if (typeof x !== 'string' && x.uri) { if (x.uri.split('/').length === 4 && !x.uri.split('/')[1] && !x.uri.split('/')[3]) { return iconDir + 'noun_15177.svg'; // App -- this is an origin } // Non-HTTP URI types imply types if (x.uri.startsWith('message:') || x.uri.startsWith('mid:')) { // message: is apple bug-- should be mid: return iconDir + 'noun_480183.svg'; // envelope noun_567486 } if (x.uri.startsWith('mailto:')) { return iconDir + 'noun_567486.svg'; // mailbox - an email desitination } // For HTTP(s) documents, we could look at the MIME type if we know it. if (x.uri.startsWith('https:') && x.uri.indexOf('#') < 0) { return tempSite(x) + 'favicon.ico'; // was x.site().uri + ... // Todo: make the document icon a fallback for if the favicon does not exist // todo: pick up a possible favicon for the web page itself from a link // was: return iconDir + 'noun_681601.svg' // document - under solid assumptions } return null; } return iconDir + 'noun_10636_grey.svg'; // Grey Circle - some thing } /** * Find something we have as explicit image data for the thing * See also [[imagesOf]] * @param thing The thing for which we want to find an image * @returns The URL of a globe icon if thing equals `ns.foaf('Agent')` * or `ns.rdf('Resource')`. Otherwise, it goes looking for * triples in `store`, * `(subject: thing), (predicate: see list below) (object: image-url)` * to find any image linked from the thing with one of the following * predicates (in order): * * ns.sioc('avatar') * * ns.foaf('img') * * ns.vcard('logo') * * ns.vcard('hasPhoto') * * ns.vcard('photo') * * ns.foaf('depiction') */ export function findImage(thing) { const kb = store; const iconDir = iconBase; if (thing.sameTerm(ns.foaf('Agent')) || thing.sameTerm(ns.rdf('Resource'))) { return iconDir + 'noun_98053.svg'; // Globe } const image = kb.any(thing, ns.sioc('avatar')) || kb.any(thing, ns.foaf('img')) || kb.any(thing, ns.vcard('logo')) || kb.any(thing, ns.vcard('hasPhoto')) || kb.any(thing, ns.vcard('photo')) || kb.any(thing, ns.foaf('depiction')); return image ? image.uri : null; } /** * Do the best you can with the data available * * @return {Boolean} Are we happy with this icon? * Sets src AND STYLE of the image. */ function trySetImage(element, thing, iconForClassMap) { const kb = store; const explitImage = findImage(thing); if (explitImage) { element.setAttribute('src', explitImage); return true; } // This is one of the classes we know about - the class itself? const typeIcon = iconForClassMap[thing.uri]; if (typeIcon) { element.setAttribute('src', typeIcon); element.style = style.classIconStyle; // element.style.border = '0.1em solid green;' // element.style.backgroundColor = '#eeffee' // pale green return true; } const schemeIcon = findImageFromURI(thing); if (schemeIcon) { element.setAttribute('src', schemeIcon); return true; // happy with this -- don't look it up } // Do we have a generic icon for something in any class its in? const types = kb.findTypeURIs(thing); for (const typeURI in types) { if (iconForClassMap[typeURI]) { element.setAttribute('src', iconForClassMap[typeURI]); return false; // maybe we can do better } } element.setAttribute('src', iconBase + 'noun_10636_grey.svg'); // Grey Circle - some thing return false; // we can do better } /** * ToDo: Also add icons for *properties* like home, work, email, range, domain, comment, */ export function setImage(element, thing) { const kb = store; const iconForClassMap = {}; for (const k in iconForClass) { const pref = k.split(':')[0]; const id = k.split(':')[1]; const theClass = ns[pref](id); iconForClassMap[theClass.uri] = uri.join(iconForClass[k], iconBase); } const happy = trySetImage(element, thing, iconForClassMap); if (!happy && thing.uri) { if (!kb.fetcher) { throw new Error('kb has no fetcher'); } kb.fetcher.nowOrWhenFetched(thing.doc(), undefined, (ok) => { if (ok) { trySetImage(element, thing, iconForClassMap); } }); } } // If a web page, then a favicon, with a fallback to ??? // See, e.g., http://stackoverflow.com/questions/980855/inputting-a-default-image export function faviconOrDefault(dom, x) { const image = dom.createElement('img'); image.style = style.iconStyle; const isOrigin = function (x) { if (!x.uri) return false; const parts = x.uri.split('/'); return parts.length === 3 || (parts.length === 4 && parts[3] === ''); }; image.setAttribute('src', iconBase + (isOrigin(x) ? 'noun_15177.svg' : 'noun_681601.svg') // App symbol vs document ); if (x.uri && x.uri.startsWith('https:') && x.uri.indexOf('#') < 0) { const res = dom.createElement('object'); // favico with a fallback of a default image if no favicon res.setAttribute('data', tempSite(x) + 'favicon.ico'); res.setAttribute('type', 'image/x-icon'); res.appendChild(image); // fallback return res; } else { setImage(image, x); return image; } } /* Two-option dialog pop-up */ function renderDeleteConfirmPopup(dom, refererenceElement, prompt, deleteFunction) { function removePopup() { refererenceElement.parentElement.removeChild(refererenceElement); } function removePopupAndDoDeletion() { removePopup(); deleteFunction(); } const popup = dom.createElement('div'); popup.style = style.confirmPopupStyle; popup.style.position = 'absolute'; popup.style.top = '-1em'; // try leaving original button clear popup.style.display = 'grid'; popup.style.gridTemplateColumns = 'auto auto'; const affirm = dom.createElement('div'); affirm.style.gridColumn = '1/2'; affirm.style.gridRow = '1'; // @@ sigh; TS. could pass number in fact const cancel = dom.createElement('div'); cancel.style.gridColumn = '1/2'; cancel.style.gridRow = '2'; const xButton = cancelButton(dom, removePopup); popup.appendChild(xButton); xButton.style.gridColumn = '1'; xButton.style.gridRow = '2'; const cancelPrompt = popup.appendChild(dom.createElement('button')); cancelPrompt.style = style.buttonStyle; cancelPrompt.style.gridRow = '2'; cancelPrompt.style.gridColumn = '2'; cancelPrompt.textContent = 'Cancel'; // @@ I18n const affirmIcon = button(dom, icons.iconBase + 'noun_925021.svg', 'Delete it'); // trashcan popup.appendChild(affirmIcon); affirmIcon.style.gridRow = '1'; affirmIcon.style.gridColumn = '1'; const sureButtonElt = popup.appendChild(dom.createElement('button')); sureButtonElt.style = style.buttonStyle; sureButtonElt.style.gridRow = '1'; sureButtonElt.style.gridColumn = '2'; sureButtonElt.textContent = prompt; popup.appendChild(sureButtonElt); affirmIcon.addEventListener('click', removePopupAndDoDeletion); sureButtonElt.addEventListener('click', removePopupAndDoDeletion); // xButton.addEventListener('click', removePopup) cancelPrompt.addEventListener('click', removePopup); return popup; } /** * Delete button with a check you really mean it * @@ Supress check if command key held down? */ export function deleteButtonWithCheck(dom, container, noun, deleteFunction) { function createPopup() { const refererenceElement = dom.createElement('div'); container.insertBefore(refererenceElement, deleteButton); refererenceElement.style.position = 'relative'; // Needed as reference for popup refererenceElement.appendChild(renderDeleteConfirmPopup(dom, refererenceElement, prompt, deleteFunction)); } const minusIconURI = iconBase + 'noun_2188_red.svg'; // white minus in red #cc0000 circle const deleteButton = dom.createElement('img'); deleteButton.setAttribute('src', minusIconURI); deleteButton.setAttribute('style', style.smallButtonStyle); // @@tsc - would set deleteButton.style deleteButton.style.float = 'right'; // Historically this has alwaus floated right const prompt = 'Remove this ' + noun; deleteButton.title = prompt; // @@ In an ideal world, make use of hover an accessibility option deleteButton.classList.add('hoverControlHide'); deleteButton.addEventListener('click', createPopup); container.classList.add('hoverControl'); container.appendChild(deleteButton); deleteButton.setAttribute('data-testid', 'deleteButtonWithCheck'); return deleteButton; // or button div? caller may change size of image } /* Make a button * * @param dom - the DOM document object * @Param iconURI - the URI of the icon to use (if any) * @param text - the tooltip text or possibly button contents text * @param handler <function> - A handler to called when button is clicked * * @returns <dDomElement> - the button */ export function button(dom, iconURI, text, handler, options = { buttonColor: 'Primary', needsBorder: false }) { const button = dom.createElement('button'); button.setAttribute('type', 'button'); // button.innerHTML = text // later, user preferences may make text preferred for some if (iconURI) { const img = button.appendChild(dom.createElement('img')); img.setAttribute('src', iconURI); img.setAttribute('style', 'width: 2em; height: 2em;'); // trial and error. 2em disappears img.title = text; button.setAttribute('style', style.buttonStyle); } else { button.textContent = text.toLocaleUpperCase(); button.onmouseover = function () { if (options.buttonColor === 'Secondary') { if (options.needsBorder) { button.setAttribute('style', style.secondaryButtonNoBorderHover); } else { button.setAttribute('style', style.secondaryButtonHover); } } else { if (options.needsBorder) { button.setAttribute('style', style.primaryButtonNoBorderHover); } else { button.setAttribute('style', style.primaryButtonHover); } } }; button.onmouseout = function () { if (options.buttonColor === 'Secondary') { if (options.needsBorder) { button.setAttribute('style', style.secondaryButtonNoBorder); } else { button.setAttribute('style', style.secondaryButton); } } else { if (options.needsBorder) { button.setAttribute('style', style.primaryButtonNoBorder); } else { button.setAttribute('style', style.primaryButton); } } }; if (options.buttonColor === 'Secondary') { if (options.needsBorder) { button.setAttribute('style', style.secondaryButtonNoBorder); } else { button.setAttribute('style', style.secondaryButton); } } else { if (options.needsBorder) { button.setAttribute('style', style.primaryButtonNoBorder); } else { button.setAttribute('style', style.primaryButton); } } } if (handler) { button.addEventListener('click', handler, false); } return button; } /* Make a cancel button * * @param dom - the DOM document object * @param handler <function> - A handler to called when button is clicked * * @returns <dDomElement> - the button */ export function cancelButton(dom, handler) { const b = button(dom, cancelIconURI, 'Cancel', handler); if (b.firstChild) { // sigh for tsc b.firstChild.style.opacity = '0.3'; // Black X is too harsh: current language is grey X } return b; } /* Make a continue button * * @param dom - the DOM document object * @param handler <function> - A handler to called when button is clicked * * @returns <dDomElement> - the button */ export function continueButton(dom, handler) { return button(dom, checkIconURI, 'Continue', handler); } /* Grab a name for a new thing * * Form to get the name of a new thing before we create it * @params theClass Misspelt to avoid clashing with the JavaScript keyword * @returns: a promise of (a name or null if cancelled) */ export function askName(dom, kb, container, predicate, theClass, noun) { return new Promise(function (resolve, _reject) { const form = dom.createElement('div'); // form is broken as HTML behaviour can resurface on js error // classLabel = utils.label(ns.vcard('Individual')) predicate = predicate || ns.foaf('name'); // eg 'name' in user's language noun = noun || (theClass ? utils.label(theClass) : ' '); // eg 'folder' in users's language const prompt = noun + ' ' + utils.label(predicate) + ': '; form.appendChild(dom.createElement('p')).textContent = prompt; const namefield = dom.createElement('input'); namefield.setAttribute('type', 'text'); namefield.setAttribute('size', '100'); namefield.setAttribute('maxLength', '2048'); // No arbitrary limits namefield.setAttribute('style', style.textInputStyle); namefield.select(); // focus next user input form.appendChild(namefield); container.appendChild(form); // namefield.focus() function gotName() { form.parentNode.removeChild(form); resolve(namefield.value.trim()); } namefield.addEventListener('keyup', function (e) { if (e.keyCode === 13) { gotName(); } }, false); form.appendChild(dom.createElement('br')); form.appendChild(cancelButton(dom, function (_event) { form.parentNode.removeChild(form); resolve(null); })); form.appendChild(continueButton(dom, function (_event) { gotName(); })); namefield.focus(); }); // Promise } /** * A TR to represent a draggable person, etc in a list * * pred is unused param at the moment */ export const personTR = renderAsRow; // The legacy name is used in a lot of places export function renderAsRow(dom, pred, obj, options) { const tr = dom.createElement('tr'); options = options || {}; // tr.predObj = [pred.uri, obj.uri] moved to acl-control const td1 = tr.appendChild(dom.createElement('td')); const td2 = tr.appendChild(dom.createElement('td')); const td3 = tr.appendChild(dom.createElement('td')); // const image = td1.appendChild(dom.createElement('img')) const image = options.image || faviconOrDefault(dom, obj); td1.setAttribute('style', 'vertical-align: middle; width:2.5em; padding:0.5em; height: 2.5em;'); td2.setAttribute('style', 'vertical-align: middle; text-align:left;'); td3.setAttribute('style', 'vertical-align: middle; width:2em; padding:0.5em; height: 4em;'); td1.appendChild(image); if (options.title) { td2.textContent = options.title; } else { setName(td2, obj); // This is async } if (options.deleteFunction) { deleteButtonWithCheck(dom, td3, options.noun || 'one', options.deleteFunction); } if (obj.uri) { // blank nodes need not apply if (options.link !== false) { const anchor = td3.appendChild(linkIcon(dom, obj)); anchor.classList.add('HoverControlHide'); td3.appendChild(dom.createElement('br')); } if (options.draggable !== false) { // default is on image.setAttribute('draggable', 'false'); // Stop the image being dragged instead - just the TR makeDraggable(tr, obj); } } ; tr.subject = obj; return tr; } /* A helper function for renderAsDiv * creates the NameDiv for the person * Note: could not move it to the helper file because they call exported functions * from buttons * @internal exporting this only for unit tests */ export function createNameDiv(dom, div, title, obj) { const nameDiv = div.appendChild(dom.createElement('div')); if (title) { nameDiv.textContent = title; } else { setName(nameDiv, obj); // This is async } } /* A helper function for renderAsDiv * creates the linkDiv for the person * Note: could not move it to the helper file because they call exported functions * from buttons * @internal exporting this only for unit tests */ export function createLinkDiv(dom, div, obj, options) { const linkDiv = div.appendChild(dom.createElement('div')); linkDiv.setAttribute('style', style.linkDivStyle); if (options.deleteFunction) { deleteButtonWithCheck(dom, linkDiv, options.noun || 'one', options.deleteFunction); } if (obj.uri) { // blank nodes need not apply if (options.link !== false) { createLinkForURI(dom, linkDiv, obj); } makeDraggable(div, obj); } } /** * A Div to represent a draggable person, etc in a list * configurable to add an onClick listener */ export function renderAsDiv(dom, obj, options) { const div = dom.createElement('div'); div.setAttribute('style', style.renderAsDivStyle); options = options || {}; const image = options.image || faviconOrDefault(dom, obj); createImageDiv(dom, div, image); createNameDiv(dom, div, options.title, obj); createLinkDiv(dom, div, obj, options); if (options.clickable && options.onClickFunction) { addClickListenerToElement(div, options.onClickFunction); } // to be compatible with the SolidOS table layout if (options.wrapInATR) { const tr = wrapDivInATR(dom, div, obj); return tr; } return div; } /** * Refresh a DOM tree recursively */ export function refreshTree(root) { if (root.refresh) { root.refresh(); return; } for (let i = 0; i < root.children.length; i++) { refreshTree(root.children[i]); } } /** * Component that displays a list of resources, for instance * the attachments of a message, or the various documents related * to a meeting. * Accepts dropping URLs onto it to add attachments to it. */ export function attachmentList(dom, subject, div, options = {}) { // options = options || {} const deleteAttachment = function (target) { if (!kb.updater) { throw new Error('kb has no updater'); } kb.updater.update(st(subject, predicate, target, doc), [], function (uri, ok, errorBody, _xhr) { if (ok) { refresh(); } else { complain(undefined, 'Error deleting one: ' + errorBody); } }); }; function createNewRow(target) { const theTarget = target; const opt = { noun }; if (modify) { opt.deleteFunction = function () { deleteAttachment(theTarget); }; } return personTR(dom, predicate, target, opt); } const refresh = function () { const things = kb.each(subject, predicate); things.sort(); utils.syncTableToArray(attachmentTable, things, createNewRow); }; function droppedURIHandler(uris) { const ins = []; uris.forEach(function (u) { const target = sym(u); // Attachment needs text label to disinguish I think not icon. debug.log('Dropped on attachemnt ' + u); // icon was: iconBase + 'noun_25830.svg' ins.push(st(subject, predicate, target, doc)); }); if (!kb.updater) { throw new Error('kb has no updater'); } kb.updater.update([], ins, function (uri, ok, errorBody, _xhr) { if (ok) { refresh(); } else { complain(undefined, 'Error adding one: ' + errorBody); } }); } function droppedFileHandler(files) { var _a, _b; uploadFiles(kb.fetcher, files, (_a = options.uploadFolder) === null || _a === void 0 ? void 0 : _a.uri, // Files (_b = options.uploadFolder) === null || _b === void 0 ? void 0 : _b.uri, // Pictures function (theFile, destURI) { const ins = [st(subject, predicate, kb.sym(destURI), doc)]; if (!kb.updater) { throw new Error('kb has no updater'); } kb.updater.update([], ins, function (uri, ok, errorBody, _xhr) { if (ok) { refresh(); } else { complain(undefined, 'Error adding link to uploaded file: ' + errorBody); } }); }); } const doc = options.doc || subject.doc(); if (options.modify === undefined) options.modify = true; const modify = options.modify; const promptIcon = options.promptIcon || iconBase + 'noun_748003.svg'; // target // const promptIcon = options.promptIcon || (iconBase + 'noun_25830.svg') // paperclip const predicate = options.predicate || ns.wf('attachment'); const noun = options.noun || 'attachment'; const kb = store; const attachmentOuter = div.appendChild(dom.createElement('table')); attachmentOuter.setAttribute('style', 'margin-top: 1em; margin-bottom: 1em;'); const attachmentOne = attachmentOuter.appendChild(dom.createElement('tr')); const attachmentLeft = attachmentOne.appendChild(dom.createElement('td')); const attachmentRight = attachmentOne.appendChild(dom.createElement('td')); const attachmentTable = attachmentRight.appendChild(dom.createElement('table')); attachmentTable.appendChild(dom.createElement('tr')) // attachmentTableTop ; attachmentOuter.refresh = refresh; // Participate in downstream changes // ;(attachmentTable as any).refresh = refresh <- outer should be best? refresh(); if (modify) { // const buttonStyle = 'width; 2em; height: 2em; margin: 0.5em; padding: 0.1em;' const paperclip = button(dom, promptIcon, 'Drop attachments here'); // paperclip.style = buttonStyle // @@ needed? default has white background attachmentLeft.appendChild(paperclip); const fhandler = options.uploadFolder ? droppedFileHandler : null; makeDropTarget(paperclip, droppedURIHandler, fhandler); // beware missing the wire of the paparclip! makeDropTarget(attachmentLeft, droppedURIHandler, fhandler); // just the outer won't do it if (options.uploadFolder) { // Addd an explicit file upload button as well const buttonDiv = fileUploadButtonDiv(dom, droppedFileHandler); attachmentLeft.appendChild(buttonDiv); // buttonDiv.children[1].style = buttonStyle } } return attachmentOuter; } // ///////////////////////////////////////////////////////////////////////////// /** * Event Handler for links within solid apps. * * Note that native links have constraints in Firefox, they * don't work with local files for instance (2011) */ export function openHrefInOutlineMode(e) { e.preventDefault(); e.stopPropagation(); const target = utils.getTarget(e); const uri = target.getAttribute('href'); if (!uri) return debug.log('openHrefInOutlineMode: No href found!\n'); const dom = window.document; if (dom.outlineManager) { // @@ TODO Remove the use of document as a global object // TODO fix dependency cycle to solid-panes by calling outlineManager ; dom.outlineManager.GotoSubject(store.sym(uri), true, undefined, true, undefined); } else if (window && window.panes && window.panes.getOutliner) { // @@ TODO Remove the use of window as a global object ; window.panes .getOutliner() .GotoSubject(store.sym(uri), true, undefined, true, undefined); } else { debug.log('ERROR: Can\'t access outline manager in this config'); } // dom.outlineManager.GotoSubject(store.sym(uri), true, undefined, true, undefined) } /** * Make a URI in the Tabulator.org annotation store out of the URI of the thing to be annotated. * * @@ Todo: make it a personal preference. */ export function defaultAnnotationStore(subject) { if (subject.uri === undefined) return undefined; let s = subject.uri; if (s.slice(0, 7) !== 'http://') return undefined; s = s.slice(7); // Remove const hash = s.indexOf('#'); if (hash >= 0) s = s.slice(0, hash); // Strip trailing else { const slash = s.lastIndexOf('/'); if (slash < 0) return undefined; s = s.slice(0, slash); } return store.sym('http://tabulator.org/wiki/annnotation/' + s); } /** * Retrieve all RDF class URIs from solid-ui's RDF store * @returns an object `ret` such that `Object.keys(ret)` is * the list of all class URIs. */ export function allClassURIs() { const set = {}; store .statementsMatching(undefined, ns.rdf('type'), undefined) .forEach(function (st) { if (st.object.value) set[st.object.value] = true; }); store .statementsMatching(undefined, ns.rdfs('subClassOf'), undefined) .forEach(function (st) { if (st.object.value) set[st.object.value] = true; if (st.subject.value) set[st.subject.value] = true; }); store .each(undefined, ns.rdf('type'), ns.rdfs('Class')) .forEach(function (c) { if (c.value) set[c.value] = true; }); return set; } /** * Figuring which properties we know about * * When the user inputs an RDF property, like for a form field * or when specifying the relationship between two arbitrary things, * then er can prompt them with properties the session knows about * * TODO: Look again by catching this somewhere. (On the kb?) * TODO: move to diff module? Not really a button. * @param {Store} kb The quadstore to be searched. */ export function propertyTriage(kb) { const possibleProperties = {}; // if (possibleProperties === undefined) possibleProperties = {} // const kb = store const dp = {}; const op = {}; let no = 0; let nd = 0; let nu = 0; const pi = kb.predicateIndex; // One entry for each pred for (const p in pi) { const object = pi[p][0].object; if (object.termType === 'Literal') { dp[p] = true; nd++; } else { op[p] = true; no++; } } // If nothing discovered, then could be either: const ps = kb.each(undefined, ns.rdf('type'), ns.rdf('Property')); for (let i = 0; i < ps.length; i++) { const p = ps[i].toNT(); if (!op[p] && !dp[p]) { dp[p] = true; op[p] = true; nu++; } } possibleProperties.op = op; possibleProperties.dp = dp; info(`propertyTriage: ${no} non-lit, ${nd} literal. ${nu} unknown.`); return possibleProperties; } /** * General purpose widgets */ /** * A button for jumping */ export function linkButton(dom, object) { const b = dom.createElement('button'); b.setAttribute('type', 'button'); b.textContent = 'Goto ' + utils.label(object); b.addEventListener('click', function (_event) { // b.parentNode.removeChild(b) // TODO fix dependency cycle to solid-panes by calling outlineManager ; dom.outlineManager.GotoSubject(object, true, undefined, true, undefined); }, true); return b; } /** * A button to remove some other element from the page */ export function removeButton(dom, element) { const b = dom.createElement('button'); b.setAttribute('type', 'button'); b.textContent = '✕'; // MULTIPLICATION X b.addEventListener('click', function (_event) { ; element.parentNode.removeChild(element); }, true); return b; } // Description text area // // Make a box to demand a description or display existing one // // @param dom - the document DOM for the user interface // @param kb - the graph which is the knowledge base we are working with // @param subject - a term, the subject of the statement(s) being edited. // @param predicate - a term, the predicate of the statement(s) being edited // @param store - The web document being edited // @param callbackFunction - takes (boolean ok, string errorBody) // /////////////////////////////////////// Random I/O widgets ///////////// // //// Column Header Buttons // // These are for selecting different modes, sources,styles, etc. // /* buttons.headerButtons = function (dom, kb, name, words) { const box = dom.createElement('table') var i, word, s = '<tr>' box.setAttribute('style', 'width: 90%; height: 1.5em') for (i=0; i<words.length; i++) { s += '<td><input type="radio" name="' + name + '" id="' + words[i] + '" value=' } box.innerHTML = s + '</tr>' } */ // //////////////////////////////////////////////////////////// // // selectorPanel // // A vertical panel for selecting connections to left or right. // // @param inverse means this is the object rather than the subject // export function selectorPanel(dom, kb, type, predicate, inverse, possible, options, callbackFunction, linkCallback) { return selectorPanelRefresh(dom.createElement('div'), dom, kb, type, predicate, inverse, possible, options, callbackFunction, linkCallback); } export function selectorPanelRefresh(list, dom, kb, type, predicate, inverse, possible, options, callbackFunction, linkCallback) { const style0 = 'border: 0.1em solid #ddd; border-bottom: none; width: 95%; height: 2em; padding: 0.5em;'; let selected = null; list.innerHTML = ''; const refreshItem = function (box, x) { // Scope to hold item and x let item; // eslint-disable-next-line prefer-const let image; const setStyle = function () { const already = inverse ? kb.each(undefined, predicate, x) : kb.each(x, predicate); iconDiv.setAttribute('class', already.length === 0 ? 'hideTillHover' : ''); // See tabbedtab.css image.setAttribute('src', options.connectIcon || iconBase + 'noun_25830.svg'); image.setAttribute('title', already.length ? already.length : 'attach'); }; const f = index.twoLine.widgetForClass(type); // eslint-disable-next-line prefer-const item = f(dom, x); item.setAttribute('style', style0); const nav = dom.createElement('div'); nav.setAttribute('class', 'hideTillHover'); // See tabbedtab.css nav.setAttribute('style', 'float:right; width:10%'); const a = dom.createElement('a'); a.setAttribute('href', x.uri); a.setAttribute('style', 'float:right'); nav.appendChild(a).textContent = '>'; box.appendChild(nav); const iconDiv = dom.createElement('div'); iconDiv.setAttribute('style', (inverse ? 'float:left;' : 'float:right;') + ' width:30px;'); image = dom.createElement('img'); setStyle(); iconDiv.appendChild(image); box.appendChild(iconDiv); item.addEventListener('click', function (event) { if (selected === item) { // deselect item.setAttribute('style', style0); selected = null; } else { if (selected) selected.setAttribute('style', style0); item.setAttribute('style', style0 + 'background-color: #ccc; color:black;'); selected = item; } callbackFunction(x, event, selected === item); setStyle(); }, false); image.addEventListener('click', function (event) { linkCallback(x, event, inverse, setStyle); }, false); box.appendChild(item); return box; }; for (let i = 0; i < possible.length; i++) { const box = dom.createElement('div'); list.appendChild(box); refreshItem(box, possible[i]); } return list; } // ########################################################################### // // Small compact views of things // export let index = {}; // /////////////////////////////////////////////////////////////////////////// // We need these for anything which is a subject of an attachment. // // These should be moved to type-dependeent UI code. Related panes maybe function twoLineDefault(dom, x) { // Default const box = dom.createElement('div'); box.textContent = utils.label(x); return box; } /** * Find a function that can create a widget for a given class * @param c The RDF class for which we want a widget generator function */ function twoLineWidgetForClass(c) { let widget = index.twoLine[c.uri]; const kb = store; if (widget) return widget; const sup = kb.findSuperClassesNT(c); for (const cl in sup) { widget = index.twoLine[kb.fromNT(cl).uri]; if (widget) return widget; } return index.twoLine['']; } /** * Display a transaction * @param x Should have attributes through triples in store: * * ns.qu('payee') -> a named node * * ns.qu('date) -> a literal * * ns.qu('amount') -> a literal */ function twoLineTransaction(dom, x) { let failed = ''; const enc = function (p) { const y = store.any(x, ns.qu(p)); if (!y) failed += '@@ No value for ' + p + '! '; return y ? utils.escapeForXML(y.value) : '?'; // @@@@ }; const box = dom.createElement('table'); box.innerHTML = ` <tr> <td colspan="2"> ${enc('payee')}</td> < /tr> < tr > <td>${enc('date').slice(0, 10)}</td> <td style = "text-align: right;">${enc('amount')}</td> </tr>`; if (failed) { box.innerHTML = ` <tr> <td><a href="${utils.escapeForXML(x.uri)}">${utils.escapeForXML(failed)}</a></td> </tr>`; } return box; } /** * Display a trip * @param x Should have attributes through triples in store: * * ns.dc('title') -> a literal * * ns.cal('dtstart') -> a literal * * ns.cal('dtend') -> a literal */ function twoLineTrip(dom, x) { const enc = function (p) { const y = store.any(x, p); return y ? utils.escapeForXML(y.value) : '?'; }; const box = dom.createElement('table'); box.innerHTML = ` <tr> <td colspan="2">${enc(ns.dc('title'))}</td> </tr> <tr style="color: #777"> <td>${enc(ns.cal('dtstart'))}</td> <td>${enc(ns.cal('dtend'))}</td> </tr>`; return box; } /** * Stick a stylesheet link the document if not already there */ export function addStyleSheet(dom, href) { const links = dom.querySelectorAll('link'); for (let i = 0; i < links.length; i++) { if ((links[i].getAttribute('rel') || '') === 'stylesheet' && (links[i].getAttribute('href') || '') === href) { return; } } const link = dom.createElement('link'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('type', 'text/css'); link.setAttribute('href', href); dom.getElementsByTagName('head')[0].appendChild(link); } // Figure (or guess) whether this is an image, etc // export function isAudio(file) { return isImage(file, 'audio'); } export function isVideo(file) { return isImage(file, 'video'); } /** * */ export function isImage(file, kind) { const dcCLasses = { audio: 'http://purl.org/dc/dcmitype/Sound', image: 'http://purl.org/dc/dcmitype/Image', video: 'http://purl.org/dc/dcmitype/MovingImage' }; const what = kind || 'image'; // See https://github.com/linkeddata/rdflib.js/blob/e367d5088c/src/formula.ts#L554 // const typeURIs = store.findTypeURIs(file); // See https://github.com/linkeddata/rdflib.js/blob/d5000f/src/utils-js.js#L14 // e.g.'http://www.w3.org/ns/iana/media-types/audio' const prefix = Util.mediaTypeClass(what + '/*').uri.split('*')[0]; for (const t in typeURIs) { if (t.startsWith(prefix)) return true; } if (dcCLasses[what] in typeURIs) return true; return false; } /** * File upload button * @param dom The DOM aka document * @param droppedFileHandler Same handler function as drop, takes array of file objects * @returns {Element} - a div with a button and a inout in it * The input is hidden, as it is uglky - the user clicks on the nice icons and fires the input. */ // See https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications export function fileUploadButtonDiv(dom, droppedFileHandler) { const div = dom.createElement('div'); const input = div.appendChild(dom.createElement('input')); input.setAttribute('type', 'file'); input.setAttribute('multiple', 'true'); input.addEventListener('change', (event) => { debug.log('File drop event: ', event); if (event.files) { droppedFileHandler(event.files); } else if (event.target && event.target.files) { droppedFileHandler(event.target.files); } else { alert('Sorry no files .. internal error?'); } }, false); input.style = 'display:none'; const buttonElt = div.appendChild(button(dom, iconBase + 'noun_Upload_76574_000000.svg', 'Upload files', _event => { input.click(); })); makeDropTarget(buttonElt, null, droppedFileHandler); // Can also just drop on button return div; } index = { line: { // Approx 80em }, twoLine: { '': twoLineDefault, 'http://www.w3.org/2000/10/swap/pim/qif#Transaction': twoLineTransaction, 'http://www.w3.org/ns/pim/trip#Trip': twoLineTrip, widgetForClass: twoLineWidgetForClass } }; //# sourceMappingURL=buttons.js.map