Skip to content

Commit ee1c4ea

Browse files
Add losos.js — 3KB linked data pane framework bundle
1 parent 58d7af2 commit ee1c4ea

1 file changed

Lines changed: 246 additions & 0 deletions

File tree

losos.js

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/**
2+
* LOSOS — Linked Objects OS
3+
* 3KB gzipped linked data pane framework
4+
* https://linkedobjects.org
5+
* Copyright (c) 2026 Melvin Carvalho. AGPL-3.0-or-later.
6+
*/
7+
(function(root) {
8+
'use strict'
9+
10+
// === LION Store ===
11+
12+
class Store {
13+
constructor() { this.nodes = new Map() }
14+
15+
load(jsonLd) {
16+
const ctx = jsonLd['@context'] || {}
17+
this._index(jsonLd, ctx)
18+
return this
19+
}
20+
21+
_index(obj, ctx, parent) {
22+
if (!obj || typeof obj !== 'object') return
23+
if (Array.isArray(obj)) { obj.forEach(function(o) { this._index(o, ctx, parent) }.bind(this)); return }
24+
25+
var id = obj['@id'] || (parent ? parent + '/' + Math.random().toString(36).slice(2, 6) : '#this')
26+
if (!obj['@id']) obj['@id'] = id
27+
var node = Object.assign({ '@id': id }, obj)
28+
29+
if (node['@type']) node['@type'] = this._expand(node['@type'], ctx)
30+
31+
var keys = Object.keys(node)
32+
for (var i = 0; i < keys.length; i++) {
33+
var key = keys[i], val = node[key]
34+
if (key.charAt(0) === '@') continue
35+
var expanded = this._expand(key, ctx)
36+
if (expanded !== key) { node[expanded] = val; delete node[key] }
37+
if (val && typeof val === 'object' && !Array.isArray(val)) this._index(val, ctx, id)
38+
if (Array.isArray(val)) {
39+
for (var j = 0; j < val.length; j++) {
40+
if (val[j] && typeof val[j] === 'object') this._index(val[j], ctx, id)
41+
}
42+
}
43+
}
44+
this.nodes.set(id, node)
45+
}
46+
47+
_expand(term, ctx) {
48+
var colon = term.indexOf(':')
49+
if (colon === -1) return term
50+
var prefix = term.slice(0, colon), local = term.slice(colon + 1)
51+
if (ctx[prefix]) return ctx[prefix] + local
52+
return term
53+
}
54+
55+
get(id) { return this.nodes.get(id) || null }
56+
57+
prop(id) {
58+
var node = typeof id === 'string' ? this.get(id) : id
59+
if (!node) return undefined
60+
var keys = Array.prototype.slice.call(arguments, 1)
61+
for (var i = 0; i < keys.length; i++) {
62+
var entries = Object.entries(node)
63+
for (var j = 0; j < entries.length; j++) {
64+
var k = entries[j][0], v = entries[j][1]
65+
if (k === '@id' || k === '@type' || k === '@context') continue
66+
if (k.indexOf(keys[i]) !== -1) return v
67+
}
68+
}
69+
return undefined
70+
}
71+
72+
propAll(id, key) {
73+
var val = this.prop(id, key)
74+
if (val === undefined) return []
75+
return Array.isArray(val) ? val : [val]
76+
}
77+
78+
type(id) {
79+
var node = typeof id === 'string' ? this.get(id) : id
80+
return node && node['@type'] || null
81+
}
82+
83+
find(fn) {
84+
var results = []
85+
this.nodes.forEach(function(node) { if (fn(node)) results.push(node) })
86+
return results
87+
}
88+
89+
statementsMatching(subject, predicate, object) {
90+
var node = typeof subject === 'string' ? this.get(subject)
91+
: subject && subject.value ? this.get(subject.value) : null
92+
if (!node) return []
93+
94+
var stmts = []
95+
var entries = Object.entries(node)
96+
for (var i = 0; i < entries.length; i++) {
97+
var key = entries[i][0], val = entries[i][1]
98+
if (key.charAt(0) === '@' && key !== '@type') continue
99+
var predUri = key === '@type'
100+
? 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' : key
101+
var values = Array.isArray(val) ? val : [val]
102+
for (var j = 0; j < values.length; j++) {
103+
var v = values[j]
104+
var obj = typeof v === 'object' && v['@id']
105+
? { termType: 'NamedNode', value: v['@id'] }
106+
: { termType: 'Literal', value: String(v) }
107+
stmts.push({
108+
subject: { termType: 'NamedNode', value: node['@id'] },
109+
predicate: { termType: 'NamedNode', value: predUri },
110+
object: obj
111+
})
112+
}
113+
}
114+
return stmts
115+
}
116+
}
117+
118+
function createStore(source) {
119+
var store = new Store()
120+
if (typeof source === 'string') source = JSON.parse(source)
121+
store.load(source)
122+
return store
123+
}
124+
125+
// === Shell ===
126+
127+
async function loadPanes() {
128+
var panes = []
129+
var els = document.querySelectorAll('script[data-pane]')
130+
for (var i = 0; i < els.length; i++) {
131+
try {
132+
var mod = await import(els[i].src)
133+
if (mod.default && mod.default.render) panes.push(mod.default)
134+
} catch (err) { console.warn('Failed to load pane:', els[i].src, err) }
135+
}
136+
return panes
137+
}
138+
139+
async function loadData() {
140+
var srcEls = document.querySelectorAll('script[type="application/ld+json"][src]')
141+
for (var i = 0; i < srcEls.length; i++) {
142+
try {
143+
var res = await fetch(srcEls[i].src + '?t=' + Date.now(), { cache: 'no-store' })
144+
srcEls[i].textContent = await res.text()
145+
} catch (err) { console.warn('Failed to fetch data:', srcEls[i].src, err) }
146+
}
147+
148+
var store = new Store()
149+
var dataEls = document.querySelectorAll('script[type="application/ld+json"]')
150+
for (var i = 0; i < dataEls.length; i++) {
151+
if (!dataEls[i].textContent.trim()) continue
152+
try { store.load(JSON.parse(dataEls[i].textContent)) }
153+
catch (err) { console.warn('Failed to parse data island:', err) }
154+
}
155+
return store
156+
}
157+
158+
function findSubject(store) {
159+
var hashThis = store.get('#this')
160+
if (hashThis) return { termType: 'NamedNode', value: '#this' }
161+
for (var entry of store.nodes) {
162+
if (entry[1]['@type']) return { termType: 'NamedNode', value: entry[0] }
163+
}
164+
return null
165+
}
166+
167+
function renderTabs(panes, container, subject, store) {
168+
var tabBar = document.createElement('div')
169+
tabBar.id = 'pane-tabs'
170+
tabBar.style.cssText = 'display:flex;gap:0;border-bottom:1px solid rgba(255,255,255,0.08);overflow-x:auto;max-width:960px;margin:0 auto'
171+
172+
var content = document.createElement('div')
173+
content.id = 'pane-container'
174+
content.style.cssText = 'max-width:960px;margin:0 auto'
175+
176+
var selectPane = function(pane, tab) {
177+
content.innerHTML = ''
178+
for (var i = 0; i < tabBar.children.length; i++) {
179+
tabBar.children[i].setAttribute('aria-selected', 'false')
180+
tabBar.children[i].style.borderBottomColor = 'transparent'
181+
tabBar.children[i].style.color = 'rgba(255,255,255,0.5)'
182+
}
183+
tab.setAttribute('aria-selected', 'true')
184+
tab.style.borderBottomColor = '#7c3aed'
185+
tab.style.color = 'rgba(255,255,255,0.9)'
186+
pane.render(subject, store, content)
187+
}
188+
189+
var first = null
190+
for (var i = 0; i < panes.length; i++) {
191+
var pane = panes[i]
192+
try { if (!pane.canHandle(subject, store)) continue }
193+
catch(e) { continue }
194+
195+
var tab = document.createElement('button')
196+
tab.className = 'pane-tab'
197+
tab.setAttribute('aria-selected', 'false')
198+
tab.style.cssText = 'border:none;background:none;padding:10px 18px;cursor:pointer;font:600 0.85em/1.4 -apple-system,sans-serif;color:rgba(255,255,255,0.5);border-bottom:2px solid transparent;white-space:nowrap'
199+
tab.textContent = (pane.icon ? pane.icon + ' ' : '') + pane.label
200+
tab.addEventListener('click', selectPane.bind(null, pane, tab))
201+
tabBar.appendChild(tab)
202+
if (!first) first = { pane: pane, tab: tab }
203+
}
204+
205+
container.appendChild(tabBar)
206+
container.appendChild(content)
207+
if (first) selectPane(first.pane, first.tab)
208+
}
209+
210+
async function boot(el) {
211+
var rootEl = el || document.getElementById('losos') || document.body
212+
var results = await Promise.all([loadPanes(), loadData()])
213+
var panes = results[0], store = results[1]
214+
var subject = findSubject(store)
215+
216+
if (!subject) {
217+
rootEl.innerHTML = '<p style="padding:2em;color:#888">No data found.</p>'
218+
return
219+
}
220+
renderTabs(panes, rootEl, subject, store)
221+
}
222+
223+
// === Export ===
224+
225+
var LOSOS = { Store: Store, createStore: createStore, boot: boot }
226+
227+
// Module export
228+
if (typeof module !== 'undefined' && module.exports) {
229+
module.exports = LOSOS
230+
} else if (typeof define === 'function' && define.amd) {
231+
define(function() { return LOSOS })
232+
}
233+
234+
// Always attach to global
235+
root.LOSOS = LOSOS
236+
237+
// Auto-boot
238+
if (typeof document !== 'undefined' && document.getElementById('losos')) {
239+
if (document.readyState === 'loading') {
240+
document.addEventListener('DOMContentLoaded', function() { boot() })
241+
} else {
242+
boot()
243+
}
244+
}
245+
246+
})(typeof self !== 'undefined' ? self : typeof global !== 'undefined' ? global : this);

0 commit comments

Comments
 (0)