Mithril: render one DOM element bases on another - javascript

I have a fixed height div (body) containing two children, the header and the content. The header's height is changing on click of a button, and the content's height should be auto adjusting to fill the rest of the body. Now the problem is, the new header's height is calculated after the header and content divs got rendered, so the content div's height won't be updated upon the button click. Here's the shortened code:
return m('.body', {
style: {
height: '312px'
}
}, [
m('.header', /* header contents */),
m('.content', {
style: {
height: (312 - this._viewModel._headerHeight()) + 'px'
}
}, /* some contents */)
])
The headerHeight function calculates the header's height and applies changes to it. However the new height is calculated after it's rendered thus won't be applied immediately to the calculation of content's height - there's always a lag.
Any idea to fix it?

This is a regular problem when dealing with dynamic DOM layouts in which some writable DOM properties are derived from other readable DOM properties. This is especially tough to reason about in declarative virtual DOM idioms like Mithril because they're based on the premise that every view function should be self-complete snapshots of UI state — which in this case isn't possible.
You have 3 options: you can either break out of the virtual DOM idiom to achieve this functionality by directly manipulating the DOM outside of Mithril's view, or you can model your component to operate on a '2 pass draw', whereby each potential change to the header element results in 1 draw to update the header and a second draw to update the content accordingly. Alternatively, you might be able to get away with a pure CSS solution.
Because you only need to update one property, you're almost certainly better off going for the first option. By using the config function, you can write custom functionality that executes after the view on every draw.
return m('.body', {
style: {
height: '312px'
},
config : function( el ){
el.lastChild.style.height = ( 312 - el.firstChild.offsetHeight ) + 'px'
}
}, [
m('.header', /* header contents */),
m('.content', /* some contents */)
])
The second option is more idiomatic in terms of virtual DOM philosophy because it avoids direct DOM manipulation and keeps all stateful data in a model read and applied by the view. This approach becomes more useful when you have a multitude of dynamic DOM-related properties, because you can inspect the entire view model as the view is rendered — but it's also a lot more complicated and inefficient, especially for your scenario:
controller : function(){
this.headerHeight = 0
},
view : function( ctrl ){
return m('.body', {
style: {
height: '312px'
}
}, [
m('.header', {
config : function( el ){
if( el.offsetHeight != ctrl.headerHeight ){
ctrl.headerHeight = el.offsetHeight
window.requestAnimationFrame( m.redraw )
}
}, /* header contents */),
m('.content', {
style : {
height : ( 312 - ctrl.headerHeight ) + 'px'
}
}, /* some contents */)
])
}
A third option — depending on which browsers you need to support — would be to use the CSS flexbox module.
return m('.body', {
style: {
height: '312px',
display: 'flex',
flexDirection: 'column'
}
}, [
m('.header', {
style : {
flexGrow: 1,
flexShrink: 0
}
}, /* header contents */),
m('.content', {
style : {
flexGrow: 0,
flexShrink: 1
}
}, /* some contents */)
])
This way, you can simply state that the container is a flexbox, that the header should grow to fit its content and never shrink, and that the contents should shrink but never grow.

Related

How To Place Children Items of my Custom Item at my desired Location? (QML)

Assuming I want to create my CustomItem
//MyCustomItem.qml
RowLayout{
spacing: 0
Image{
id: divider
source: "qrc:/screen_header/vertical_divider_screen_header.png"
}
Item{ //THIS PART HERE SHOULD ALWAYS BE THE CHILDREN ITEMS
id:mid_item_TEXT_OR_IMAGE
Layout.alignment: Qt.AlignHCenter
}
Image{
id: divider2
source: "qrc:/screen_header/vertical_divider_screen_header.png"
}
}
I want to use this CustomItem and every children should be automatically be placed inside "Item" but the Layout.alignment should persist.
For Example
Placing an image inside would result in
//main.qml
MyCustomItem{
Image{
source: "qrc:/test.png"
}
}
Placing a Text inside would result in
MyCustomItem{
Text{
text: "AUTO"
}
}
How can I achieve this?
You can use a default property to automagically direct the Image/Text into mid_item_TEXT_OR_IMAGE with the following code:
default property alias children: mid_item_TEXT_OR_IMAGE.data
Note that this allows multiple elements inside MyCustomItem (including non-visual ones). If you don't want this, you can use a Control (Qt doc) instead of mid_item_TEXT_OR_IMAGE and alias to contentItem.
Btw, the Layout.alignment: Qt.AlignHCenter that you really want will not work, since the RowLayout will assign the Item a place on the horizontal axis based on the items. It does work for Qt.AlignVCenter since that is perpendicular to the layout direction.
With the following code you can center all contents in the middle like in your images:
RowLayout {
default property alias data: mid_item_TEXT_OR_IMAGE.data
Rectangle { //replace with your Divider.png
width: 1
Layout.fillHeight: true
color: "white"
}
Item {
Layout.fillWidth: true
implicitWidth: mid_item_TEXT_OR_IMAGE.childrenRect.width
implicitHeight: mid_item_TEXT_OR_IMAGE.childrenRect.height
Item{ //THIS PART HERE SHOULD ALWAYS BE THE CHILDREN ITEMS
id:mid_item_TEXT_OR_IMAGE
width: childrenRect.width
height: childrenRect.height
anchors.centerIn: parent
}
}
Rectangle {
width: 1
Layout.fillHeight: true
color: "white"
}
}
By using the implicitWidth and implicitHeight, the RowLayout will also grow according to the content that you put in. The other way around also works, you can give width/height for MyCustomItem on the place where you use it
I think what you're looking for is a default property. If you define your default property to be the data field of your Item, then by default all child objects will become children of that Item.
//MyCustomItem.qml
RowLayout{
default property alias contents: mid_item_TEXT_OR_IMAGE.data
Item{
id:mid_item_TEXT_OR_IMAGE
Layout.alignment: Qt.AlignHCenter
}
}

How to optimize many dom reflow operations caused by getComputedStyle?

I'm building solution for business PDF like HTML elements splitting based on their content height which can be used for example - with puppeter/wkhtmltopdf to render suitable layout as much similar with my HTML/JS solution as possible. My goal is also to create unlimited to margins/padding/box-sizing way to styling it in CSS.
For better explanation, imagine div container with constant height, 1000px for example, but content is unlimited, and everytime its sum will take more than thats div specified height, we have to clone that page with appriopriate layout and content which overflows its boundary.
One of use cases is to use it in React because it has very high possibility to make it fancy with his reactive functionalities (Live pdf preview). But building it with react was such a pain, so I decided to create it in plain javascript.
I'm watching for DOM update using MutationObserver
private _initOnDomUpdateEvents(observer: Element, callback?: () => any) {
const mutation = new MutationObserver(() => {
callback();
});
mutation.observe(observer, {
attributes: true,
subtree: true,
childList: true
})
this.watchers.push(mutation);
return mutation;
}
And everytime the DOM has ben updated in observer scope (for example in .css selector named .sheet), I'm creating tree of its children which contains details about their relative height, offsetY, margins, paddings, borders, box-sizing, overflow and display - It has to be very accurate, pixel perfect almost. But to archive this, I've used very expansive solution, and optimisation gives me headache everytime I think of it.
To imagine my problem, look at this.
class Tree {
public getElementTree(element: Element): TreeElement {
const main = Tree._getChildrenBoundaryDetails(element, element);
main.setChildren(Tree._buildDimensionsTree(main, element));
return main;
}
private static _buildDimensionsTree(parent: TreeElement, element: Element) {
return Array.from(element.children).map(e => {
const dimensions = Tree._getChildrenBoundaryDetails(parent.getDomElement(), e)
dimensions.setChildren(Array.from(e.children).map(e => Tree._getChildrenBoundaryDetails(parent.getDomElement(), e)))
return dimensions;
})
}
private static _getChildrenBoundaryDetails(parent: Element, element: Element): TreeElement {
const style = getComputedStyle(element);
const offsetHeightTop = element.getBoundingClientRect().top
- parent.getBoundingClientRect().top;
return new TreeElement({
borderBox: style.boxSizing === 'border-box',
element: element,
offsetY: {
from: offsetHeightTop,
to: offsetHeightTop + parseFloat(style.height)
},
margin: {
top: parseFloat(style.marginTop),
bottom: parseFloat(style.marginBottom)
},
padding: {
top: parseFloat(style.paddingTop),
bottom: parseFloat(style.paddingBottom),
},
border: {
top: parseFloat(style.borderTopWidth),
bottom: parseFloat(style.borderBottomWidth)
}
})
}
}
Do you see that recursion in Tree._buildDimensionsTree? It's terrifying if you imagine, how many children may exists in real world examples, and for each, you have to call window.getComputedStyle. So I'm comming with question.
How to create more CPU friendly solution which don't requires as many DOM Reflows as in my example.
Thank you for help.

Monaco editor dynamically resizable

I have been searching for a discussion about if it's possible to mimic the html tag textarea's resizing when using Monaco Editor's field all over the Internet but I couldn't find one answering my question.
I'm using the monaco-editor npm package in a React application. Do you have any idea if this is easy to implement?
Thank you in advance!
SOLUTION
With pure css I selected the target html element and just added these properties:
div {
resize: vertical;
overflow: auto;
}
TL;DR: add automaticLayout: true to your editor's configuration.
NL;PR:
Monaco has a built-in auto resize to parent container functionality:
createEditorWithAutoResize(){
this.editor = monaco.editor.create(
this.editorDiv.current, {
value: "var x = 0;",
language: 'javascript',
automaticLayout: true // <<== the important part
}
);
}
componentDidMount(){this.createEditorWithAutoResize();}
constructor(props){super(props); this.editorDiv = React.createRef();}
render(){return <div ref={this.editorDiv} className="editor" ></div>}
And the CSS for the editor (it avoids rendering the editor for the first time with like 10px height):
.editor{
height: 100%;
}
First tested: v0.10.1, Last tested: v0.32.1
Note:
< v0.20.0: The mechanism does not listen to its container size changes, it polls them.
#nrayburn-tech (Monaco Editor's contributor): Version 0.20 uses MutationObserver for all browsers. Version 0.21 and later uses ResizeObserver on supported browsers, otherwise, it uses polling as a fallback.
if you have a reference to the editor you can just call
editor.layout()
on some resize event.
For example, on window resize:
window.onresize = function (){
editor.layout();
};
For anyone coming here having this issue in a basic web app (html, css, javascript) I've found a solution for the resizing issue I'm experiencing.
I have the monaco editor in a resizable flex container. It will only grow the width, not shrink it, and vertical resizing doesn't seem to work out of the box.
If you use the monaco config "automaticLayout: true" and the following CSS it seems to resize as expected:
.monaco-editor { position: absolute !important; }
I tried the max-width 99% trick but it causes a laggy delayed effect when increasing the width near edge of page.
For posterity, the solution I arrived on was to set automaticLayout: false so that I could perform all the layout in a resize event listener.
const placeholder = document.getElementById('placeholder')
const editor = monaco.editor.create(placeholder, {
value: '// hello world',
language: 'javascript',
automaticLayout: false // or remove, it defaults to false
})
// we need the parent of the editor
const parent = placeholder.parentElement
window.addEventListener('resize', () => {
// make editor as small as possible
editor.layout({ width: 0, height: 0 })
// wait for next frame to ensure last layout finished
window.requestAnimationFrame(() => {
// get the parent dimensions and re-layout the editor
const rect = parent.getBoundingClientRect()
editor.layout({ width: rect.width, height: rect.height })
})
})
By first reducing the editor layout to 0 we can safely query the dimensions of the parent element without the child (editor) contributing to its size. We can then match the editor to the new parent dimensions. Since this takes place over a single frame, there should be no flickering or lag.
this is old question but get the problem to and solved it with react-resize-detector
based on ResizeObserver it feet perfectly to the need (check browser compatibility)
Exemple of component :
import React, { Component } from 'react';
import ReactResizeDetector from 'react-resize-detector';
import * as monaco from 'monaco-editor';
class Editor extends Component {
constructor(props) {
super(props)
this.state = {
width: 0,
height: 0,
}
this.editor_div = React.createRef()
this.handle_rezise = this.handle_rezise.bind(this);
}
componentDidMount() {
const editor_model = monaco.editor.createModel('', 'sql');
this.monaco_editor = monaco.editor.create(this.editor_div.current, this.props.editorOptions);
this.monaco_editor.setModel(editor_model);
}
componentWillUnmount() {
this.monaco_editor && this.monaco_editor.dispose();
}
handle_rezise(width, height) {
this.monaco_editor.layout({ height, width });
}
render() {
return(
<div
className="editor-container"
style={{ height: '100%' }}>
<ReactResizeDetector
handleWidth
handleHeight
onResize={ this.handle_rezise }
refreshMode="debounce"
refreshRate={100} />
<div
className="editor"
ref={ this.editor_div }
style={{ height: '100%' }} />
</div>
)
}
}
export default Editor;
Hope it's help
In my case I'm using that exact CSS but although automaticLayout: true works, I found out overkill (seems to pooling the DOM 100ms interval and I have several editors opened in the document. SO I ended up implementing it manually :
just in case , my needs are different: I want the user to resize it the container - in a standard way and cheap (both on code and performance) on libraries and performance. This is what I did:
css container : resize: vertical; overflow: auto
and this js :
function installResizeWatcher(el, fn, interval){
let offset = {width: el.offsetWidth, height: el.offsetHeight}
setInterval(()=>{
let newOffset = {width: el.offsetWidth, height: el.offsetHeight}
if(offset.height!=newOffset.height||offset.width!=newOffset.width){
offset = newOffset
fn()
}
}, interval)
}
const typeScriptCodeContainer = document.getElementById('typeScriptCodeContainer')
typeScriptCodeEditor = monaco.editor.create(typeScriptCodeContainer, Object.assign(editorOptions, {value: example.codeValue}))
installResizeWatcher(typeScriptCodeContainer, typeScriptCodeEditor.layout.bind(typeScriptCodeEditor), 2000)
yes, 2 seconds interval and make sure it registers only once. I see there is / was a resize interval on 100ms for the automatic relayout in monaco - IMHO that's too much.
See it in action: https://typescript-api-playground.glitch.me/?example=2

Remember scroll position in QML element

I have some qml that acts as the output from the application (kind of like a read-only console). However, it's annoying to use because as it prints information it scrolls back to the top.
For example, lets say I print a line to my TextArea every second, after a minute or so, I'll have enough lines that I now have a scroll bar. I scroll to the bottom, a second later, a line of text is printed causing it to scroll back to the top.
The desired functionality I would like is to automatically scroll to the bottom (much like how a console is when it prints stuff out) unless the user overrides this by scrolling up, then the text should stay put. I hope that made sense, here is some code:
ScrollArea {
id: helpTextScrollArea
anchors.left: parent.left
anchors.right: parent.right
anchors.top: myButton.bottom
anchors.topMargin: 5
anchors.bottom: parent.bottom
visible: false
horizontalScrollBar.visible: false
onVisibleChanged: {
helpText.visible = visible
}
HelpTextArea {
id: helpText
width: parent.parent.width - helpTextScrollArea.verticalScrollBar.width
text: "Oops, no documentation."
onVisibleChanged: {
if(helpTextScrollArea.visible != visible) {
helpTextScrollArea.visible = visible
}
}
}
}
Flickable
{
id: flick
anchors.left: parent.left
anchors.right: parent.right
anchors.top: runStopButton.bottom
anchors.bottom: parent.bottom
anchors.topMargin: 5
TextArea{
id: messageArea
anchors.fill: parent
focus: true
readOnly: true
visible: !helpTextScrollArea.visible
wrapMode: TextEdit.Wrap
function addText(newText) {
text += newText + "\n"
}
}
}
Note: I don't think my Flickable does anything, it was part of my experimenting to fix the problem. Also, I use the function addText to print to the text area. I hardly know anything about qml and most of this code was written by someone else and I'm trying to work with it. Thank you!
You can bind contentY property of Flickable to an expression that will calculate proper position.
Flickable {
id: flick
states: State {
name: "autoscroll"
PropertyChanges {
target: flick
contentY: messageArea.height - height
}
}
onMovementEnded: {
if (contentY === messageArea.height - height) {
state = "autoscroll"
}
else {
state = "" // default state
}
}
// ...
}
Flickable has got 4 readonly properties in called visibleArea.* (you can read their meaning in the QML documentation) :
visibleArea.xPosition : xPosition = contentX / contentWidth
visibleArea.yPosition : yPosition = contentY / contentHeight
visibleArea.widthRatio : widthRatio = width / contentWidth
visibleArea.heightRatio : heightRatio = height / contentHeight
If you are using GridView instead of Flickable, you can use these methods.
positionViewAtBeginning()
positionViewAtEnd()
positionViewAtIndex(int index, PositionMode mode)
I found these to be really helpful, as when the line of text is being added, just call the positionViewAtEnd() method inside of the GridView.
Hope it helps

Help with adding CSS to jquery

I am really new to jquery and I dont really have time to see how it works (I work with PHP a lot) I want to make a jquery slideshow but I dont know how to add z-index to the images here: http://www.albavid.com/new/index.html
I can see that jquery adds some css elements like opacity, width.. etc.
I tried to add a bigger z-index to the current image and lower z-index to the right images and left images.
Can anyone tell me how to add css with jquery so the current image(the biggest) to be on the top and then the others behind it.
Replace your javascript on the page with this:
I added in $(el).css('z-index', 5) and $(el).css('z-index', 0) in currentCss, beforeCss, and afterCss.
jQuery( document ).ready( function(){
jQuery( '#flip' ).jcoverflip({
current: 2,
beforeCss: function( el, container, offset ){
$(el).css('z-index', 0);
return [
$.jcoverflip.animationElement( el, { left: ( container.width( )/2 - 210 - 110*offset + 20*offset )+'px', bottom: '20px' }, { } ),
$.jcoverflip.animationElement( el.find( 'img' ), { width: Math.max(10,200-10*offset*offset) + 'px' }, {} )
];
},
afterCss: function( el, container, offset ){
$(el).css('z-index', 0);
return [
$.jcoverflip.animationElement( el, { left: ( container.width( )/2 + 110 + 110*offset )+'px', bottom: '10px' }, { } ),
$.jcoverflip.animationElement( el.find( 'img' ), { width: Math.max(10,200-10*offset*offset) + 'px' }, {} )
];
},
currentCss: function( el, container ){
$(el).css('z-index', 5);
return [
$.jcoverflip.animationElement( el, { left: ( container.width( )/2 - 200 )+'px', bottom: 0 }, { } ),
$.jcoverflip.animationElement( el.find( 'img' ), { width: '400px' }, { } )
];
},
change: function(event, ui){
jQuery('#scrollbar').slider('value', ui.to*25);
}
});
jQuery('#scrollbar').slider({
value: 50,
stop: function(event, ui) {
if(event.originalEvent) {
var newVal = Math.round(ui.value/25);
jQuery( '#flip' ).jcoverflip( 'current', newVal );
jQuery('#scrollbar').slider('value', newVal*25);
}
}
});
});
you can also get it working by using different classes the use addclass() - removeClass() to toggle the effect works well if you're already familiar with CSS overrides
Try setting the position property of the elements to relative, otherwise they will be treated as static and ignore all settings to z-index.
It looks like the z-index in question should be applied to the <li>, not the <img>.
You can alter the z-index of an element by using the .css() function.
$("#elem").css('z-index','1000');
However, two better options would be to either define CSS classes for the elements you want to style and toggle between them, or use some nice jQuery slideshow plugins.
You can alter the inline CSS by selecting the element and using the css method
Suppose an element has no z-index, or a different one, and you want to change it to 5
$("#elementId").css('z-index', 5);
Another option is to set up classes with certain CSS styles before hand and change the class of certain elements ie (currentShown, nextUp, etc)
rather then waste your time reinventing the wheel why not just use a jquery slideshow plugin?
Z-index'ing the <li> instead of the <img> is the right solution to fix the overlap. However, because the <li>s are dynamic and flip before and after each other, an applied z-index to all in a sequential order from left-to-right will not solve his issue.
Solving the issue will need a few more actions to invoke within jquery but not too much extra work. What needs to happen is for jQuery to apply a z-index to the current <li> higher than the rest of the batch for each event on the slider.
Since I don't have access to your code, you basically want to apply the following logic:
Whatever <li> is in front it should have a z-index:1 and all other <li>s should have z-index:0. This should happen on every change in the slider.
#mcgrailm: Give him a break man, you can't learn how to swim unless you jump into the pool.

Categories

Resources