Hi,
i am quite new to it, so forgive me my (most likely) stupid question. I want to create vertical panes in a list which contain some text and an image under QuartexPascal. The pure javascript implementation looks like this.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Kacheln</title> <style> body { margin: 0; padding: 0; background-color: #f0f0f0; font-family: Arial, sans-serif; font-size: 14px; } .tile { display: inline-block; width: 200px; height: 200px; background-color: white; margin: 10px; position: relative; cursor: pointer; transition: transform 0.2s; } .tile:focus { transform: scale(1.1); box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); outline: none; } .tile-image { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; } .tile-content { position: absolute; bottom: 0; left: 0; right: 0; padding: 5px; background-color: rgba(0, 0, 0, 0.5); color: white; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tile-title { font-weight: bold; margin-bottom: 5px; } .tile-description { font-size: 12px; } .tile-container { /*display: flex; flex-direction: row; overflow-x: auto;*/ width: 100%; height: 220px; align-items: center; justify-content: center; } ::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track { background-color: #f0f0f0; border-radius: 5px; } ::-webkit-scrollbar-thumb { background-color: #c0c0c0; border-radius: 5px; } </style> </head> <body> <div class="tile-container"></div> <script> function addTile(imageUrl, title, description, backgroundColor) { const tileContainer = document.querySelector('.tile-container'); const tile = document.createElement('div'); tile.classList.add('tile'); tile.tabIndex = 0; const tileImage = document.createElement('img'); tileImage.classList.add('tile-image'); tileImage.src = imageUrl; const tileContent = document.createElement('div'); tileContent.classList.add('tile-content'); tileContent.style.backgroundColor = backgroundColor; const tileTitle = document.createElement('div'); tileTitle.classList.add('tile-title'); tileTitle.innerText = title; const tileDescription = document.createElement('div'); tileDescription.classList.add('tile-description'); tileDescription.innerText = description; tileContent.appendChild(tileTitle); tileContent.appendChild(tileDescription); tile.appendChild(tileImage); tile.appendChild(tileContent); tileContainer.appendChild(tile); } function focusNextTile(isForward) { const tiles = document.querySelectorAll('.tile'); const activeTile = document.activeElement; if (isForward) { const nextTile = activeTile.nextElementSibling || tiles[0]; nextTile.focus(); } else { const previousTile = activeTile.previousElementSibling || tiles[tiles.length - 1 ]; previousTile.focus(); } } document.addEventListener('keydown', function(event) { if (event.key === 'ArrowRight') { focusNextTile(true); } else if (event.key === 'ArrowLeft') { focusNextTile(false); } }); addTile('https://picsum.photos/200/200', 'Tile 1', 'Description 1', '#ff6347'); addTile('https://picsum.photos/200/200', 'Tile 2', 'Description 2', '#32cd32'); addTile('https://picsum.photos/200/200', 'Tile 3', 'Description 3', '#4169e1'); addTile('https://picsum.photos/200/200', 'Tile 4', 'Description 4', '#da70d6'); addTile('https://picsum.photos/200/200', 'Tile 5', 'Description 5', '#ffa500'); </script> </body> </html>
How could a solution in qtx look like?
Cheers,
Christian
Hi! No problem asking questions like this. In fact, it is a very reasonable question considering the hybrid nature of the system.
First, let's divide this into more maintainable segments so we can get to work on it.
- CSS styling
- Scripting functions
- HTML elements
Let's go through these one by one
CSS styling
You have two options with regards to the css. You can either save that out as a separate css file (which I would do), or you can embed the CSS into your code and load it at runtime. As a rule I never hardcode CSS that has to do with appearances, so in your case I would copy and paste the CSS into a new css file - and then simply add a reference to it in your QTX project HTML file. Just add another line to the <head> section in the project HTML file, and provide the filename of your css file there.
<link href="my_new_styles.css" rel="stylesheet">
You can now use these new styles on any widget via the CssClasses property that all TQTXWidgets have, this property is an interface with a ClassAdd() method. You can add as many styles (a single style definition is called a "class rule" in the CSS world, which can be confusing). Adding a style to a widget is as easy as:
mywidget.CssClasses.ClassAdd('some_style_name');
Scripting functions
Much like with our CSS above, you have a couple of options, but these require a few words of explaining before you start. In short you have 3 paths you can pick from:
- Export to a script file
- Embed into your pascal code via an ASM section
- Rewrite as pure pascal
Export as a script file
You can copy the Javascript code into a new *.js file and then load it automatically with the HTML document like we did above with the CSS. This can be done in more or less the exact same way, except that we use the <script> tag in the <head> section instead, like this:
<script src="my_script_functions.js" defer="true"></script>
In your case however, you want to omit the last 5 lines (those that call the addTile() functions) because those take for granted that the HTML in the document is exactly like expected. You want to control when that happens, and those 5 lines should only be executed once your QTX application is finished initializing everything. I will return to this further down in this post.
You also want to omit the document.addEventListner() block, because that too should be executed in a more controlled fashion.
Embed into your pascal code
As you have probably seen in various examples, QTX recycles the old ASM keyword for use with inline JavaScript. In Delphi and Freepascal the ASM keyword allows you to write assembly code (x86 for example) which is handy for advanced developers. In QTX you can use an ASM section whenever you want to execute some JavaScript.
So you could in fact add an initialization section to your mainform unit, then put an ASM section inside there with the code. Please note that the same rule as above applies -- you dont want to include those last 5 lines since they expect that the HTML elements exists exactly like in the original document. You can call those from another ASM section, for example from the Form's InitializeObject procedure.
The same goes for that document.addEventListner() call, that is best to execute after QTX has finished doing it's thing.
Rewrite as pure pascal
Depending on your JS skills (read: how well you understand JS) this is by far the best way to go. It is however not always possible if JS is not something you have worked deeply with. But just to be clear: all JS can be re-written as QTX pascal, providing the author knows both JS and the RTL well.
Ok, let's take a look at what this thing dones!
What does the JS actually do?
Looking at your code, this is relatively simple stuff. It appears more complex than it is due to the strange use of const, which does not have the same meaning as pascal consts.
In JS there is a concept of read-only variables, and for some strange reason they adopted the const keyword for that. In other words, "const abc = xyz" actually means "create variable abc that will never change, and assign it the value of xyz".
The const keyword just informs JSVM that it can optimize that variable to read-only since you will never actually write to it. But for us pascal developers who are used to const as being a static definition, one that we assign to variables or pass as parameters, the code makes no sense.
If we look at the HTML it actually defines only a single pre-existing HTML element (container), and adds a single CSS style to it (tile-container):
<div class="tile-container"></div>
At the top of the addTile() JS function we find this curious line:
const tileContainer = document.querySelector('.tile-container');
All this does is to ask the document "give me all html elements that has the css style tile-container, and put that in a read-only variable called tileContainer", which returns an array of elements regardless of what type they might be.
Personally I think this code is sloppy since it doesnt take height for multiple tile containers, but this is how the routine locates the container element in the HTML document.
If we look further we find that calling addTile() creates a new DIV elements that the routine adds to this container by code. Not unlike what the QTX RTL does when it creates widgets. Once created it assigns a css style called "tile" to this.
const tile = document.createElement('div'); tile.classList.add('tile'); tile.tabIndex = 0;
The routine continues by creating various elements it needs to create the visual tile, and finally it glues it all together with this code:
tileContent.appendChild(tileTitle); tileContent.appendChild(tileDescription); tile.appendChild(tileImage); tile.appendChild(tileContent); tileContainer.appendChild(tile);
That last line there is where the tile is added to the container, that <div> element that pre-existed in the HTML.
At the bottom of the JS we find this little snippet:
document.addEventListener('keydown', function(event) { if (event.key === 'ArrowRight') { focusNextTile(true); } else if (event.key === 'ArrowLeft') { focusNextTile(false); } });
This simply adds an event-handler (delegate) to the keydown event for the document itself. It checks for arrow right / left key-presses and calls the function focusNextTime() accordingly.
Putting it all together
I dont really have time right now to re-write all of this as pascal, so instead we are going to use the existing JS from within our QTX program. The result will be the same.
- Save out the CSS as a separate file as described above (and add <link> tag to your project's html file)
- Save the JS as a separate file (and add <script></script> tag to your project's html file). Remember to remove those 5 lines at the bottom and the event handler code above too!
- Drop a TQTXPanel on your mainform, click Save to store the changes
- Switch to the code-tab for Form1
Then we ASM keyword to execute the raw JS from within Form1, like this:
procedure Tform1.InitializeObject;
begin
inherited;
{$I "impl::form1"}
// Turn Panel1 into the container by adding the css style
panel1.CssClasses.ClassAdd('tile-container');
// Install the keyboard event handler
asm
document.addEventListener('keydown', function(event) {
if (event.key === 'ArrowRight') {
focusNextTile(true);
} else if (event.key === 'ArrowLeft') {
focusNextTile(false);
}
});
end;
Self.WhenReady( procedure (Widget: TQTXWidget)
begin
// Initialize the tiles
asm
addTile('https://picsum.photos/200/200', 'Tile 1', 'Description 1', '#ff6347');
addTile('https://picsum.photos/200/200', 'Tile 2', 'Description 2', '#32cd32');
addTile('https://picsum.photos/200/200', 'Tile 3', 'Description 3', '#4169e1');
addTile('https://picsum.photos/200/200', 'Tile 4', 'Description 4', '#da70d6');
addTile('https://picsum.photos/200/200', 'Tile 5', 'Description 5', '#ffa500');
end;
end);
end;
Notice that we turn Panel1 into the container because we add that special CSS style to it that the JavaScript code expects to find.
I also wrap the execution of the addTile() lines in a WhenReady() call. All TQTXWidgets (forms also derive from TQTXWidget) has a WhenReady() method. This makes sure that whatever code inside the wrap executes only when the widget has been fully created and has been injected into the document object model. It is just to make sure that we dont run a piece of code prematurely.
What you can do to clean this up further, is to add a OnKeyDown delegate to the mainform and check the event.key value for "ArrowRight|Left" in pascal, and then use an ASM snippet to call focusNextTile() there instead -- getting rid of that hardcoded JS snippet.
Also, the TQTXDOMApplication object has methods for adding delegates to the document itself, so it can be a good idea to experiment with that to learn more about this.
I hope this helps!
/Jon
Wow, thanks for this detailed explanation. I followed your explanation and completed the project. It is compiling, but somehow not working. I probably did something wrong, but I do not see any errors in the console.
Cheers
Christian
Attaching a file does not seem to be allowed, so I uploaded my zip to the following url: https://www.dvbviewer.tv/privat/qtx.zip
@hackbart You had forgotten to check Form1 in the build-config 🙂 It never showed form1.
This might be my fault, I will go over the templates and make sure form1 is checked in the config!
I have enclosed a zip with a working project. I removed the panel and just create the tiles directly on the form, it seems more natural.
If you modify the CSS for the container so it clips children, the list of tiles will not break into multiple lines. Normally you would have a "container inside a container", with the inner container set to blocking (full width based on content) and the outer set to clipped -- this way you get a sort of NetFlix like horizontal tile strip that doesnt bleed outside the outer container's bounds. Once you get into the various CSS style options you can do some pretty amazing things with very little code!
Tusen takk, this example works. At the moment I try to figure out how to replace the javascript by Pascal code, but I suppose this will become a bit more complicated then.
I solved it in this way, which might be the best solution in this case.
unit form1; interface uses qtx.sysutils, qtx.classes, qtx.dom.types, qtx.dom.events, qtx.dom.graphics, qtx.dom.widgets, qtx.dom.application, qtx.dom.forms, qtx.delegates, qtx.dom.events.mouse, qtx.dom.events.pointer, qtx.dom.events.keyboard, qtx.dom.control.panel; type Tform1 = class(TQTXForm) {$I "intf::form1"} protected procedure HandleDelegate1(Sender: TQTXDOMKeyboardDelegate; EventObj: JKeyboardEvent); procedure InitializeObject; override; procedure FinalizeObject; override; procedure StyleObject; override; public procedure focusNextTile(AIsForward: Boolean); procedure addTile(imageUrl, title, description, backgroundColor: string); end; implementation procedure Tform1.focusNextTile(AIsForward: Boolean); begin asm const tiles = document.querySelectorAll('.tile'); const activeTile = document.activeElement; if (@AIsForward) { const nextTile = activeTile.nextElementSibling || tiles[0]; nextTile.focus(); } else { const previousTile = activeTile.previousElementSibling || tiles[tiles.length - 1]; previousTile.focus(); } end; end; procedure TForm1.addTile(imageUrl, title, description, backgroundColor: string); begin asm const tileContainer = document.querySelector('.tile-container'); const tile = document.createElement('div'); tile.classList.add('tile'); tile.tabIndex = 0; const tileImage = document.createElement('img'); tileImage.classList.add('tile-image'); tileImage.src = @imageUrl; const tileContent = document.createElement('div'); tileContent.classList.add('tile-content'); tileContent.style.backgroundColor = @backgroundColor; const tileTitle = document.createElement('div'); tileTitle.classList.add('tile-title'); tileTitle.innerText = @title; const tileDescription = document.createElement('div'); tileDescription.classList.add('tile-description'); tileDescription.innerText = @description; tileContent.appendChild(tileTitle); tileContent.appendChild(tileDescription); tile.appendChild(tileImage); tile.appendChild(tileContent); tileContainer.appendChild(tile); end; end; procedure Tform1.HandleDelegate1(Sender: TQTXDOMKeyboardDelegate; EventObj: JKeyboardEvent); begin if EventObj.key = 'ArrowRight' then begin focusNextTile(true) end else if EventObj.key = 'ArrowLeft' then begin focusNextTile(false); end; end; procedure Tform1.InitializeObject; begin inherited; {$I "impl::form1"} // Turn Panel1 into the container by adding the css style self.CssClasses.ClassAdd('tile-container'); self.WhenReady( procedure (widget: TQTXWidget) begin // Initialize the tiles addTile('https://picsum.photos/201/200', 'Tile 1', 'Description 1', '#ff6347'); addTile('https://picsum.photos/202/200', 'Tile 2', 'Description 2', '#32cd32'); addTile('https://picsum.photos/203/200', 'Tile 3', 'Description 3', '#4169e1'); addTile('https://picsum.photos/204/200', 'Tile 4', 'Description 4', '#da70d6'); addTile('https://picsum.photos/205/200', 'Tile 5', 'Description 5', '#ffa500'); addTile('https://picsum.photos/206/200', 'Tile 6', 'Description 6', '#f9aF00'); end); end; procedure Tform1.FinalizeObject; begin inherited; end; procedure Tform1.StyleObject; begin inherited; PositionMode := TQTXWidgetPositionMode.cpInitial; DisplayMode := TQTXWidgetDisplayMode.cdBlock; end; end.
It is really nice to jump from one small "Nice" progress step to another while working with QTX. The asm syntax highlight seem to be broke, but this is just cosmetic.
Oh and what took me a bit is that when I add a delegate the name of the Delegate is always Delegate1. I got some collisions when adding several delegates in the form.
@hackbart Always delegate1? Are you using the latest build?
Just tested this myself, you are correct, that is a bug. I think I know what happened there, will issue a fix ASAP!
The delegate problem has now been fixed, download the latest build 🙂
niceee