zadam / trilium

Build your personal knowledge base with Trilium Notes
GNU Affero General Public License v3.0
27.52k stars 1.88k forks source link

[Feature request] Run webapps inside nodes / scripting API #1168

Open tomthe opened 4 years ago

tomthe commented 4 years ago

Hi, Trilium is really great, thanks for your work!

I know that it is possible to render data from another note with html and js inside a note. But would it also be possible to run a proper html+js webapp inside a node and persist the data somehow? What I have in my mind is that you could run something like tiddlywiki or flowy (I need checkable bullet points..) or a spreadsheet or MathJax or any client-side webapp you want inside a note.

Is there anything that would prevent something like this? Would it be possible, if everything is in one html-file?

zadam commented 4 years ago

Hi, so running the HTML is fine but as you mention there's an issue with persisting the data - Trilium has scripting API which allows you to programatically save data to notes but of course e.g. tiddlywiki does not understand those so you would have to write a little bit of integration code.

BTW, checkable bullet points are supported:

I also plan to add some sort of data grid functionality, but that will take some time.

tomthe commented 4 years ago

oh, this is really nice, I missed the To-do List functionality!

I played around with the scripting API a bit. A render-note seems to execute all child javascript notes upon activation. If I want to use a library (a js-file), I just add this js-file as a file-note child-note to the js- or html-note that uses it, without worrying about importing?

If I want to read and save data in a note, I create a JS-frontend-note and a JS-backend-note as child notes. In the backend-note I can retrieve the Note-Object (e.g. by ID); read the data/content with Note.getContent or save the data/content with Note.setContent. Is that approximately correct?

zadam commented 4 years ago

I played around with the scripting API a bit. A render-note seems to execute all child javascript notes upon activation.

Actually no, you need the renderNote relation to specify which note should be executed.

If I want to use a library (a js-file), I just add this js-file as a file-note child-note to the js- or html-note that uses it, without worrying about importing?

But yes, this part is correct, children of JS/HTML files are automatically included/imported so you don't need to do explicit import. If they define CommonJS module.exports then they are also automatically imported into the parent note and can be called using the child note's name.

Unfortunately all this is not very well documented and usually it's best to follow examples in the demo document. Better docs and examples are planned ...

If I want to read and save data in a note, I create a JS-frontend-note and a JS-backend-note as child notes.
In the backend-note I can retrieve the Note-Object (e.g. by ID); read the data/content with Note.getContent or save the data/content with Note.setContent. Is that approximately correct?

Yes, that sounds correct.

tomthe commented 4 years ago

I played around with the scripting API a bit. A render-note seems to execute all child javascript notes upon activation.

Actually no, you need the renderNote relation to specify which note should be executed.

yes, I wrote it wrong. But child-notes of executed javascript-notes are executed before them.

Thanks for clarifying. The documentation is far from perfect and complete, but that is expected in such a project at this stage.

avinashkanaujiya commented 4 years ago

@tomthe if you are successful in doing it. Could write maybe a article or contribute to wiki about how you did it? It will be helpful to see what things are possible in trilium.

tomthe commented 4 years ago

As an exercice, I try to integrate a spreadsheet-library into trilium. It's quite easy to display x-spreadsheets. I can write down the necessary steps here, but I am still fighting with the API to save and restore the data

zadam commented 4 years ago

The scripting API is only available in JS-Notes, not in HTML notes, correct?

Yes.

I have a lot of "Uncaught ReferenceError: ...... is not defined" because child-notes are not imported automatically!? I don't quite understand why it works in the examples and not in my code

If you want to call the method defined in a child note, you need to use the child note as the name of the module. Methods in the child note needs to be "published" using module.exports. Scripts notes are behaving as CommonJS modules.

So e.g. you have structure like this:

where ChildScript:

function doSomething() {
}

module.exports = {
    doSomething
};

and MainScript:

ChildScript.doSomething()

Regarding the architecture: Where would you store the json-data? In a child note with a special relation?
What if I want to have several spreadsheets all over my trilium-tree?

  • I create a new render-note with a relation @renderNote=SpreadsheetCode
  • The script figures out the id of the render-note and creates a new child note as data-note and loads/saves to this note
  • is this possible?

Yes, that sounds reasonable, I use this pattern myself. API has several contextual properties - one of them is originEntity which defines on which entity is the script executing, here it would be the renderNote. Personally I would simply create a named child note under this originEntity, relation IMHO is not strictly necessary.

tomthe commented 4 years ago

But in my case, ChildScript is not defined in MainScript. MainScript is a html-note in my case.

zadam commented 4 years ago

Ok, then do it like this (if you even need ChildScript):

As discussed before, HTML notes are really just templates, they should not have any inline JavaScript (or at least not the JS code which tries to integrate with Trilium API).

With this setup, MainScript and ChildScript will be automatically included and executed.

