I'm developing a calendar component for Angular and I need to position some div element (representing events) in a grid.
this.days.forEach((day: SchedulerViewDay, dayIndex: number) => {
day.events = this.getEventsInPeriod({...}).map((event: CalendarSchedulerEvent) => {
const segmentDuration: number = 60.0 / this.hourSegments;
const dayStartDate: Date =
setSeconds(setMinutes(setHours(
setDate(setMonth(setYear(new Date(), day.date.getFullYear()), day.date.getMonth()), day.date.getDate()), this.dayStartHour), this.dayStartMinute), 0);
const segmentsNumber: number = (differenceInMinutes(event.start, dayStartDate) / segmentDuration);
return <CalendarSchedulerEvent>{
...
height: this.hourSegmentHeight * (differenceInMinutes(event.end, event.start) / segmentDuration),
top: (this.hourSegmentHeight * segmentsNumber)
};
});
With that code, this is the result:
As you can see the event div is not correctly positioned. I think that this is due to the grid cell borders so i've applied that fix:
top: (this.hourSegmentHeight * segmentsNumber) + (segmentsNumber / 2) // 1px for each separator line
Each border is 1px width and I consider only solid borders and not dashed ones (this is the motivation of / 2.
This almost resolve the problem. There's still a misalignment.
Moreover this is not an elegant solution but I can't figure out to make it more clean.
How can I correctly position these divs?
Thank you very much!
EDIT: implementation of inorganik's solution (first answer)
I've created the following mixins in my scss file:
#mixin day-x($attr, $attr-count: 7, $attr-steps: 1, $unit: '%') {
$attr-list: null;
#for $i from 1 through $attr-count {
$attr-value: $attr-steps * $i;
&.day#{$i} {
#{$attr}: #{$attr-value}#{$unit};
}
$attr-list: append($attr-list, unquote(".#{$attr}-#{$attr-value}"), comma);
}
#{$attr-list} {
//append style to all classes
}
}
#mixin time-x($attr, $start-hour: 0, $end-hour: 23, $attr-steps: 2, $minutes-steps: 15, $unit: '%') {
$attr-list: null;
$hours: $start-hour;
$minutes: 0;
$attr-count: ((($end-hour + 1) - $start-hour) * 60) / $minutes-steps;
#for $i from 0 through $attr-count {
$attr-value: $attr-steps * $i;
#if($i > 0) {
$minutes: $minutes + $minutes-steps;
#if($minutes == 60) {
$minutes: 0;
$hours: $hours + 1;
}
}
$hoursString: '#{$hours}';
#if($hours < 10) {
$hoursString: '0#{$hours}';
}
$minutesString: '#{$minutes}';
#if($minutes < 10) {
$minutesString: '0#{$minutes}';
}
&.time#{$hoursString}#{$minutesString} {
#{$attr}: #{$attr-value}#{$unit};
}
$attr-list: append($attr-list, unquote(".#{$attr}-#{$attr-value}"), comma);
}
#{$attr-list} {
//append style to all classes
}
}
#mixin length-x($attr, $attr-steps: 2, $minutes-steps: 15, $unit: '%') {
$attr-list: null;
$attr-count: 24 * 60 / $minutes-steps;
#for $i from 0 through $attr-count {
$attr-name: $minutes-steps * $i;
$attr-value: $attr-steps * $i;
&.length#{$attr-name} {
#{$attr}: #{$attr-value}#{$unit};
}
$attr-list: append($attr-list, unquote(".#{$attr}-#{$attr-value}"), comma);
}
#{$attr-list} {
//append style to all classes
}
}
#include day-x('left', 7, 5);
#include time-x('top', 6, 22, 1.47);
#include length-x('height', 1.47);
Then I apply these styles with [ngClass] attribute, calling the following method (I don't need getDay, for now):
getPositioningClasses(event: CalendarSchedulerEvent): string {
const classes: string[] = [
this.getDayClass(event.start),
this.getTimeClass(event.start),
this.getLengthClass(differenceInMinutes(event.end, event.start))
];
return classes.join(' ');
}
private getDayClass(date: Date): string {
return '';
}
private getTimeClass(date: Date): string {
let hours: string = date.getHours() < 10 ? `0${date.getHours()}` : `${date.getHours()}`;
let minutes: string = date.getMinutes() < 10 ? `0${date.getMinutes()}` : `${date.getMinutes()}`;
return `time${hours}${minutes}`;
}
private getLengthClass(durationInMinutes: number): string {
return `length${durationInMinutes}`;
}
This is the result:
Graphically it works like a charm but I need to keep segments height (58px) aligned to the percentage increment used to generate positioning classes (now it's 1.47%).
Is there a way to make those variables (segments height and percentage height and top position increments) from Angular code, making the segment height configurable by the user and let these increments to adapt itself?
Thank you very much!
EDIT 2: another problem
Hi again! I've encountered another problem.
The component hour range it is not fixed but it is configurable from outside.
In the example in the question is 6AM - 22PM. For that range the percentage of 1.47 is fine but if I change the range (eg. 0AM - 22AM) the calendar height is higher and the percentage is not ok anymore.
I think I need to calculate those positioning values from Typescript. But I can't figure out how to do that.
To try something I'm trying this:
ngAfterViewInit(): void {
this.calendarContainerHeight = this.calendarContainer.nativeElement.clientHeight;
const segmentPercentage: number = 100.0 * this.hourSegmentHeight / this.calendarContainerHeight;
console.log("calendarContainerHeight: ", this.calendarContainerHeight);
console.log("segmentPercentage: ", segmentPercentage);
setTimeout(() => {
this.view.days.forEach(d => {
d.events.forEach((event: CalendarSchedulerEvent) => {
let hours: number = event.start.getHours();
if (this.dayStartHour > 0) { hours = hours - this.dayStartHour; }
const numberOfSegments: number = hours * this.hourSegments;
event.top = numberOfSegments * segmentPercentage;
});
});
});
}
Then I've added [style.top.%]="event.top" to the event div. This is the result (ignore height for now, I've not managed them yet):
As you can see the percentage is not accurate and those events which are in the middle of the day (or towards the end of the day) are not positioned correctly.
How could I solve this problem?
Thank you very much, again!
It seems like a brittle solution to calculate pixel distances in code. I would use CSS classes. You can have an SCSS mixin generate all the possible classes for you for days, times and segment lengths. Make 3 methods that return css class strings, one for day, one for time, and one for segment length which you can apply with [ngClass]. Then a method which calls each one, something like below. This assumes you have an array of segments which store date/time/length info on them:
getPositioningClasses(segment: any): string {
const classes = [this.getDayClass(segment.dayStartDate), this.getTimeClass(segment.dayStartDate), this.getLengthClass(segment.timeLength)];
return classes.join(' ');
}
So your markup might end up looking like this:
<segment [ngClass]="getPositioningClasses(segment)"></segment>
Compiled:
<segment class="day0 time1430 length130"></segment>
day0 - you could have 0-X for however many days you display. This class is just responsible for distance from the left. Use percentages if you can so that the layout can be flexible.
time1430 in this case 2:30pm - This class is just responsible for distance from the top
length130 in this case an hour and a half. Ideally set the smallest increment of time (15m?) as a percentage height, then let the mixin multiply the increment for each possible length.
Also to make sure you don't have to factor in borders, put the box-sizing: border-box css rule on pretty much everything.
Related
I'm implementing a range slider by months using ngx-slider.
I want the value to be 1-12 and the display legend to be January-December.
I achieve it using the code below.
export class AppComponent {
minValue: number = 3;
maxValue: number = 9;
options: Options = {
floor: 1,
ceil: 12,
translate: (value: number, label: LabelType): string => {
switch (label) {
case LabelType.Low:
return "<b>March</b>";
case LabelType.High:
return "<b>October</b>";
default:
return "$" + value;
}
}
};
}
And the result is this one:
Link to the image result
How can I remove the dollar sign of the start and end value?
Or do you have better recommendation npm library for range slider for months?
Hello this time slider I used in the project
https://refreshless.com/nouislider/examples/
You can change the style using CSS,
Hope it can be adopted.
I want to override the functionality of an HTMLElement so that the scrollTop property, on get or set, will do some extra logic before touching the actual value. The only way, as far as I have found, is to delete the original property and then use Object.defineProperty():
delete element.scrollTop;
Object.defineProperty(element, "scrollTop", {
get: function() : number {
...
},
set: function(value: number) {
...
}
});
However, this removes all access to the original scrollTop, so the new logic can't do something like return base.scrollTop. The comments on this older, similar question claim that getting access to the original value is not possible when overriding with Object.defineProperty().
I'm wondering if a possible alternative is to create an adapter class that implements the HTMLElement interface and wraps the HTMLElement in question. All implemented properties delegate to the wrapped element's properties, but its scrollTop would do the extra work I need.
I'm quite new to Typescript, but is the alternative possible? If so, is there a lightweight way of defining all other properties on the adapter that we're not touching to automatically delegate to the wrapped element?
Here is my solution to the problem (which is a bit more tricky than might seem)
I implemented some utility for that and usage look like:
Let's say we want to replace some div 'scrollTop' method to return 50 if scrollTop is bigger than 50 and return original value if it is less than 50:
const div = document.createElement("div")
replaceOriginalPropertyDescriptor(div, "scrollTop", (originalDescriptor, div) => {
return {
// Keep rest of the descriptor (like setter) original
...originalDescriptor,
get() {
const originalScrollTop = originalDescriptor.get!.apply(div);
if (originalScrollTop > 50) return 50;
return originalScrollTop
},
}
})
Here is implementation with bunch of comments:
/**
* Will replace property descriptor of target while allowing us to access original one.
*
* Example:
*
* Will replace div.scrollTop to return 50, if original scrollTop is bigger than 50. Otherwise returns original scrollTop
*
* const div = document.createElement("div")
*
* replaceOriginalPropertyDescriptor(div, "scrollTop", (originalDescriptor, div) => {
* return {
* get() {
* const originalScrollTop = originalDescriptor.get.apply(div);
*
* if (originalScrollTop > 50) return 50;
*
* return originalScrollTop
* },
* // Keep rest of the descriptor (like setter) original
* ...originalDescriptor,
* }
* })
*/
function replaceOriginalPropertyDescriptor<T extends object>(
input: T,
property: keyof T,
newDescriptorCreator: (originalDescriptor: PropertyDescriptor, target: T) => PropertyDescriptor
) {
const originalDescriptor = findPropertyDescriptor(input, property);
if (!originalDescriptor) {
throw new Error(`Cannot replace original descriptor ${String(property)}. Target has no such property`);
}
const newDescriptor = newDescriptorCreator(originalDescriptor, input);
Reflect.defineProperty(input, property, newDescriptor);
}
What was my use case:
I am using drag and drop library that is reading scroll positions like 500 times a second. I wanted to cache this value for lifetime of a single frame. As I cannot control source code of the library itself, I am kinda injecting this cache to HTMLElements itself so they keep previous scroll position values for 1 frame.
Angular 6/7, Material Design.
Since I don't have access to the total number of items the item count is irrelevant (the box in the screen shot).
How do I remove the item count completely? Or alternatively show the page I'm currently on instead of the item count?
<mat-paginator
itemsPerPageLabel="Items per page"
(page)="changePage()"
[length]="resultsLength"
[pageSizeOptions]="[10, 100]">
</mat-paginator>
Remove the range label by inserting in global CSS
.mat-paginator-range-label {
display: none;
}
Insert page number instead (of course based on your API - you might not have the page info!) by inserting in your component
ngAfterViewChecked() {
const list = document.getElementsByClassName('mat-paginator-range-label');
list[0].innerHTML = 'Page: ' + this.page.toString();
}
and of course delete the CSS rule above!
Paginator now looks like this
I just modified Johan Faerch's solution to fit more to your question.
Create method which has two parameters, one for matpaginator and another for list of HTMLCollectionOf
paginatorList: HTMLCollectionOf<Element>;
onPaginateChange(paginator: MatPaginator, list: HTMLCollectionOf<Element>) {
setTimeout((idx) => {
let from = (paginator.pageSize * paginator.pageIndex) + 1;
let to = (paginator.length < paginator.pageSize * (paginator.pageIndex + 1))
? paginator.length
: paginator.pageSize * (paginator.pageIndex + 1);
let toFrom = (paginator.length == 0) ? 0 : `${from} - ${to}`;
let pageNumber = (paginator.length == 0) ? `0 of 0` : `${paginator.pageIndex + 1} of ${paginator.getNumberOfPages()}`;
let rows = `Page ${pageNumber} (${toFrom} of ${paginator.length})`;
if (list.length >= 1)
list[0].innerHTML = rows;
}, 0, paginator.pageIndex);
}
How to call this method? you can initialize this on ngAfterViewInit()
ngAfterViewInit(): void {
this.paginatorList = document.getElementsByClassName('mat-paginator-range-label');
this.onPaginateChange(this.paginator, this.paginatorList);
this.paginator.page.subscribe(() => { // this is page change event
onPaginateChange(this.paginator, this.paginatorList);
});
}
Include this method in your css file(note: do not include in the main styles.css file)
.mat-paginator-range-label {
display: none;
}
You can call onPaginateChange(this.paginator, this.paginatorList) functions wherever you need to change the page number details other than clicking on the navigation buttons on the mat paginator.
Result looks like this
i'm currently learning html and css. I'm working on a blogger blog template.
My template has a front page of post thumbnails.
I have set a class for my default thumbnail for when my posts do not have an image and I have set the url in the css.
.altthumbnails {
background: url(myimagelocation.jpg);
}
is there any way I can make more than one default thumbnail? I would like to chose maybe three images to show at random when there is no post image.
Thanks for any replies
You can not do it only with pure css. But you can do it either by the server-side code you use or js which the one I suggest. Basicly one of the easiest solution for this is you can make different classes for each different image you wanna use, then you can produce a random number in js and use this number to apply different classes to your elements. Here's an example:
DEMO
In the example there are 5 different css class that supply 5 different background-image and all their names end with numbers 1 >> 5.
.thumb_1 { .. }
.thumb_2 { .. }
.thumb_3 { .. }
.thumb_4 { .. }
.thumb_5 { .. }
In javascript we're gonna loop through the objects, generate a random number 1 >> 5 and use this number to add a new class like this.
var div = document.getElementsByClassName("thumb");
//var rand = Math.floor(Math.random() * 5 ); // for just one image
for (var i = 0; i < div.length; i++){
var rand = Math.floor(Math.random() * 5 ); // for multiple images
div[i].className += " thumb_" + (rand + 1) ;
};
I'm trying to make a custom GTK widget in Vala, but I'm already failing at the very first basic attempt, so I'd like some help in knowing where I'm going wrong. I feel like I must be missing something painstakingly obvious, but I just can't see it.
I have three files with the following contents:
start.vala:
using Gtk;
namespace WTF
{
MainWindow main_window;
int main(string[] args)
{
Gtk.init(ref args);
main_window = new MainWindow();
Gtk.main();
return 0;
}
}
main_window.vala:
using Gtk;
namespace WTF
{
public class MainWindow : Window
{
public MainWindow()
{
/* */
Entry entry = new Entry();
entry.set_text("Yo!");
this.add(entry);
/* */
/*
CustomWidget cw = new CustomWidget();
this.add(cw);
/* */
this.window_position = WindowPosition.CENTER;
this.set_default_size(400, 200);
this.destroy.connect(Gtk.main_quit);
this.show_all();
}
}
}
custom_widget.vala:
using Gtk;
namespace WTF
{
public class CustomWidget : Bin
{
public CustomWidget()
{
Entry entry = new Entry();
entry.set_text("Yo");
this.add(entry);
this.show_all();
}
}
}
As you can see, in main_window.vala, I have two sets of code. One that adds the Entry widget directly, and one that adds my custom widget. If you run the one that adds the Entry widget directly, you get this result:
If you run the one with the custom widget, however, you get this result:
Just for the record, this is the complication command I use:
valac --pkg gtk+-2.0 start.vala main_window.vala custom_widget.vala -o wtf
EDIT:
Following user4815162342's suggestion, I implemented the size_allocate method on my custom Bin widget, like so:
public override void size_allocate(Gdk.Rectangle r)
{
stdout.printf("Size_allocate: %d,%d ; %d,%d\n", r.x, r.y, r.width, r.height);
Allocation a = Allocation() { x = r.x, y = r.y, width = r.width, height = r.height };
this.set_allocation(a);
stdout.printf("\tHas child: %s\n", this.child != null ? "true" : "false");
if (this.child != null)
{
int border_width = (int)this.border_width;
Gdk.Rectangle cr = Gdk.Rectangle()
{
x = r.x + border_width,
y = r.y + border_width,
width = r.width - 2 * border_width,
height = r.height - 2 * border_width
};
stdout.printf("\tChild size allocate: %d,%d ; %d, %d\n", cr.x, cr.y, cr.width, cr.height);
this.child.size_allocate(cr);
}
}
It writes the following in the console:
Size_allocate: 0,0 ; 400,200
Has child: true
Child size allocate: 0,0 ; 400, 200
And the window renders thusly:
GtkBin is an abstract single-child container, typically intended to decorate the child widget in some way, or change its visibility or size. Without some added value, a single-child container would be indistinguishable from the widget it contains and therefore not very useful.
Since GtkBin doesn't know what kind of decorations you will draw around the child, it expects you to implement your own size_allocate. A simple implementation is available in gtk_event_area_size_allocate, a more complex one in gtk_button_size_allocate.
This answer shows a minimal size_allocate implementation in PyGTK which should be straightforward to port to Vala. If you do anything more complex than that, you will need to also implement expose, and possibly other methods, but this will get you started.