solid-ui
Version:
UI library for writing Solid read-write-web applications
867 lines (696 loc) • 27.4 kB
JavaScript
;
/** **************
* Notepad Widget
*/
/** @module UI.pad
*/
var $rdf = require('rdflib');
var padModule = module.exports = {};
var UI = {
authn: require('./signin'),
icons: require('./iconBase'),
log: require('./log'),
ns: require('./ns'),
pad: padModule,
rdf: $rdf,
store: require('./store'),
widgets: require('./widgets')
};
var kb = UI.store;
var ns = UI.ns;
var utils = require('./utils');
/** Figure out a random color from my webid
*
* @param {NamedNode} author - The author of text being displayed
* @returns {String} The CSS color generated, constrained to be light for a background color
*/
UI.pad.lightColorHash = function (author) {
var hash = function hash(x) {
return x.split('').reduce(function (a, b) {
a = (a << 5) - a + b.charCodeAt(0);
return a & a;
}, 0);
};
return author && author.uri ? '#' + (hash(author.uri) & 0xffffff | 0xc0c0c0).toString(16) : '#ffffff'; // c0c0c0 forces pale
}; // no id -> white
// Manage participation in this session
//
// This is more general tham the pad.
//
UI.pad.renderPartipants = function (dom, table, padDoc, subject, me, options) {
table.setAttribute('style', 'margin: 0.8em;');
var newRowForParticpation = function newRowForParticpation(parp) {
var person = kb.any(parp, ns.wf('participant'));
var tr;
if (!person) {
tr = dom.createElement('tr');
tr.textContent = '???'; // Don't crash - invalid part'n entry
return tr;
}
var bg = kb.anyValue(parp, ns.ui('backgroundColor')) || 'white';
var block = dom.createElement('div');
block.setAttribute('style', 'height: 1.5em; width: 1.5em; margin: 0.3em; border 0.01em solid #888; background-color: ' + bg);
tr = UI.widgets.personTR(dom, null, person, options);
table.appendChild(tr);
var td = dom.createElement('td');
td.setAttribute('style', 'vertical-align: middle;');
td.appendChild(block);
tr.insertBefore(td, tr.firstChild);
return tr;
};
var syncTable = function syncTable() {
var parps = kb.each(subject, ns.wf('participation')).map(function (parp) {
return [kb.anyValue(parp, UI.ns.cal('dtstart')) || '9999-12-31', parp];
});
parps.sort(); // List in order of joining
var participations = parps.map(function (p) {
return p[1];
});
utils.syncTableToArray(table, participations, newRowForParticpation);
};
table.refresh = syncTable;
syncTable();
return table;
};
/** Record, or find old, Particpation object
*
* A particpaption object is a place to record things specifically about
* subject and the user, such as preferences, start of membership, etc
* @param {Node} subject - The thing in which the participation is happening
* @param {NamedNode} document - Where to record the data
* @param {NamedNode} me - The logged in user
*
*/
UI.pad.participationObject = function (subject, padDoc, me) {
return new Promise(function (resolve, reject) {
if (!me) {
throw new Error('Not user id');
}
var parps = kb.each(subject, ns.wf('participation')).filter(function (pn) {
return kb.holds(pn, ns.wf('participant'), me);
});
if (parps.length > 1) {
throw new Error('Multiple records of your participation');
}
if (parps.length) {
// If I am not already recorded
resolve(parps[0]); // returns the particpation object
} else {
var participation = UI.widgets.newThing(padDoc);
var ins = [UI.rdf.st(subject, ns.wf('participation'), participation, padDoc), UI.rdf.st(participation, ns.wf('participant'), me, padDoc), UI.rdf.st(participation, ns.cal('dtstart'), new Date(), padDoc), UI.rdf.st(participation, ns.ui('backgroundColor'), UI.pad.lightColorHash(me), padDoc)];
kb.updater.update([], ins, function (uri, ok, errorMessage) {
if (!ok) {
reject(new Error('Error recording your partipation: ' + errorMessage));
} else {
resolve(participation);
}
});
resolve(participation);
}
});
};
/** Record my participation and display participants
*
* @param {NamedNode} subject - the thing in which participation is happening
* @param {NamedNode} padDoc - The document into which the particpation should be recorded
* @param {DOMNode} refreshable - A DOM element whose refresh() is to be called if the change works
*
*/
UI.pad.recordParticipation = function (subject, padDoc, refreshable) {
var me = UI.authn.currentUser();
if (!me) return; // Not logged in
var parps = kb.each(subject, ns.wf('participation')).filter(function (pn) {
return kb.holds(pn, ns.wf('participant'), me);
});
if (parps.length > 1) {
throw new Error('Multiple records of your participation');
}
if (parps.length) {
// If I am not already recorded
return parps[0]; // returns the particpation object
} else {
var participation = UI.widgets.newThing(padDoc);
var ins = [UI.rdf.st(subject, ns.wf('participation'), participation, padDoc), UI.rdf.st(participation, ns.wf('participant'), me, padDoc), UI.rdf.st(participation, UI.ns.cal('dtstart'), new Date(), padDoc), UI.rdf.st(participation, ns.ui('backgroundColor'), UI.pad.lightColorHash(me), padDoc)];
kb.updater.update([], ins, function (uri, ok, errorMessage) {
if (!ok) {
throw new Error('Error recording your partipation: ' + errorMessage);
}
if (refreshable && refreshable.refresh) {
refreshable.refresh();
} // UI.pad.renderPartipants(dom, table, padDoc, subject, me, options)
});
return participation;
}
}; // Record my participation and display participants
//
UI.pad.manageParticipation = function (dom, container, padDoc, subject, me, options) {
var table = dom.createElement('table');
container.appendChild(table);
UI.pad.renderPartipants(dom, table, padDoc, subject, me, options);
try {
UI.pad.recordParticipation(subject, padDoc, table);
} catch (e) {
container.appendChild(UI.widgets.errorMessageBlock(dom, 'Error recording your partipation: ' + e)); // Clean up?
}
return table;
};
UI.pad.notepad = function (dom, padDoc, subject, me, options) {
options = options || {};
var exists = options.exists;
var table = dom.createElement('table');
var kb = UI.store;
var ns = UI.ns;
if (me && !me.uri) throw new Error('UI.pad.notepad: Invalid userid');
var updater = UI.store.updater;
var PAD = $rdf.Namespace('http://www.w3.org/ns/pim/pad#');
table.setAttribute('style', 'padding: 1em; overflow: auto; resize: horizontal; min-width: 40em;');
var upstreamStatus = null;
var downstreamStatus = null;
if (options.statusArea) {
var t = options.statusArea.appendChild(dom.createElement('table'));
var tr = t.appendChild(dom.createElement('tr'));
upstreamStatus = tr.appendChild(dom.createElement('td'));
downstreamStatus = tr.appendChild(dom.createElement('td'));
upstreamStatus.setAttribute('style', 'width:50%');
downstreamStatus.setAttribute('style', 'width:50%');
}
var complain = function complain(message, upstream) {
console.log(message);
if (options.statusArea) {
(upstream ? upstreamStatus : downstreamStatus).appendChild(UI.widgets.errorMessageBlock(dom, message, 'pink'));
}
};
var clearStatus = function clearStatus(upsteam) {
if (options.statusArea) {
options.statusArea.innerHTML = '';
}
};
var setPartStyle = function setPartStyle(part, colors, pending) {
var chunk = part.subject;
colors = colors || '';
var baseStyle = 'font-size: 100%; font-family: monospace; width: 100%; border: none; white-space: pre-wrap;';
var headingCore = 'font-family: sans-serif; font-weight: bold; border: none;';
var headingStyle = ['font-size: 110%; padding-top: 0.5em; padding-bottom: 0.5em; width: 100%;', 'font-size: 120%; padding-top: 1em; padding-bottom: 1em; width: 100%;', 'font-size: 150%; padding-top: 1em; padding-bottom: 1em; width: 100%;'];
var author = kb.any(chunk, ns.dc('author'));
if (!colors && author) {
// Hash the user webid for now -- later allow user selection!
var bgcolor = UI.pad.lightColorHash(author);
colors = 'color: ' + (pending ? '#888' : 'black') + '; background-color: ' + bgcolor + ';';
}
var indent = kb.any(chunk, PAD('indent'));
indent = indent ? indent.value : 0;
var style = indent >= 0 ? //
// baseStyle + 'padding-left: ' + (indent * 3) + 'em;'
baseStyle + 'text-indent: ' + indent * 3 + 'em;' : headingCore + headingStyle[-1 - indent];
part.setAttribute('style', style + colors);
};
var removePart = function removePart(part) {
var chunk = part.subject;
if (!chunk) throw new Error('No chunk for line to be deleted!'); // just in case
var prev = kb.any(undefined, PAD('next'), chunk);
var next = kb.any(chunk, PAD('next'));
if (prev.sameTerm(subject) && next.sameTerm(subject)) {
// Last one
console.log("You can't delete the only line.");
return;
}
var del = kb.statementsMatching(chunk, undefined, undefined, padDoc).concat(kb.statementsMatching(undefined, undefined, chunk, padDoc));
var ins = [$rdf.st(prev, PAD('next'), next, padDoc)];
var label = chunk.uri.slice(-4);
console.log('Deleting line ' + label);
updater.update(del, ins, function (uri, ok, errorMessage, response) {
if (ok) {
var row = part.parentNode;
var before = row.previousSibling;
row.parentNode.removeChild(row);
console.log(' deleted line ' + label + ' ok ' + part.value);
if (before && before.firstChild) {
before.firstChild.focus();
}
} else if (response && response.status === 409) {
// Conflict
setPartStyle(part, 'color: black; background-color: #ffd;'); // yellow
part.state = 0; // Needs downstream refresh
utils.beep(0.5, 512); // Ooops clash with other person
setTimeout(function () {
// Ideally, beep! @@
reloadAndSync(); // Throw away our changes and
// updater.requestDownstreamAction(padDoc, reloadAndSync)
}, 1000);
} else {
console.log(' removePart FAILED ' + chunk + ': ' + errorMessage);
console.log(" removePart was deleteing :'" + del);
setPartStyle(part, 'color: black; background-color: #fdd;'); // failed
var res = response ? response.status : ' [no response field] ';
complain('Error ' + res + ' saving changes: ' + errorMessage["true"]); // upstream,
// updater.requestDownstreamAction(padDoc, reloadAndSync);
}
;
});
}; // removePart
var changeIndent = function changeIndent(part, chunk, delta) {
var del = kb.statementsMatching(chunk, PAD('indent'));
var current = del.length ? Number(del[0].object.value) : 0;
if (current + delta < -3) return; // limit negative indent
var newIndent = current + delta;
var ins = $rdf.st(chunk, PAD('indent'), newIndent, padDoc);
updater.update(del, ins, function (uri, ok, errorBody) {
if (!ok) {
console.log("Indent change FAILED '" + newIndent + "' for " + padDoc + ': ' + errorBody);
setPartStyle(part, 'color: black; background-color: #fdd;'); // failed
updater.requestDownstreamAction(padDoc, reloadAndSync);
} else {
setPartStyle(part); // Implement the indent
}
});
}; // Use this sort of code to split the line when return pressed in the middle @@
/*
function doGetCaretPosition doGetCaretPosition (oField) {
var iCaretPos = 0
// IE Support
if (document.selection) {
// Set focus on the element to avoid IE bug
oField.focus()
// To get cursor position, get empty selection range
var oSel = document.selection.createRange()
// Move selection start to 0 position
oSel.moveStart('character', -oField.value.length)
// The caret position is selection length
iCaretPos = oSel.text.length
// Firefox suppor
} else if (oField.selectionStart || oField.selectionStart === '0') {
iCaretPos = oField.selectionStart
}
// Return results
return (iCaretPos)
}
*/
var addListeners = function addListeners(part, chunk) {
part.addEventListener('keydown', function (event) {
var queueProperty, queue; // up 38; down 40; left 37; right 39 tab 9; shift 16; escape 27
switch (event.keyCode) {
case 13:
// Return
var before = event.shiftKey;
console.log('enter'); // Shift-return inserts before -- only way to add to top of pad.
if (before) {
queue = kb.any(undefined, PAD('next'), chunk);
queueProperty = 'newlinesAfter';
} else {
queue = kb.any(chunk, PAD('next'));
queueProperty = 'newlinesBefore';
}
queue[queueProperty] = queue[queueProperty] || 0;
queue[queueProperty] += 1;
if (queue[queueProperty] > 1) {
console.log(' queueing newline queue = ' + queue[queueProperty]);
return;
}
console.log(' go ahead line before ' + queue[queueProperty]);
newChunk(part, before); // was document.activeElement
break;
case 8:
// Delete
if (part.value.length === 0) {
console.log('Delete key line ' + chunk.uri.slice(-4) + ' state ' + part.state);
switch (part.state) {
case 1: // contents being sent
case 2:
// contents need to be sent again
part.state = 4; // delete me
return;
case 3: // being deleted already
case 4:
// already deleme state
return;
case undefined:
case 0:
part.state = 3; // being deleted
removePart(part);
event.preventDefault();
break;
// continue
default:
throw new Error('pad: Unexpected state ' + part);
}
}
break;
case 9:
// Tab
var delta = event.shiftKey ? -1 : 1;
changeIndent(part, chunk, delta);
event.preventDefault(); // default is to highlight next field
break;
case 27:
// ESC
console.log('escape');
updater.requestDownstreamAction(padDoc, reloadAndSync);
event.preventDefault();
break;
case 38:
// Up
if (part.parentNode.previousSibling) {
part.parentNode.previousSibling.firstChild.focus();
event.preventDefault();
}
break;
case 40:
// Down
if (part.parentNode.nextSibling) {
part.parentNode.nextSibling.firstChild.focus();
event.preventDefault();
}
break;
default:
}
});
var updateStore = function updateStore(part) {
var chunk = part.subject;
setPartStyle(part, undefined, true);
var old = kb.any(chunk, ns.sioc('content')).value;
var del = [$rdf.st(chunk, ns.sioc('content'), old, padDoc)];
var ins = [$rdf.st(chunk, ns.sioc('content'), part.value, padDoc)];
var newOne = part.value; // DEBUGGING ONLY
if (part.lastSent) {
if (old !== part.lastSent) {
throw new Error("Out of order, last sent expected '" + old + "' but found '" + part.lastSent + "'");
}
}
part.lastSent = newOne;
console.log(' Patch proposed to ' + chunk.uri.slice(-4) + " '" + old + "' -> '" + newOne + "' ");
updater.update(del, ins, function (uri, ok, errorBody, xhr) {
if (!ok) {
// alert("clash " + errorBody);
console.log(' patch FAILED ' + xhr.status + " for '" + old + "' -> '" + newOne + "': " + errorBody);
if (xhr.status === 409) {
// Conflict - @@ we assume someone else
setPartStyle(part, 'color: black; background-color: #fdd;');
part.state = 0; // Needs downstream refresh
utils.beep(0.5, 512); // Ooops clash with other person
setTimeout(function () {
updater.requestDownstreamAction(padDoc, reloadAndSync);
}, 1000);
} else {
setPartStyle(part, 'color: black; background-color: #fdd;'); // failed pink
part.state = 0;
complain(' Error ' + xhr.status + ' sending data: ' + errorBody, true);
utils.beep(1.0, 128); // Other
// @@@ Do soemthing more serious with other errors eg auth, etc
}
} else {
clearStatus(true); // upstream
setPartStyle(part); // synced
console.log(" Patch ok '" + old + "' -> '" + newOne + "' ");
if (part.state === 4) {
// delete me
part.state = 3;
removePart(part);
} else if (part.state === 3) {// being deleted
// pass
} else if (part.state === 2) {
part.state = 1; // pending: lock
updateStore(part);
} else {
part.state = 0; // clear lock
}
}
});
};
part.addEventListener('input', function inputChangeListener(event) {
// console.log("input changed "+part.value);
setPartStyle(part, undefined, true); // grey out - not synced
console.log('Input event state ' + part.state + " value '" + part.value + "'");
switch (part.state) {
case 3:
// being deleted
return;
case 4:
// needs to be deleted
return;
case 2:
// needs content updating, we know
return;
case 1:
part.state = 2; // lag we need another patch
return;
case 0:
case undefined:
part.state = 1; // being upadted
updateStore(part);
}
}); // listener
}; // addlisteners
var newPartAfter = function newPartAfter(tr1, chunk, before) {
// @@ take chunk and add listeners
var text = kb.any(chunk, ns.sioc('content'));
text = text ? text.value : '';
var tr = dom.createElement('tr');
if (before) {
table.insertBefore(tr, tr1);
} else {
// after
if (tr1 && tr1.nextSibling) {
table.insertBefore(tr, tr1.nextSibling);
} else {
table.appendChild(tr);
}
}
var part = tr.appendChild(dom.createElement('input'));
part.subject = chunk;
part.setAttribute('type', 'text');
part.value = text;
if (me) {
setPartStyle(part, '');
addListeners(part, chunk);
} else {
setPartStyle(part, 'color: #222; background-color: #fff');
console.log("Note can't add listeners - not logged in");
}
return part;
};
var newChunk = function newChunk(ele, before) {
// element of chunk being split
var kb = UI.store;
var indent = 0;
var queueProperty = null;
var here, prev, next, queue, tr1;
if (ele) {
if (ele.tagName.toLowerCase() !== 'input') {
console.log('return pressed when current document is: ' + ele.tagName);
}
here = ele.subject;
indent = kb.any(here, PAD('indent'));
indent = indent ? Number(indent.value) : 0;
if (before) {
prev = kb.any(undefined, PAD('next'), here);
next = here;
queue = prev;
queueProperty = 'newlinesAfter';
} else {
prev = here;
next = kb.any(here, PAD('next'));
queue = next;
queueProperty = 'newlinesBefore';
}
tr1 = ele.parentNode;
} else {
prev = subject;
next = subject;
tr1 = undefined;
}
var chunk = UI.widgets.newThing(padDoc);
var label = chunk.uri.slice(-4);
var del = [$rdf.st(prev, PAD('next'), next, padDoc)];
var ins = [$rdf.st(prev, PAD('next'), chunk, padDoc), $rdf.st(chunk, PAD('next'), next, padDoc), $rdf.st(chunk, ns.dc('author'), me, padDoc), $rdf.st(chunk, ns.sioc('content'), '', padDoc)];
if (indent > 0) {
// Do not inherit
ins.push($rdf.st(chunk, PAD('indent'), indent, padDoc));
}
console.log(' Fresh chunk ' + label + ' proposed');
updater.update(del, ins, function (uri, ok, errorBody, xhr) {
if (!ok) {
// alert("Error writing new line " + label + ": " + errorBody);
console.log(' ERROR writing new line ' + label + ': ' + errorBody);
} else {
var newPart = newPartAfter(tr1, chunk, before);
setPartStyle(newPart);
newPart.focus(); // Note this is delayed
if (queueProperty) {
console.log(' Fresh chunk ' + label + ' updated, queue = ' + queue[queueProperty]);
queue[queueProperty] -= 1;
if (queue[queueProperty] > 0) {
console.log(' Implementing queued newlines = ' + next.newLinesBefore);
newChunk(newPart, before);
}
}
}
});
};
var consistencyCheck = function consistencyCheck() {
var found = [];
var failed = 0;
function complain2(msg) {
complain(msg);
failed++;
}
if (!kb.the(subject, PAD('next'))) {
complain2('No initial next pointer');
return false; // can't do linked list
} // var chunk = kb.the(subject, PAD('next'))
var prev = subject;
var chunk;
for (;;) {
chunk = kb.the(prev, PAD('next'));
if (!chunk) {
complain2('No next pointer from ' + prev);
}
if (chunk.sameTerm(subject)) {
break;
}
prev = chunk;
var label = chunk.uri.split('#')[1];
if (found[chunk.uri]) {
complain2('Loop!');
return false;
}
found[chunk.uri] = true;
var k = kb.each(chunk, PAD('next')).length;
if (k !== 1) complain2('Should be 1 not ' + k + ' next pointer for ' + label);
k = kb.each(chunk, PAD('indent')).length;
if (k > 1) complain2('Should be 0 or 1 not ' + k + ' indent for ' + label);
k = kb.each(chunk, ns.sioc('content')).length;
if (k !== 1) complain2('Should be 1 not ' + k + ' contents for ' + label);
k = kb.each(chunk, ns.dc('author')).length;
if (k !== 1) complain2('Should be 1 not ' + k + ' author for ' + label);
var sts = kb.statementsMatching(undefined, ns.sioc('contents'));
sts.map(function (st) {
if (!found[st.subject.uri]) {
complain2('Loose chunk! ' + st.subject.uri);
}
});
}
return !failed;
}; // Ensure that the display matches the current state of the
var sync = function sync() {
// var first = kb.the(subject, PAD('next'))
if (kb.each(subject, PAD('next')).length !== 1) {
var msg = 'Pad: Inconsistent data - NEXT pointers: ' + kb.each(subject, PAD('next')).length;
console.log(msg);
if (options.statusAra) {
options.statusArea.textContent += msg;
}
return;
} // var last = kb.the(undefined, PAD('previous'), subject)
// var chunk = first // = kb.the(subject, PAD('next'));
var row; // First see which of the logical chunks have existing physical manifestations
var manif = []; // Find which lines correspond to existing chunks
for (var chunk = kb.the(subject, PAD('next')); !chunk.sameTerm(subject); chunk = kb.the(chunk, PAD('next'))) {
for (var i = 0; i < table.children.length; i++) {
var tr = table.children[i];
if (tr.firstChild.subject.sameTerm(chunk)) {
manif[chunk.uri] = tr.firstChild;
}
}
} // Remove any deleted lines
for (var _i = table.children.length - 1; _i >= 0; _i--) {
row = table.children[_i];
if (!manif[row.firstChild.subject.uri]) {
table.removeChild(row);
}
} // Insert any new lines and update old ones
row = table.firstChild; // might be null
for (var _chunk = kb.the(subject, PAD('next')); !_chunk.sameTerm(subject); _chunk = kb.the(_chunk, PAD('next'))) {
var text = kb.any(_chunk, ns.sioc('content')).value; // superstitious -- don't mess with unchanged input fields
// which may be selected by the user
if (row && manif[_chunk.uri]) {
var part = row.firstChild;
if (text !== part.value) {
part.value = text;
}
setPartStyle(part);
part.state = 0; // Clear the state machine
delete part.lastSent; // DEBUG ONLY
row = row.nextSibling;
} else {
newPartAfter(row, _chunk, true); // actually before
}
}
;
}; // Refresh the DOM tree
var refreshTree = function refreshTree(root) {
if (root.refresh) {
root.refresh();
return;
}
for (var i = 0; i < root.children.length; i++) {
refreshTree(root.children[i]);
}
};
var reloading = false;
var checkAndSync = function checkAndSync() {
console.log(' reloaded OK');
clearStatus();
if (!consistencyCheck()) {
complain('CONSITENCY CHECK FAILED');
} else {
refreshTree(table);
}
};
var reloadAndSync = function reloadAndSync() {
if (reloading) {
console.log(' Already reloading - stop');
return; // once only needed
}
reloading = true;
var retryTimeout = 1000; // ms
var tryReload = function tryReload() {
console.log('try reload - timeout = ' + retryTimeout);
updater.reload(updater.store, padDoc, function (ok, message, xhr) {
reloading = false;
if (ok) {
checkAndSync();
} else {
if (xhr.status === 0) {
complain('Network error refreshing the pad. Retrying in ' + retryTimeout / 1000);
reloading = true;
retryTimeout = retryTimeout * 2;
setTimeout(tryReload, retryTimeout);
} else {
complain('Error ' + xhr.status + 'refreshing the pad:' + message + '. Stopped. ' + padDoc);
}
}
});
};
tryReload();
};
table.refresh = sync; // Catch downward propagating refresh events
table.reloadAndSync = reloadAndSync;
if (!me) console.log('Warning: must be logged in for pad to be edited');
if (exists) {
console.log('Existing pad.');
if (consistencyCheck()) {
sync();
if (kb.holds(subject, PAD('next'), subject)) {
// Empty list untenable
newChunk(); // require at least one line
}
} else {
console.log(table.textContent = 'Inconsistent data. Abort');
}
} else {
// Make new pad
console.log('No pad exists - making new one.');
var insertables = [$rdf.st(subject, ns.rdf('type'), PAD('Notepad'), padDoc), $rdf.st(subject, ns.dc('author'), me, padDoc), $rdf.st(subject, ns.dc('created'), new Date(), padDoc), $rdf.st(subject, PAD('next'), subject, padDoc)];
updater.update([], insertables, function (uri, ok, errorBody) {
if (!ok) {
complain(errorBody);
} else {
console.log('Initial pad created');
newChunk(); // Add a first chunck
// getResults();
}
});
}
return table;
};
//# sourceMappingURL=pad.js.map