diff --git a/AG_GRID_PROGRESS.md b/AG_GRID_PROGRESS.md index e1cf65b..65271b4 100644 --- a/AG_GRID_PROGRESS.md +++ b/AG_GRID_PROGRESS.md @@ -962,6 +962,21 @@ Point de deploiement: - correctif final: `_force_horizontal_scroll_width()` additionne maintenant les largeurs reelles `getActualWidth()` et ne modifie plus jamais les largeurs de colonnes - invariant: une fonction de scrollbar peut dimensionner le conteneur de scroll, mais ne doit jamais changer la taille des colonnes utilisateur +### A faire - tri en mode grouping AG Grid + +- demande UX: permettre `asc` / `desc` / aucun tri sur les colonnes meme quand le grouping AG Grid manuel est actif +- la configuration de vue doit memoriser ce tri, comme les colonnes, filtres checklist, `valueFilters` et grouping +- point de vigilance majeur: ne pas simplement remettre `sortable: true` avec le tri natif AG Grid sur le `rowData` groupe +- raison: le grouping est construit manuellement dans `_flatten_grouped_rows()` avec lignes `group`, `record`, `total`; un tri natif AG Grid apres coup peut separer les records de leurs groupes ou deplacer sous-totaux/totaux +- approche recommandee: + - capturer l'intention de tri utilisateur dans l'etat de vue + - trier les records source avant `_flatten_grouped_rows()` + - pour une colonne de grouping, trier l'ordre des groupes a ce niveau + - pour une colonne non groupee, trier les records a l'interieur des groupes + - reconstruire ensuite `rowData` en preservant `expandedPaths`, sous-totaux, grand total et filtres checklist actifs +- invariant: en mode grouping, AG Grid ne doit jamais trier directement les lignes deja groupees; SAO doit trier les records avant construction des groupes +- tests a prevoir: asc/desc/none, colonne groupee, colonne non groupee, groupes ouverts/fermes, sous-totaux, grand total, filtres checklist actifs, sauvegarde/restauration de preset, retour a `Default view` + ### Correctif largeur formulaire avec menu gauche masque - bug observe: dans la forme Purchase/contrat, meme menu gauche cache, le formulaire ne s'etirait plus sur toute la largeur; il restait visuellement autour de 75% de l'ecran @@ -972,6 +987,16 @@ Point de deploiement: - fichiers modifies: toujours synchroniser `tryton-sao.css` et `dist/tryton-sao.css`, car `index.html` charge le CSS depuis `dist/` - lecon: apres un correctif AG Grid/offcanvas, tester les deux etats du hamburger; menu visible, la zone centrale doit rester en colonne Bootstrap, menu cache, elle doit redevenir pleine largeur +### Invariant critique scroll AG Grid vs formulaire classique + +- regression observee apres les essais de scroll vertical AG Grid: une forme Purchase classique, sans AG Grid principale, etait coupee verticalement et mal redimensionnee, avec ou sans menu gauche +- cause reelle: `_resize_grid_host()` pose des dimensions inline sur le `content_box` quand il contient une AG Grid (`height`, `minHeight`, `flexBasis`, `flexShrink`, `width`, `maxWidth`); si le meme `content_box` est ensuite reutilise pour une forme classique, ces styles inline peuvent survivre apres retrait de la classe `ag-grid-content-box` +- autre cause dangereuse: ne jamais mettre `overflow-y: hidden` sur `.screen-container` ou sur un conteneur commun pour supprimer le scroll SAO; cela casse les formulaires classiques et les sous-listes embarquees +- correctif cle: dans `Sao.ScreenContainer.set`, quand `is_ag_grid` est faux, nettoyer explicitement les dimensions inline du `content_box` avant d'ajouter le widget classique +- invariant: les adaptations de scroll AG Grid doivent etre strictement limitees a `.content-box.ag-grid-content-box` et aux conteneurs internes AG Grid; une fiche formulaire doit garder `.content-box` en `flex: 1 1 auto` avec `overflow-y: auto` +- test obligatoire apres toute retouche layout/scroll: ouvrir une forme Purchase sans AG Grid principale, menu gauche ouvert puis cache; verifier que le formulaire descend jusqu'aux onglets/lignes et que la zone centrale se redimensionne correctement +- test complementaire: ouvrir Lots Management ou une vue report AG Grid avec filtres hauts; verifier que le scroll vertical utile reste celui de l'AG Grid et que le conteneur SAO ne cree pas un deuxieme scroll parasite + ### Commits importants - `fc9ab5d Save SAO AG Grid work` diff --git a/AG_GRID_SESSION_INDEX.md b/AG_GRID_SESSION_INDEX.md index ad017bb..3432507 100644 --- a/AG_GRID_SESSION_INDEX.md +++ b/AG_GRID_SESSION_INDEX.md @@ -322,6 +322,7 @@ Traces a lire si le sujet revient: - resize colonnes: ne jamais utiliser la largeur par defaut metier comme `minWidth`; `_get_default_column_width()` sert au premier affichage, `_get_minimum_column_width()` sert au resize et aux corrections de vieux states - resize colonnes: ne pas lancer le refresh layout global depuis `onColumnResized`; annuler les timers pendant le drag et sauver la largeur avec `skipLayoutRefresh`, sinon `doLayout()` peut annuler la largeur choisie par l'utilisateur - resize colonnes: `_force_horizontal_scroll_width()` ne doit jamais appeler `setColumnWidth()`; calculer le scroll avec `getActualWidth()` pour respecter les largeurs utilisateur +- futur tri en mode grouping: autoriser asc/desc/none et le memoriser dans la config de vue, mais ne pas reactiver le tri natif AG Grid sur le `rowData` groupe; capturer l'intention, trier les records source avant `_flatten_grouped_rows()`, puis reconstruire les lignes `group`/`record`/`total` en preservant expansions, totaux et filtres checklist actifs ### AG Grid - règles d'or @@ -339,3 +340,10 @@ Traces a lire si le sujet revient: - correctif: `.row-offcanvas:not(.active) .main-offcanvas` force `float: none`, `flex: 0 0 100%`, `width: 100%`, `max-width: 100%` - complement: `.screen-container` force `min-width: 0`, `width: 100%`, `max-width: 100%` - verifier apres ce type de retouche: menu gauche ouvert et cache, formulaire classique, vue tree AG Grid, et toujours synchroniser `dist/tryton-sao.css` + +### Invariant critique scroll/layout AG Grid + +- ne jamais supprimer le scroll vertical SAO au niveau `.screen-container`; limiter les overrides de scroll a `.content-box.ag-grid-content-box` +- `_resize_grid_host()` peut poser des styles inline sur le `content_box`; quand `Sao.ScreenContainer.set()` repasse sur un widget non AG Grid, nettoyer `height`, `minHeight`, `flexBasis`, `flexShrink`, `width`, `maxWidth` +- regression typique si cet invariant casse: forme Purchase classique coupee en bas, onglets/lignes mal visibles, grande zone blanche ou largeur incorrecte apres toggle du menu gauche +- test obligatoire: Purchase form sans AG Grid principale avec menu gauche ouvert/cache, puis Lots Management/report AG Grid avec filtres hauts diff --git a/dist/tryton-sao.css b/dist/tryton-sao.css index fee35ea..ed0f2a7 100644 --- a/dist/tryton-sao.css +++ b/dist/tryton-sao.css @@ -9685,6 +9685,48 @@ img.icon { .filter-box { margin: 5px 0; } +.ag-grid-screen-filter-toggle-bar { + display: flex; + justify-content: flex-start; + margin: 0 0 5px; +} +.ag-grid-tree-toolbar > .ag-grid-screen-filter-toggle-bar { + margin: 0 10px 0 auto; +} +.ag-grid-screen-filter-toggle { + display: inline-flex; + align-items: center; + gap: 7px; + min-height: 30px; + padding: 4px 10px; + border: 1px solid #d6e4df; + border-radius: 8px; + background: linear-gradient(180deg, #f6fbf9 0%, #edf5f1 100%); + color: #355154; + box-shadow: 0 2px 7px rgba(53, 81, 84, 0.1); + font-size: 12px; + font-weight: 600; + line-height: 1.2; + text-transform: none !important; + transition: background-color 0.18s ease, border-color 0.18s ease, + box-shadow 0.18s ease, transform 0.18s ease; +} +.ag-grid-screen-filter-toggle:hover, +.ag-grid-screen-filter-toggle:focus { + background: linear-gradient(180deg, #f2faf7 0%, #e7f2ed 100%); + border-color: #bfd5cd; + color: #28484b; + box-shadow: 0 3px 9px rgba(53, 81, 84, 0.13); +} +.ag-grid-screen-filter-toggle:active { + transform: translateY(1px); + box-shadow: 0 1px 5px rgba(53, 81, 84, 0.1); +} +.ag-grid-screen-filter-toggle .icon { + width: 14px; + height: 14px; + opacity: 0.78; +} @media screen and (max-width: 991px) { .filter-box button[disabled] { display: none; @@ -9696,6 +9738,13 @@ img.icon { flex: 1; overflow-y: auto; } +.content-box:not(.ag-grid-content-box) { + flex: 1 1 auto; + height: auto; + min-height: 0; + overflow-y: auto; + overflow-x: visible; +} .content-box.ag-grid-content-box { flex: 0 0 auto; min-height: 0; @@ -10788,10 +10837,10 @@ body { left: 0; } .row-offcanvas:not(.active) .main-offcanvas { - float: none; - flex: 0 0 100%; - width: 100%; - max-width: 100%; + float: none !important; + flex: 0 0 100% !important; + width: 100% !important; + max-width: 100% !important; } .sidebar-offcanvas { position: absolute !important; diff --git a/dist/tryton-sao.js b/dist/tryton-sao.js index 4c0a020..29e750c 100644 --- a/dist/tryton-sao.js +++ b/dist/tryton-sao.js @@ -1278,6 +1278,11 @@ var Sao = { Mousetrap.bind(['ctrl+m', 'command+m'], function(e) { e.preventDefault(); jQuery('.row-offcanvas').toggleClass('active'); + [0, 80, 180, 320].forEach(function(delay) { + window.setTimeout(function() { + window.dispatchEvent(new Event('resize')); + }, delay); + }); return false; }); } @@ -12422,6 +12427,23 @@ var Sao = { 'class': 'row' }).appendTo(this.filter_box); this.el.append(this.filter_box); + this.ag_grid_filter_toggle_bar = jQuery('
', { + 'class': 'ag-grid-screen-filter-toggle-bar', + }).hide(); + this.ag_grid_filter_toggle = jQuery('', { + 'type': 'button', + 'class': 'btn btn-default btn-sm ag-grid-screen-filter-toggle', + 'aria-expanded': 'true', + 'title': Sao.i18n.gettext('Hide filters'), + }).append(Sao.common.ICONFACTORY.get_icon_img('tryton-unfold-less', { + 'aria-hidden': true, + })).append(jQuery('', { + 'class': 'ag-grid-screen-filter-toggle-label', + 'text': ' ' + Sao.i18n.gettext('Hide filters'), + })).appendTo(this.ag_grid_filter_toggle_bar); + this.ag_grid_filter_toggle.click( + this.toggle_ag_grid_filters.bind(this)); + this.el.append(this.ag_grid_filter_toggle_bar); this.filter_button = jQuery('', { type: 'button', 'class': 'btn btn-link', @@ -12802,17 +12824,83 @@ var Sao = { this.tab.hide(); } }, + toggle_ag_grid_filters: function() { + this.set_ag_grid_filters_collapsed( + !this.el.hasClass('ag-grid-screen-filters-collapsed')); + }, + set_ag_grid_filters_collapsed: function(collapsed) { + this.el.toggleClass('ag-grid-screen-filters-collapsed', collapsed); + if (collapsed) { + this.filter_box.hide(); + } else { + this.filter_box.show(); + } + if (this.ag_grid_filter_toggle) { + this.ag_grid_filter_toggle + .attr('aria-expanded', String(!collapsed)) + .attr('title', Sao.i18n.gettext( + collapsed ? 'Show filters' : 'Hide filters')); + this.ag_grid_filter_toggle + .find('img.icon') + .replaceWith(Sao.common.ICONFACTORY.get_icon_img( + collapsed ? 'tryton-unfold-more' : + 'tryton-unfold-less', { + 'aria-hidden': true, + })); + this.ag_grid_filter_toggle + .find('.ag-grid-screen-filter-toggle-label') + .text(' ' + Sao.i18n.gettext( + collapsed ? 'Show filters' : 'Hide filters')); + } + window.setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, 0); + }, + set_ag_grid_filter_toggle_visible: function(visible) { + if (!this.ag_grid_filter_toggle_bar) { + return; + } + this.el.toggleClass('ag-grid-screen-with-filter-toggle', visible); + if (visible) { + this.ag_grid_filter_toggle_bar.show(); + this.set_ag_grid_filters_collapsed( + this.el.hasClass('ag-grid-screen-filters-collapsed')); + } else { + this.ag_grid_filter_toggle_bar.hide(); + this.el.removeClass('ag-grid-screen-filters-collapsed'); + } + }, set: function(widget) { if (this.alternate_view) { this.alternate_viewport.children().detach(); this.alternate_viewport.append(widget); } else { + var is_ag_grid = Boolean(widget && widget.hasClass && + widget.hasClass('ag-grid-tree-container')); this.content_box.children().detach(); this.content_box.toggleClass( 'ag-grid-content-box', - Boolean(widget && widget.hasClass && - widget.hasClass('ag-grid-tree-container'))); + is_ag_grid); + if (!is_ag_grid) { + this.content_box.css({ + height: '', + minHeight: '', + flexBasis: '', + flexShrink: '', + width: '', + maxWidth: '', + }); + } this.content_box.append(widget); + if (is_ag_grid) { + var toolbar = widget.find('.ag-grid-tree-toolbar').first(); + if (toolbar.length) { + toolbar.append(this.ag_grid_filter_toggle_bar); + } + } else { + this.el.append(this.ag_grid_filter_toggle_bar); + } + this.set_ag_grid_filter_toggle_visible(is_ag_grid); } }, get_text: function() { @@ -26393,11 +26481,35 @@ function eval_pyson(value){ return; } var rect = this.grid_host[0].getBoundingClientRect(); - var available = window.innerHeight - rect.top - 36; - var height = Math.max(420, Math.min(720, available)); - height = Math.max(520, height); var content_box = this.grid_host.closest('.content-box.ag-grid-content-box'); var screen_container = this.grid_host.closest('.screen-container'); + var toolbar_height = this.toolbar && this.toolbar.length ? + Math.ceil(this.toolbar.outerHeight(true)) : 0; + var external_scroll_height = 20; + var bottom_margin = 96; + var available = window.innerHeight - rect.top - + external_scroll_height - bottom_margin; + if (content_box.length) { + var content_rect = content_box[0].getBoundingClientRect(); + available = Math.min( + available, + window.innerHeight - content_rect.top - toolbar_height - + external_scroll_height - bottom_margin); + } + var compact_min_height = 260; + var readable_min_height = 420; + var maximum_height = 720; + var height; + if (available >= compact_min_height) { + height = Math.max( + compact_min_height, + Math.min(maximum_height, available)); + } else { + height = Math.max( + readable_min_height, + Math.min(maximum_height, + window.innerHeight - rect.top - bottom_margin)); + } var width_right = window.innerWidth - 12; if (screen_container.length) { width_right = Math.min( @@ -26413,9 +26525,6 @@ function eval_pyson(value){ width: available_width + 'px', maxWidth: available_width + 'px', }); - var toolbar_height = this.toolbar && this.toolbar.length ? - Math.ceil(this.toolbar.outerHeight(true)) : 0; - var external_scroll_height = 20; var container_height = height + toolbar_height + external_scroll_height; this.el.css({ height: container_height + 'px', diff --git a/index.html b/index.html index ea6070a..4c8d93a 100644 --- a/index.html +++ b/index.html @@ -163,8 +163,16 @@ this repository contains the full copyright notices and license terms. --> applyThemeMode( document.documentElement.getAttribute('data-theme-mode')); jQuery('.body').show(); + function notifyLayoutResize() { + [0, 80, 180, 320].forEach(function(delay) { + window.setTimeout(function() { + window.dispatchEvent(new Event('resize')); + }, delay); + }); + } jQuery('[data-toggle="offcanvas"]').click(function() { jQuery('.row-offcanvas').toggleClass('active'); + notifyLayoutResize(); }); jQuery('#tabs').on('ready', function() { var mq = window.matchMedia('(max-width: 991px)'); diff --git a/tryton-sao.css b/tryton-sao.css index fee35ea..ed0f2a7 100644 --- a/tryton-sao.css +++ b/tryton-sao.css @@ -9685,6 +9685,48 @@ img.icon { .filter-box { margin: 5px 0; } +.ag-grid-screen-filter-toggle-bar { + display: flex; + justify-content: flex-start; + margin: 0 0 5px; +} +.ag-grid-tree-toolbar > .ag-grid-screen-filter-toggle-bar { + margin: 0 10px 0 auto; +} +.ag-grid-screen-filter-toggle { + display: inline-flex; + align-items: center; + gap: 7px; + min-height: 30px; + padding: 4px 10px; + border: 1px solid #d6e4df; + border-radius: 8px; + background: linear-gradient(180deg, #f6fbf9 0%, #edf5f1 100%); + color: #355154; + box-shadow: 0 2px 7px rgba(53, 81, 84, 0.1); + font-size: 12px; + font-weight: 600; + line-height: 1.2; + text-transform: none !important; + transition: background-color 0.18s ease, border-color 0.18s ease, + box-shadow 0.18s ease, transform 0.18s ease; +} +.ag-grid-screen-filter-toggle:hover, +.ag-grid-screen-filter-toggle:focus { + background: linear-gradient(180deg, #f2faf7 0%, #e7f2ed 100%); + border-color: #bfd5cd; + color: #28484b; + box-shadow: 0 3px 9px rgba(53, 81, 84, 0.13); +} +.ag-grid-screen-filter-toggle:active { + transform: translateY(1px); + box-shadow: 0 1px 5px rgba(53, 81, 84, 0.1); +} +.ag-grid-screen-filter-toggle .icon { + width: 14px; + height: 14px; + opacity: 0.78; +} @media screen and (max-width: 991px) { .filter-box button[disabled] { display: none; @@ -9696,6 +9738,13 @@ img.icon { flex: 1; overflow-y: auto; } +.content-box:not(.ag-grid-content-box) { + flex: 1 1 auto; + height: auto; + min-height: 0; + overflow-y: auto; + overflow-x: visible; +} .content-box.ag-grid-content-box { flex: 0 0 auto; min-height: 0; @@ -10788,10 +10837,10 @@ body { left: 0; } .row-offcanvas:not(.active) .main-offcanvas { - float: none; - flex: 0 0 100%; - width: 100%; - max-width: 100%; + float: none !important; + flex: 0 0 100% !important; + width: 100% !important; + max-width: 100% !important; } .sidebar-offcanvas { position: absolute !important; diff --git a/tryton-sao.js b/tryton-sao.js index 4c0a020..29e750c 100644 --- a/tryton-sao.js +++ b/tryton-sao.js @@ -1278,6 +1278,11 @@ var Sao = { Mousetrap.bind(['ctrl+m', 'command+m'], function(e) { e.preventDefault(); jQuery('.row-offcanvas').toggleClass('active'); + [0, 80, 180, 320].forEach(function(delay) { + window.setTimeout(function() { + window.dispatchEvent(new Event('resize')); + }, delay); + }); return false; }); } @@ -12422,6 +12427,23 @@ var Sao = { 'class': 'row' }).appendTo(this.filter_box); this.el.append(this.filter_box); + this.ag_grid_filter_toggle_bar = jQuery('', { + 'class': 'ag-grid-screen-filter-toggle-bar', + }).hide(); + this.ag_grid_filter_toggle = jQuery('', { + 'type': 'button', + 'class': 'btn btn-default btn-sm ag-grid-screen-filter-toggle', + 'aria-expanded': 'true', + 'title': Sao.i18n.gettext('Hide filters'), + }).append(Sao.common.ICONFACTORY.get_icon_img('tryton-unfold-less', { + 'aria-hidden': true, + })).append(jQuery('', { + 'class': 'ag-grid-screen-filter-toggle-label', + 'text': ' ' + Sao.i18n.gettext('Hide filters'), + })).appendTo(this.ag_grid_filter_toggle_bar); + this.ag_grid_filter_toggle.click( + this.toggle_ag_grid_filters.bind(this)); + this.el.append(this.ag_grid_filter_toggle_bar); this.filter_button = jQuery('', { type: 'button', 'class': 'btn btn-link', @@ -12802,17 +12824,83 @@ var Sao = { this.tab.hide(); } }, + toggle_ag_grid_filters: function() { + this.set_ag_grid_filters_collapsed( + !this.el.hasClass('ag-grid-screen-filters-collapsed')); + }, + set_ag_grid_filters_collapsed: function(collapsed) { + this.el.toggleClass('ag-grid-screen-filters-collapsed', collapsed); + if (collapsed) { + this.filter_box.hide(); + } else { + this.filter_box.show(); + } + if (this.ag_grid_filter_toggle) { + this.ag_grid_filter_toggle + .attr('aria-expanded', String(!collapsed)) + .attr('title', Sao.i18n.gettext( + collapsed ? 'Show filters' : 'Hide filters')); + this.ag_grid_filter_toggle + .find('img.icon') + .replaceWith(Sao.common.ICONFACTORY.get_icon_img( + collapsed ? 'tryton-unfold-more' : + 'tryton-unfold-less', { + 'aria-hidden': true, + })); + this.ag_grid_filter_toggle + .find('.ag-grid-screen-filter-toggle-label') + .text(' ' + Sao.i18n.gettext( + collapsed ? 'Show filters' : 'Hide filters')); + } + window.setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, 0); + }, + set_ag_grid_filter_toggle_visible: function(visible) { + if (!this.ag_grid_filter_toggle_bar) { + return; + } + this.el.toggleClass('ag-grid-screen-with-filter-toggle', visible); + if (visible) { + this.ag_grid_filter_toggle_bar.show(); + this.set_ag_grid_filters_collapsed( + this.el.hasClass('ag-grid-screen-filters-collapsed')); + } else { + this.ag_grid_filter_toggle_bar.hide(); + this.el.removeClass('ag-grid-screen-filters-collapsed'); + } + }, set: function(widget) { if (this.alternate_view) { this.alternate_viewport.children().detach(); this.alternate_viewport.append(widget); } else { + var is_ag_grid = Boolean(widget && widget.hasClass && + widget.hasClass('ag-grid-tree-container')); this.content_box.children().detach(); this.content_box.toggleClass( 'ag-grid-content-box', - Boolean(widget && widget.hasClass && - widget.hasClass('ag-grid-tree-container'))); + is_ag_grid); + if (!is_ag_grid) { + this.content_box.css({ + height: '', + minHeight: '', + flexBasis: '', + flexShrink: '', + width: '', + maxWidth: '', + }); + } this.content_box.append(widget); + if (is_ag_grid) { + var toolbar = widget.find('.ag-grid-tree-toolbar').first(); + if (toolbar.length) { + toolbar.append(this.ag_grid_filter_toggle_bar); + } + } else { + this.el.append(this.ag_grid_filter_toggle_bar); + } + this.set_ag_grid_filter_toggle_visible(is_ag_grid); } }, get_text: function() { @@ -26393,11 +26481,35 @@ function eval_pyson(value){ return; } var rect = this.grid_host[0].getBoundingClientRect(); - var available = window.innerHeight - rect.top - 36; - var height = Math.max(420, Math.min(720, available)); - height = Math.max(520, height); var content_box = this.grid_host.closest('.content-box.ag-grid-content-box'); var screen_container = this.grid_host.closest('.screen-container'); + var toolbar_height = this.toolbar && this.toolbar.length ? + Math.ceil(this.toolbar.outerHeight(true)) : 0; + var external_scroll_height = 20; + var bottom_margin = 96; + var available = window.innerHeight - rect.top - + external_scroll_height - bottom_margin; + if (content_box.length) { + var content_rect = content_box[0].getBoundingClientRect(); + available = Math.min( + available, + window.innerHeight - content_rect.top - toolbar_height - + external_scroll_height - bottom_margin); + } + var compact_min_height = 260; + var readable_min_height = 420; + var maximum_height = 720; + var height; + if (available >= compact_min_height) { + height = Math.max( + compact_min_height, + Math.min(maximum_height, available)); + } else { + height = Math.max( + readable_min_height, + Math.min(maximum_height, + window.innerHeight - rect.top - bottom_margin)); + } var width_right = window.innerWidth - 12; if (screen_container.length) { width_right = Math.min( @@ -26413,9 +26525,6 @@ function eval_pyson(value){ width: available_width + 'px', maxWidth: available_width + 'px', }); - var toolbar_height = this.toolbar && this.toolbar.length ? - Math.ceil(this.toolbar.outerHeight(true)) : 0; - var external_scroll_height = 20; var container_height = height + toolbar_height + external_scroll_height; this.el.css({ height: container_height + 'px',