resoai / TileBoard

A simple yet highly configurable Dashboard for HomeAssistant
MIT License
1.63k stars 278 forks source link

feat: onLayout hook triggered on items being added/remove #703

Closed rchl closed 3 years ago

rchl commented 3 years ago

When implementing some custom, automatic layout that needs to recalculate positions and/or sizes on items being added or removed dynamically a hook like this one might be useful.

This is actually for my own custom layout that I'm using to position and size items automatically but it could be useful for others too.

timota commented 3 years ago

Looks interesting. Can you provide examples how it can be used, lets say for calculation layout and sizes? Mostly looking what do you meant when item added/removed.

Thanks

rchl commented 3 years ago

Here is something I'm using but I'm not fully satisfied with it.

I would define this layout in the config file:

const AVAILABLE_WIDTH = window.innerWidth;

const AUTO_LAYOUT = function (page, group) {
   const items = group.items;
   const options = group.itemsLayoutOptions || {};

   options.isHorizontal = typeof(options.isHorizontal) === 'boolean' ?
      options.isHorizontal : true;
   options.mainAxisLimit = typeof(options.mainAxisLimit) === 'number' ?
      options.mainAxisLimit : 0;
   options.scaleToFit = typeof(options.scaleToFit) === 'boolean' ?
      options.scaleToFit : false;

   const TILE_SIZE = page.tileSize || CONFIG.tileSize;
   const TILE_MARGIN = page.tileMargin || CONFIG.tileMargin;

   const SIZE_MAIN_AXIS = options.isHorizontal ? 'width' : 'height';
   const SIZE_CROSS_AXIS = options.isHorizontal ? 'height' : 'width';

   let mainAxisIndex = 0;
   let crossAxisIndex = 0;

   // Items grouped by row/column.
   const groupedItems = [];

   for (const item of items) {
      if (!item[SIZE_MAIN_AXIS]) {
         item[SIZE_MAIN_AXIS] = 1;
      }

      if (options.mainAxisLimit
          && (mainAxisIndex + item[SIZE_MAIN_AXIS]) > options.mainAxisLimit) {
         mainAxisIndex = 0;
         crossAxisIndex += 1;
      }

      let crossAxisContainer = groupedItems[crossAxisIndex];

      if (!crossAxisContainer) {
         crossAxisContainer = [];
         groupedItems.push(crossAxisContainer);
      }

      crossAxisContainer.push(item);

      if (!item[SIZE_CROSS_AXIS]) {
         item[SIZE_CROSS_AXIS] = 1;
      }

      mainAxisIndex += item[SIZE_MAIN_AXIS];
   }

   const calculateScaleFactor = items => {
      if (!options.scaleToFit) {
         return 1;
      }

      const itemsWidth = items.reduce((accum, item) => {
         return accum + item[SIZE_MAIN_AXIS];
      }, 0);

      // How many full tiles fit within available space?
      const fitCount = Math.floor(AVAILABLE_WIDTH / TILE_SIZE);
      // How much space would margins take for visible amount of tiles.
      const tileMargins = (fitCount - 1) * TILE_MARGIN;
      // How much to scale tiles to make them fill the screen.
      return (AVAILABLE_WIDTH - tileMargins) / (itemsWidth * TILE_SIZE);
   };

   let mainAxisPos = 0;
   let crossAxisPos = 0;
   let biggestCrossAxisSize = 0;

   for (const mainAxisContainer of groupedItems) {
      const scaleFactor = calculateScaleFactor(mainAxisContainer);

      for (const [mainAxisIndex, item] of mainAxisContainer.entries()) {
         if (mainAxisIndex === 0) {
            mainAxisPos = 0;
            crossAxisPos += biggestCrossAxisSize;
            biggestCrossAxisSize = 0;
         }

         item.position = options.isHorizontal ? [mainAxisPos, crossAxisPos] : [
            crossAxisPos, mainAxisPos,
         ];

         item.width = item.width * scaleFactor;
         item.height = item.height * scaleFactor;

         mainAxisPos += item[SIZE_MAIN_AXIS];

         biggestCrossAxisSize = Math.max(biggestCrossAxisSize, item[
            SIZE_CROSS_AXIS]);
      }
   }

   return items;
}

And then in chosen groups I'd add options like:

               onLayout: AUTO_LAYOUT,
               itemsLayoutOptions: {
                  isHorizontal: true,
                  mainAxisLimit: 4.5,
                  scaleToFit: true,
               },

This allows me to not have to set width/height/position on tiles and let them be automatically layed-out. I only use it on mobile.

rchl commented 3 years ago

Mostly looking what do you meant when item added/removed.

As for that, I run a code from onReady hook to populate groups with items. For example to automatically gather all sensors on the same page.