Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/acl/access-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export class AccessGroups {
: 'No RDF type was detected for this URI.'
const error =
`Error: Failed to add access target: ${uri} is not a recognized ACL target type.` +
` Expected one of: vcard:WebID, vcard:Group, foaf:Person, foaf:Agent, solid:AppProvider, solid:AppProviderClass, or recognized ACL classes.` +
' Expected one of: vcard:WebID, vcard:Group, foaf:Person, foaf:Agent, solid:AppProvider, solid:AppProviderClass, or recognized ACL classes.' +
' Hint: try dropping a WebID profile URI, a vcard:Group URI, or a web app origin.' +
typeDetails
debug.error(error)
Expand Down
44 changes: 22 additions & 22 deletions src/pad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class NotepadPart extends HTMLElement {
* @param {NamedNode} author - The author of text being displayed
* @returns {String} The CSS color generated, constrained to be light for a background color
*/
export function lightColorHash(author?: NamedNode): string {
export function lightColorHash (author?: NamedNode): string {
const hash = function (x) {
return x.split('').reduce(function (a, b) {
a = (a << 5) - a + b.charCodeAt(0)
Expand All @@ -66,7 +66,7 @@ export function lightColorHash(author?: NamedNode): string {
* @param {NamedNode} me - person who is logged into the pod
* @param {notepadOptions} options - the options that can be passed in consist of statusArea, exists
*/
export function notepad(
export function notepad (
dom: HTMLDocument,
padDoc: NamedNode,
subject: NamedNode,
Expand Down Expand Up @@ -164,7 +164,7 @@ export function notepad(
const next: any = kb.any(chunk as any, PAD('next'))
if (prev.sameTerm(subject) && next.sameTerm(subject)) {
// Last one
log("You can't delete the only line.")
log('You can\'t delete the only line.')
return
}

Expand Down Expand Up @@ -210,7 +210,7 @@ export function notepad(
}, 1000)
} else {
log(' removePart FAILED ' + chunk + ': ' + errorMessage)
log(" removePart was deleting :'" + del)
log(' removePart was deleting :\'' + del)
setPartStyle(part, 'color: black; background-color: #fdd;') // failed
const res = response
? (response as any).status
Expand All @@ -235,9 +235,9 @@ export function notepad(
updater.update(del, ins as any, function (uri, ok, errorBody) {
if (!ok) {
log(
"Indent change FAILED '" +
'Indent change FAILED \'' +
newIndent +
"' for " +
'\' for ' +
padDoc +
': ' +
errorBody
Expand Down Expand Up @@ -351,11 +351,11 @@ export function notepad(
if (old !== part.lastSent) {
// Non-fatal: log a warning instead of throwing, to avoid crashing the pad UI.
console.warn(
"Out of order, last sent expected '" +
'Out of order, last sent expected \'' +
old +
"' but found '" +
'\' but found \'' +
part.lastSent +
"'"
'\''
)
}
}
Expand All @@ -380,11 +380,11 @@ export function notepad(
log(
' patch FAILED ' +
(xhr as any).status +
" for '" +
' for \'' +
old +
"' -> '" +
'\' -> \'' +
newOne +
"': " +
'\': ' +
errorBody
)
if ((xhr as any).status === 409) {
Expand Down Expand Up @@ -421,7 +421,7 @@ export function notepad(
} else {
clearStatus(true) // upstream
setPartStyle(part) // synced
log(" Patch ok '" + old + "' -> '" + newOne + "' ")
log(' Patch ok \'' + old + '\' -> \'' + newOne + '\' ')

if (part.state === 4) {
// delete me
Expand All @@ -440,10 +440,10 @@ export function notepad(
})
}

part.addEventListener('input', function inputChangeListener(_event) {
part.addEventListener('input', function inputChangeListener (_event) {
// debug.log("input changed "+part.value);
setPartStyle(part, undefined, true) // grey out - not synced
log('Input event state ' + part.state + " value '" + part.value + "'")
log('Input event state ' + part.state + ' value \'' + part.value + '\'')
switch (part.state) {
case 3: // being deleted
return
Expand Down Expand Up @@ -498,7 +498,7 @@ export function notepad(
addListeners(part, chunk)
} else {
setPartStyle(part, 'color: #222; background-color: #fff')
log("Note can't add listeners - not logged in")
log('Note can\'t add listeners - not logged in')
}
return part
}
Expand Down Expand Up @@ -584,7 +584,7 @@ export function notepad(
const consistencyCheck = function () {
const found: { [uri: string]: boolean } = {}
let failed = 0
function complain2(msg) {
function complain2 (msg) {
complain(msg)
failed++
}
Expand Down Expand Up @@ -831,7 +831,7 @@ export function notepad(
*/

// @ignore exporting this only for the unit test
export function getChunks(subject: NamedNode, kb: IndexedFormula) {
export function getChunks (subject: NamedNode, kb: IndexedFormula) {
const chunks: any[] = []
for (
let chunk: any = kb.the(subject, PAD('next'));
Expand All @@ -847,7 +847,7 @@ export function getChunks(subject: NamedNode, kb: IndexedFormula) {
* Encode content to be put in XML or HTML elements
*/
// @ignore exporting this only for the unit test
export function xmlEncode(str) {
export function xmlEncode (str) {
return str.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
}

Expand All @@ -856,7 +856,7 @@ export function xmlEncode(str) {
* @param { } pad - the notepad
* @param {store} pad - the data store
*/
export function notepadToHTML(pad: any, kb: IndexedFormula) {
export function notepadToHTML (pad: any, kb: IndexedFormula) {
const chunks = getChunks(pad, kb)
let html = '<html>\n <head>\n'
const title = kb.anyValue(pad, ns.dct('title'))
Expand All @@ -866,13 +866,13 @@ export function notepadToHTML(pad: any, kb: IndexedFormula) {
html += ' </head>\n <body>\n'
let level = 0

function increaseLevel(indent) {
function increaseLevel (indent) {
for (; level < indent; level++) {
html += '<ul>\n'
}
}

function decreaseLevel(indent) {
function decreaseLevel (indent) {
for (; level > indent; level--) {
html += '</ul>\n'
}
Expand Down
55 changes: 55 additions & 0 deletions src/stories/Modals.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as UI from '../../src/index'

export default {
title: 'Modals',
}

const demoLinks = [
{ label: 'Solid Project', link: 'https://solidproject.org/' },
{ label: 'SolidOS', link: 'https://github.com/solidos' },
{ label: 'W3C', link: 'https://www.w3.org/' },
]

export const ListModal = {
render: () => {
const modal = UI.widgets.createListModal(document, demoLinks, {
withGreyedBackground: true,
ariaLabel: 'Helpful links',
})
modal.style.display = 'block'
return modal
},
name: 'List modal',
}

export const OpenListModal = {
render: () => {
const container = document.createElement('div')

const openButton = document.createElement('button')
openButton.setAttribute('type', 'button')
openButton.textContent = 'Open links modal'

const helper = document.createElement('p')
helper.textContent = 'Open the modal, then press Escape or use the close button.'

openButton.addEventListener('click', () => {
const existing = container.querySelector('.modal')
if (existing) {
existing.remove()
}

const modal = UI.widgets.createListModal(document, demoLinks, {
withGreyedBackground: true,
ariaLabel: 'Helpful links',
})
modal.style.display = 'block'
container.appendChild(modal)
})

container.appendChild(openButton)
container.appendChild(helper)
return container
},
name: 'Open list modal',
}
9 changes: 9 additions & 0 deletions src/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ export const style = { // styleModule
imageDivStyle: 'width:2.5em; padding:0.5em; height: 2.5em;',
linkDivStyle: 'width:2em; padding:0.5em; height: 4em;',

// modals
modalUnorderedListStyle: 'padding: 0 .2em;',
modalListItemStyle: 'list-style: none; box-shadow: 1px 1px 1px 1px #888888; padding: .5em;',
modalAnchorStyle: 'text-decoration: none;',
modalContentStyle: 'display: flex; flex-direction: column; background-color: #fefefe;',
modalCloseStyle: 'background: none; border: 0; padding: 0; color: #aaaaaa; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; cursor: pointer;',
modalCloseStyleHover: 'background: none; border: 0; padding: 0; color: #666666; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; text-decoration: none; cursor: pointer;',
modalCloseStyleFocus: 'background: none; border: 0; padding: 0; color: #666666; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; text-decoration: none; cursor: pointer; outline: 1px solid #888;',

// ACL
aclControlBoxContainer: 'margin: 1em;',
aclControlBoxHeader: 'font-size: 120%; margin: 0 0 1rem;',
Expand Down
1 change: 1 addition & 0 deletions src/widgets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * from './buttons'
export * from './buttons/iconLinks'
export * from './error'
export * from './forms'
export * from './modals'

export * from './forms/autocomplete/autocompleteBar'
export * from './forms/autocomplete/autocompletePicker'
Expand Down
116 changes: 116 additions & 0 deletions src/widgets/modals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
ModalWidgetStyleOptions,
getModalStyle
} from './modalsStyle'
import { style } from '../style'

export type ListItem = {
label: string,
link: string
}

// Two functions that need to be implemented to use the modal.
// When the user clicks the button, open the modal.
/* Click handler on the button to display it.
btn.onclick = function() {
modal.style.display = "block";
}

// When the user clicks anywhere outside of the modal, close it.
// Window click handler so that the modal will close
// even if the user doesn't click close.
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
} */
const closeClickHandler = (modal: HTMLDivElement, returnFocusTo?: Element | null) => {
modal.style.display = 'none'
if (returnFocusTo && returnFocusTo instanceof HTMLElement) {
returnFocusTo.focus()
}
}

const createModal = (dom: HTMLDocument, options: ModalWidgetStyleOptions, returnFocusTo?: Element | null) => {
const modal = dom.createElement('div')
modal.classList.add('modal')
modal.setAttribute('role', 'dialog')
modal.setAttribute('aria-modal', options.withGreyedBackground ? 'true' : 'false')
modal.setAttribute('aria-label', options.ariaLabel || 'List dialog')
modal.setAttribute('tabindex', '-1')
modal.addEventListener('keydown', event => {
if (event.key === 'Escape') {
closeClickHandler(modal, returnFocusTo)
}
})
modal.setAttribute('style', getModalStyle(options))
return modal
}

const createModalContent = (dom: HTMLDocument) => {
const modalContent: HTMLDivElement = dom.createElement('div')
modalContent.setAttribute('style', style.modalContentStyle)
return modalContent
}

const createCloseButton = (dom: HTMLDocument, modal: HTMLDivElement, returnFocusTo?: Element | null) => {
const closeButton: HTMLButtonElement = dom.createElement('button')
closeButton.setAttribute('type', 'button')
closeButton.setAttribute('aria-label', 'Close modal')
closeButton.setAttribute('title', 'Close')
closeButton.textContent = 'x'
closeButton.addEventListener('click', () => closeClickHandler(modal, returnFocusTo))
closeButton.addEventListener('mouseenter', () => {
closeButton.setAttribute('style', style.modalCloseStyleHover)
})
closeButton.addEventListener('mouseleave', () => {
closeButton.setAttribute('style', style.modalCloseStyle)
})
closeButton.addEventListener('focus', () => {
closeButton.setAttribute('style', style.modalCloseStyleFocus)
})
closeButton.addEventListener('blur', () => {
closeButton.setAttribute('style', style.modalCloseStyle)
})
closeButton.setAttribute('style', style.modalCloseStyle)
return closeButton
}

const createListItems = (dom: HTMLDocument, list: ListItem) => {
const li:HTMLLIElement = dom.createElement('li')
li.setAttribute('style', style.modalListItemStyle)
const link: HTMLAnchorElement = dom.createElement('a')
link.setAttribute('style', style.modalAnchorStyle)
link.href = list.link
link.textContent = list.label
li.appendChild(link)
return li
}

const createUnorderedList = (dom: HTMLDocument, listOfLinks: ListItem[]) => {
const ul: HTMLUListElement = dom.createElement('ul')
ul.setAttribute('role', 'list')
ul.setAttribute('style', style.modalUnorderedListStyle)
listOfLinks.forEach(list => {
const li = createListItems(dom, list)
ul.appendChild(li)
})
return ul
}
export const createListModal = (dom: HTMLDocument, listOfLinks: ListItem[], options: ModalWidgetStyleOptions) => {
const returnFocusTo = dom.activeElement
const modal = createModal(dom, options, returnFocusTo)
const modalContent = createModalContent(dom)
const closeButton = createCloseButton(dom, modal, returnFocusTo)
const ul = createUnorderedList(dom, listOfLinks)
modalContent.appendChild(closeButton)
modalContent.appendChild(ul)
modal.appendChild(modalContent)

setTimeout(() => {
if (closeButton.isConnected) {
closeButton.focus()
}
}, 0)

return modal
}
21 changes: 21 additions & 0 deletions src/widgets/modalsStyle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Get the modal styles, based on options such as position and background overlay.
* See https://design.inrupt.com/atomic-core/?cat=Organisms#Modals for modal design guidelines.
*/
export type ModalWidgetStyleOptions = {
topPosition?: string,
leftPosition?: string,
withGreyedBackground?: boolean,
ariaLabel?: string
}

export const getModalStyle = (options: ModalWidgetStyleOptions = {}) => {
const topPosition = (options.topPosition) ? options.topPosition : '50px'
const leftPosition = (options.leftPosition) ? options.leftPosition : '50px'

if (options.withGreyedBackground) {
return 'display: none; position: fixed; z-index: 1; overflow: auto; background-color: rgba(0,0,0,0.4); padding-top: 100px; left: 0; top: 0; width: 100%; height: 100%;'
}

return `display: none; position: fixed; z-index: 1; top: ${topPosition}; left: ${leftPosition}; overflow: auto; background-color: rgba(0,0,0,0.4);`
}
2 changes: 1 addition & 1 deletion test/helpers/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ global.WritableStream = WritableStream

// Node provides MessagePort via worker_threads; jsdom/undici expects it in global scope
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
// Intentionally require worker_threads in Jest setup to expose MessagePort globals.
const { MessageChannel, MessagePort } = require('worker_threads')
global.MessageChannel = MessageChannel
global.MessagePort = MessagePort
Expand Down
Loading
Loading