tomthe commented 4 years ago

Thank you zadam, I removed the code from the HTML-note and now that works. I wrote how I did everything - maybe this will be interesting for the wiki, if we can resolve the remaining issues?

edit: Don't use this explanation, it has some errors:

How to integrate xSpreadsheets into Trilium

    <link rel="stylesheet" href="https://unpkg.com/x-data-spreadsheet@1.1.5/dist/xspreadsheet.css">
    <script src="https://unpkg.com/x-data-spreadsheet@1.1.5/dist/xspreadsheet.js"></script>
    <div id="xspreadsheet"></div>

We will replace the online hosted js-files by local files later.

Do not use Javascript inside the HTML-note - it will behave oddly and the Trilium API is not available. The div with the id “xspreadsheet” is where the spreadsheet will be inserted.

Then create a Javascript Frontend Note as a child of the htmlnote and call it “js”. Here we will insert the Javascript to initialize the xspreadsheet. We take the code from the xspreadsheet-readme to do so:

const s3 = new x_spreadsheet("#xspreadsheet")
         .loadData() // if there is nothing, an empty spreadsheet will be created
         .change(async (data) => {
             // this function will be called on every change to the spreadsheet. save the  data somewhere
       });
       s3.validate()

Well done! we have now a working spreadsheet inside the render note!

Saving and retrieving Data in Trilium

But it will forget everything you put into it as soon as you switch away from this note. To change this, we have to write two functions that will save the data into another note and load the data from that note when necessary.

We will write two functions loadSheetData() and saveSheetToNote() that will load json-data from the sheetdata-note and save it to that note.

This is the code inside the Loadsavesheet-note:

 async function savesheettonote(sheetdata){
    console.log("saving following data: ",JSON.stringify(sheetdata));
    const dataNoteId = await api.runOnServer(async (sheetdata) => {
        let currentNote2 = api.originEntity;
        const sdataNote = await currentNote2.getDescendantNotesWithLabel("sheetdata"); sdataNote[0].setContent(JSON.stringify(sheetdata));
        return sdataNote.noteId;
    },[sheetdata]);
    await api.waitUntilSynced();
    return 0;
}

async function loadSheetData() {
    console.log("load.. .. ");
    const data2 = await api.runOnServer(async () => {
        let currentNote2 = api.originEntity;
        const sdataNote = await currentNote2.getDescendantNotesWithLabel("sheetdata");
        let data1 = sdataNote[0].getContent();
        return data1
    });
    console.log("load data2: ------------", data2);
    return JSON.parse(data2);
}

module.exports = { savesheettonote,loadSheetData };

It identifies the sheetdata-note by the label with the name sheetdata. So you have to make sure to add this note with this label as a child of the spreadsheet somewhere. In a next step, we could create this note automatically, if it can not be found.

Now change the note “js” so that it calls the correct load- and save-functions (keep in mind that these are async-functions and you have to add the await keyword in front of them, otherwise xspreadsheet only receives a promise and won't know how to interpret it):

const s3 = new x_spreadsheet("#xspreadsheet")
         .loadData(await Loadsavesheet.loadSheetData())
         .change(async (data) => {
             await Loadsavesheet.savesheettonote(data);
       });
       s3.validate()

Now you can try to edit the spreadsheet, jump to a different note or close Trilium and come back and the data should still be there.

One problem remains: the library is still hosted online and if it can't be fetched fast enough, it will fail to display on the first start. We want the js to be saved into Trilium. It was not easy to find out how to create a file note: right click on a note in the tree and click “import into note” in the context menu. Uncheck “Import recognized code files (e.g. .json) as code notes if it's unclear from metadata” and browse for the file. Javascript files should be automatically imported if they are a child of js-frontend notes.

Unfortunately this didn't work for me. The Spreadsheet was displayed but the editing was somehow wrong and it was in effect unusable. What could be wrong/different between the local files and the online-hosted version?? These are exactly the same files.

zadam commented 4 years ago

Hi, I followed your guide, tried it out and fixed some of the problems. Some remarks:

You can try my version. Just don't forget to uncheck the "safe import".

Spreadsheet demo.zip

Engr-AllanG commented 4 years ago

@tomthe Thanks for this - very cool. I had a few issues, namely the sheet data cannot be empty to start. It needs {} to begin functioning, as far as I can tell. I ended up importing the version @zadam edited.

I may be using this going forward!

ehamminga commented 3 years ago

Thanks for writing this up Tom. Still trying to wrap my head around the capabilities of Trilium and this is a good demo to start with. Zadam, I tried your demo but I get an error on line 4 of the js child of the Spreadsheet Implementation: image

thfrei commented 2 years ago

Spreadsheet demo.zip

Thank you for that demo. I think that is very nice. It would be awesome if we have editing features too, even without a new note type :-P

For anyone interested, this is how it looks: grafik