You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
686 lines
20 KiB
686 lines
20 KiB
tinymce.ScriptLoader.loadScripts(
|
|
[
|
|
'//unpkg.com/@pollyjs/core@5.1.1',
|
|
'//unpkg.com/@pollyjs/adapter-fetch@5.1.1',
|
|
'//unpkg.com/@pollyjs/persister-local-storage@5.1.1',
|
|
],
|
|
() => {
|
|
/******************************
|
|
* Mock server implementation *
|
|
******************************/
|
|
|
|
let { Polly } = window['@pollyjs/core'];
|
|
let FetchAdapter = window['@pollyjs/adapter-fetch'];
|
|
let LocalStoragePersister = window['@pollyjs/persister-local-storage'];
|
|
|
|
Polly.register(FetchAdapter);
|
|
Polly.register(LocalStoragePersister);
|
|
let polly = new Polly('test', {
|
|
adapters: ['fetch'],
|
|
persister: 'local-storage',
|
|
});
|
|
let server = polly.server;
|
|
|
|
server.any().on('request', (req) => {
|
|
console.log('Server request:', req);
|
|
});
|
|
|
|
server.any().on('beforeResponse', (req, res) => {
|
|
console.log('Server response:', res);
|
|
});
|
|
|
|
/* this would be an admin for the file, they're allowed to do all operations */
|
|
function getOwner() {
|
|
return localStorage.getItem('owner') ?? users[0].id;
|
|
}
|
|
|
|
/* Server knows the author, probably by cookie or JWT token */
|
|
function getAuthor() {
|
|
return localStorage.getItem('author') ?? users[0].id;
|
|
}
|
|
|
|
/* this would be an admin for the file, they're allowed to do all operations */
|
|
function setOwner(user) {
|
|
localStorage.setItem('owner', user) ?? users[0].id;
|
|
}
|
|
|
|
/* Server knows the author, probably by cookie or JWT token */
|
|
function setAuthor(user) {
|
|
localStorage.setItem('author', user) ?? users[0].id;
|
|
}
|
|
|
|
function randomString() {
|
|
/* ~62 bits of randomness, so very unlikely to collide for <100K uses */
|
|
return Math.random().toString(36).substring(2, 14);
|
|
}
|
|
|
|
/* Our server "database" */
|
|
function getDB() {
|
|
return JSON.parse(localStorage.getItem('fakedb') ?? '{}');
|
|
}
|
|
function setDB(data) {
|
|
localStorage.setItem('fakedb', JSON.stringify(data));
|
|
}
|
|
|
|
function getConversation(uid) {
|
|
let store = getDB();
|
|
console.log('DB get:', uid, store[uid]);
|
|
return store[uid];
|
|
}
|
|
|
|
function setConversation(uid, conversation) {
|
|
let store = getDB();
|
|
console.log('DB set:', uid, store[uid], conversation);
|
|
store[uid] = conversation;
|
|
setDB(store);
|
|
}
|
|
|
|
function deleteConversation(uid) {
|
|
let store = getDB();
|
|
console.log('DB delete:', uid);
|
|
delete store[uid];
|
|
setDB(store);
|
|
}
|
|
|
|
function deleteAllConversations() {
|
|
console.log('DB delete all');
|
|
let store = {};
|
|
setDB(store);
|
|
}
|
|
|
|
server.host('https://api.example', () => {
|
|
/* create new conversation */
|
|
server.post('/conversations/').intercept((req, res) => {
|
|
let author = getAuthor();
|
|
let { content, createdAt } = JSON.parse(req.body);
|
|
console.log(req.body);
|
|
try {
|
|
let conversationUid = randomString();
|
|
setConversation(conversationUid, [
|
|
{
|
|
author,
|
|
createdAt,
|
|
modifiedAt: createdAt,
|
|
content,
|
|
uid: conversationUid /* first comment has same uid as conversation */,
|
|
},
|
|
]);
|
|
res.status(201).json({ conversationUid });
|
|
} catch (e) {
|
|
console.log('Server error:', e);
|
|
res.status(500);
|
|
}
|
|
});
|
|
|
|
/* add new comment to conversation */
|
|
server.post('/conversations/:conversationUid').intercept((req, res) => {
|
|
let author = getAuthor();
|
|
let { content, createdAt } = JSON.parse(req.body);
|
|
let conversationUid = req.params.conversationUid;
|
|
try {
|
|
let conversation = getConversation(conversationUid);
|
|
let commentUid = randomString();
|
|
setConversation(
|
|
conversationUid,
|
|
conversation.concat([
|
|
{
|
|
author,
|
|
createdAt,
|
|
modifiedAt: createdAt,
|
|
content,
|
|
uid: commentUid,
|
|
},
|
|
])
|
|
);
|
|
res.status(201).json({ commentUid });
|
|
} catch (e) {
|
|
console.log('Server error:', e);
|
|
res.status(500);
|
|
}
|
|
});
|
|
|
|
/* edit a comment */
|
|
server
|
|
.put('/conversations/:conversationUid/:commentUid')
|
|
.intercept((req, res) => {
|
|
let author = getAuthor();
|
|
let { content, modifiedAt } = JSON.parse(req.body);
|
|
let conversationUid = req.params.conversationUid;
|
|
let commentUid = req.params.commentUid;
|
|
|
|
try {
|
|
let conversation = getConversation(conversationUid);
|
|
let commentIndex = conversation.findIndex((comment) => {
|
|
return comment.uid === commentUid;
|
|
});
|
|
let comment = conversation[commentIndex];
|
|
let canEdit = comment.author === author;
|
|
if (canEdit) {
|
|
setConversation(conversationUid, [
|
|
...conversation.slice(0, commentIndex),
|
|
{
|
|
...comment,
|
|
content,
|
|
modifiedAt,
|
|
},
|
|
...conversation.slice(commentIndex + 1),
|
|
]);
|
|
}
|
|
res.status(201).json({ canEdit });
|
|
} catch (e) {
|
|
console.log('Server error:', e);
|
|
res.status(500);
|
|
}
|
|
});
|
|
|
|
/* delete a comment */
|
|
server
|
|
.delete('/conversations/:conversationUid/:commentUid')
|
|
.intercept((req, res) => {
|
|
let author = getAuthor();
|
|
let owner = getOwner();
|
|
let conversationUid = req.params.conversationUid;
|
|
let commentUid = req.params.commentUid;
|
|
let conversation = getConversation(conversationUid);
|
|
if (!conversation) {
|
|
res.status(404);
|
|
}
|
|
let commentIndex = conversation.findIndex((comment) => {
|
|
return comment.uid === commentUid;
|
|
});
|
|
if (commentIndex === -1) {
|
|
res.status(404);
|
|
}
|
|
if (
|
|
conversation[commentIndex].author === author ||
|
|
author === owner
|
|
) {
|
|
setConversation(conversationUid, [
|
|
...conversation.slice(0, commentIndex),
|
|
...conversation.slice(commentIndex + 1),
|
|
]);
|
|
res.status(204);
|
|
} else {
|
|
res.status(403);
|
|
}
|
|
});
|
|
|
|
/* delete a conversation */
|
|
server.delete('/conversations/:conversationUid').intercept((req, res) => {
|
|
let author = getAuthor();
|
|
let owner = getOwner();
|
|
let conversationUid = req.params.conversationUid;
|
|
let conversation = getConversation(conversationUid);
|
|
if (conversation) {
|
|
if (conversation[0].author === author || author === owner) {
|
|
deleteConversation(conversationUid);
|
|
res.status(204);
|
|
} else {
|
|
res.status(403);
|
|
}
|
|
} else {
|
|
res.status(404);
|
|
}
|
|
});
|
|
|
|
/* delete all conversations */
|
|
server.delete('/conversations').intercept((req, res) => {
|
|
let author = getAuthor();
|
|
let owner = getOwner();
|
|
if (author === owner) {
|
|
deleteAllConversations();
|
|
res.status(204);
|
|
} else {
|
|
res.status(403);
|
|
}
|
|
});
|
|
|
|
/* lookup a conversation */
|
|
server.get('/conversations/:conversationUid').intercept((req, res) => {
|
|
let conversation = getConversation(req.params.conversationUid);
|
|
if (conversation) {
|
|
res.status(200).json(conversation);
|
|
} else {
|
|
res.status(404);
|
|
}
|
|
});
|
|
|
|
/* lookup users */
|
|
server.get('/users/').intercept((req, res) => {
|
|
res.status(200).json({
|
|
users,
|
|
});
|
|
});
|
|
}); /* server.host */
|
|
|
|
/* Connect using the `connectTo` API */
|
|
polly.connectTo('fetch');
|
|
|
|
/************************************************
|
|
* Fake Users and associated pickers *
|
|
* Should be based on sessions and backend data *
|
|
***********************************************/
|
|
|
|
const users = [
|
|
{ id: 'alex', displayName: 'Alex' },
|
|
{ id: 'jessie', displayName: 'Jessie' },
|
|
{ id: 'sam', displayName: 'Sam' },
|
|
];
|
|
|
|
/* Set initial Owner */
|
|
setOwner(users[2].id);
|
|
|
|
/* Set initial Author */
|
|
setAuthor(users[0].id);
|
|
|
|
/********************************
|
|
* Tiny Comments functions *
|
|
* (must call "done" or "fail") *
|
|
********************************/
|
|
|
|
/**
|
|
* Callback for when the operation was successful.
|
|
* @template T
|
|
* @callback done
|
|
* @param {T} data - the data
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* Callback for when the operation failed.
|
|
* @callback fail
|
|
* @param {string|Error} error - the reason for the failure
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* The data supplied to create a comment.
|
|
* @typedef {Object} TinyCommentsCreateReq
|
|
* @property {string} content - comment content
|
|
* @property {string} createdAt - ISO creation date
|
|
*/
|
|
|
|
/**
|
|
* The response returned when a comment was created on the server.
|
|
* @typedef {Object} TinyCommentsCreateResp
|
|
* @property {string} conversationUid - ID of created comment
|
|
* @property {?fail} onError - error callback to call when the comment can't be put into the document
|
|
* @property {?done<string>} onSuccess - success callback to call when the comment is put into the document
|
|
*/
|
|
|
|
/**
|
|
* Conversation "create" function. Saves the comment as a new conversation,
|
|
* and asynchronously returns a conversation unique ID via the "done"
|
|
* callback.
|
|
*
|
|
* @param {TinyCommentsCreateReq} req - the comment to create
|
|
* @param {done<TinyCommentsCreateResp>} done - callback to call when the comment is created on the server
|
|
* @param {fail} fail - callback to call when something fails
|
|
*/
|
|
function tinycomments_create(req, done, fail) {
|
|
let content = req.content;
|
|
let createdAt = req.createdAt;
|
|
|
|
fetch('https://api.example/conversations/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ content: content, createdAt: createdAt }),
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to create comment');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then((req2) => {
|
|
let conversationUid = req2.conversationUid;
|
|
done({ conversationUid: conversationUid });
|
|
})
|
|
.catch((e) => {
|
|
fail(e);
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @typedef {Object} TinyCommentsReplyReq
|
|
* @property {string} conversationUid
|
|
* @property {string} content
|
|
* @property {string} createdAt
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @typedef {Object} TinyCommentsReplyResp
|
|
* @property {string} commentUid
|
|
*/
|
|
|
|
/**
|
|
* Conversation "reply" function. Saves the comment as a reply to the
|
|
* an existing conversation, and asynchronously returns via the "done"
|
|
* callback when finished.
|
|
*
|
|
* @param {TinyCommentsReplyReq} req - the comment to append
|
|
* @param {done<TinyCommentsReplyResp>} done - callback to call when the comment is created on the server
|
|
* @param {fail} fail - callback to call when something fails
|
|
*/
|
|
function tinycomments_reply(req, done, fail) {
|
|
let conversationUid = req.conversationUid;
|
|
let content = req.content;
|
|
let createdAt = req.createdAt;
|
|
|
|
fetch('https://api.example/conversations/' + conversationUid, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ content: content, createdAt: createdAt }),
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to reply to comment');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then((req2) => {
|
|
let commentUid = req2.commentUid;
|
|
done({ commentUid: commentUid });
|
|
})
|
|
.catch((e) => {
|
|
fail(e);
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
* @typedef {Object} TinyCommentsEditReq
|
|
* @property {string} conversationUid
|
|
* @property {string} commentUid
|
|
* @property {string} content
|
|
* @property {string} modifiedAt
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @typedef {Object} TinyCommentsEditResp
|
|
* @property {boolean} canEdit
|
|
* @property {?string} reason
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @param {TinyCommentsEditReq} req
|
|
* @param {done<TinyCommentsEditResp>} done
|
|
* @param {fail} fail
|
|
*/
|
|
function tinycomments_edit_comment(req, done, fail) {
|
|
let conversationUid = req.conversationUid;
|
|
let commentUid = req.commentUid;
|
|
let content = req.content;
|
|
let modifiedAt = req.modifiedAt;
|
|
|
|
fetch(
|
|
'https://api.example/conversations/' +
|
|
conversationUid +
|
|
'/' +
|
|
commentUid,
|
|
{
|
|
method: 'PUT',
|
|
body: JSON.stringify({ content: content, modifiedAt: modifiedAt }),
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
)
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to edit comment');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then((req2) => {
|
|
let canEdit = req2.canEdit;
|
|
done({ canEdit: canEdit });
|
|
})
|
|
.catch((e) => {
|
|
fail(e);
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @typedef TinyCommentsDeleteReq
|
|
* @property {string} conversationUid
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @typedef TinyCommentsDeleteResp
|
|
* @property {boolean} canDelete
|
|
* @property {?string} reason
|
|
*/
|
|
|
|
/**
|
|
* Conversation "delete" function. Deletes an entire conversation.
|
|
* Returns asynchronously whether the conversation was deleted.
|
|
* Failure to delete due to permissions or business rules is indicated
|
|
* by `{canDelete: false}`, while unexpected errors should be indicated using the
|
|
* "fail" callback.
|
|
* @param {TinyCommentsDeleteReq} req
|
|
* @param {done<TinyCommentsDeleteResp>} done
|
|
* @param {fail} fail
|
|
*/
|
|
function tinycomments_delete(req, done, fail) {
|
|
let conversationUid = req.conversationUid;
|
|
fetch('https://api.example/conversations/' + conversationUid, {
|
|
method: 'DELETE',
|
|
}).then((response) => {
|
|
if (response.ok) {
|
|
done({ canDelete: true });
|
|
} else if (response.status === 403) {
|
|
done({ canDelete: false });
|
|
} else {
|
|
fail(new Error('Something has gone wrong...'));
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @typedef TinyCommentsDeleteAllReq
|
|
* @type {object}
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @typedef TinyCommentsDeleteAllResp
|
|
* @property {boolean} canDelete
|
|
* @property {?string} reason
|
|
*/
|
|
|
|
/**
|
|
* All conversations "delete_all" function. Deletes all conversations.
|
|
* Returns asynchronously whether all conversations were deleted.
|
|
* Failure to delete due to permissions or business rules is indicated
|
|
* by `{canDelete: false}`, while unexpected errors should be indicated using the
|
|
* "fail" callback.
|
|
* @param {TinyCommentsDeleteAllReq} _req - no options
|
|
* @param {done<TinyCommentsDeleteAllResp>} done
|
|
* @param {fail} fail
|
|
*/
|
|
function tinycomments_delete_all(_req, done, fail) {
|
|
fetch('https://api.example/conversations', {
|
|
method: 'DELETE',
|
|
}).then((response) => {
|
|
if (response.ok) {
|
|
done({ canDelete: true });
|
|
} else if (response.status === 403) {
|
|
done({ canDelete: false });
|
|
} else {
|
|
fail(new Error('Something has gone wrong...'));
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @typedef TinyCommentsDeleteCommentReq
|
|
* @property {string} conversationUid
|
|
* @property {string} commentUid
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @typedef TinyCommentsDeleteCommentResp
|
|
* @property {boolean} canDelete
|
|
* @property {?string} reason
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @param {TinyCommentsDeleteCommentReq} req
|
|
* @param {done<TinyCommentsDeleteCommentResp>} done
|
|
* @param {fail} fail
|
|
*/
|
|
function tinycomments_delete_comment(req, done, fail) {
|
|
let conversationUid = req.conversationUid;
|
|
let commentUid = req.commentUid;
|
|
|
|
fetch(
|
|
'https://api.example/conversations/' +
|
|
conversationUid +
|
|
'/' +
|
|
commentUid,
|
|
{
|
|
method: 'DELETE',
|
|
}
|
|
).then((response) => {
|
|
if (response.ok) {
|
|
done({ canDelete: true });
|
|
} else if (response.status === 403) {
|
|
done({ canDelete: false });
|
|
} else {
|
|
fail(new Error('Something has gone wrong...'));
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @typedef TinyCommentsLookupReq
|
|
* @property {string} conversationUid
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @typedef TinyCommentsLookupRespComment
|
|
* @property {string} author
|
|
* @property {?string} authorName
|
|
* @property {string} createdAt
|
|
* @property {string} modifiedAt
|
|
* @property {string} content
|
|
* @property {string} uid
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @typedef TinyCommentsLookupRespConversation
|
|
* @property {string} uid
|
|
* @property {TinyCommentsLookupRespComment[]} comments
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* @typedef TinyCommentsLookupResp
|
|
* @property {TinyCommentsLookupRespConversation} conversation
|
|
*/
|
|
|
|
/**
|
|
* Conversation "lookup" function. Retreives an existing conversation
|
|
* via a conversation unique ID. Asynchronously returns the conversation
|
|
* via the "done" callback.
|
|
*
|
|
* @param {TinyCommentsLookupReq} req
|
|
* @param {done<TinyCommentsLookupResp>} done
|
|
* @param {fail} fail
|
|
*/
|
|
function tinycomments_lookup({ conversationUid }, done, fail) {
|
|
let lookup = async function () {
|
|
let convResp = await fetch(
|
|
'https://api.example/conversations/' + conversationUid
|
|
);
|
|
if (!convResp.ok) {
|
|
throw new Error('Failed to get conversation');
|
|
}
|
|
let comments = await convResp.json();
|
|
let usersResp = await fetch('https://api.example/users/');
|
|
if (!usersResp.ok) {
|
|
throw new Error('Failed to get users');
|
|
}
|
|
let { users } = await usersResp.json();
|
|
let getUser = function (userId) {
|
|
return users.find((u) => {
|
|
return u.id === userId;
|
|
});
|
|
};
|
|
return {
|
|
conversation: {
|
|
uid: conversationUid,
|
|
comments: comments.map((comment) => {
|
|
return {
|
|
...comment,
|
|
content: comment.content,
|
|
authorName: getUser(comment.author)?.displayName,
|
|
};
|
|
}),
|
|
},
|
|
};
|
|
};
|
|
lookup()
|
|
.then((data) => {
|
|
console.log('Lookup success ' + conversationUid, data);
|
|
done(data);
|
|
})
|
|
.catch((err) => {
|
|
console.error('Lookup failure ' + conversationUid, err);
|
|
fail(err);
|
|
});
|
|
}
|
|
|
|
tinymce.init({
|
|
selector: 'textarea#callback-mode',
|
|
height: 800,
|
|
plugins: 'paste code tinycomments help lists',
|
|
toolbar:
|
|
'undo redo | formatselect | ' +
|
|
'bold italic backcolor | alignleft aligncenter ' +
|
|
'alignright alignjustify | bullist numlist outdent indent | ' +
|
|
'removeformat | addcomment showcomments | help',
|
|
menubar: 'file edit view insert format tc',
|
|
menu: {
|
|
tc: {
|
|
title: 'Comments',
|
|
items: 'addcomment showcomments deleteallconversations',
|
|
},
|
|
},
|
|
tinycomments_create,
|
|
tinycomments_reply,
|
|
tinycomments_edit_comment,
|
|
tinycomments_delete,
|
|
tinycomments_delete_all,
|
|
tinycomments_delete_comment,
|
|
tinycomments_lookup,
|
|
/* The following setup callback opens the comments sidebar when the editor loads */
|
|
setup: function (editor) {
|
|
editor.on('SkinLoaded', () => {
|
|
editor.execCommand('ToggleSidebar', false, 'showcomments', {
|
|
skip_focus: true,
|
|
});
|
|
});
|
|
},
|
|
});
|
|
}
|
|
);
|