From 20d0a1fe1fbc7087dc1c5f54a66c1d8fb822b668 Mon Sep 17 00:00:00 2001 From: Geo Perez Date: Tue, 24 May 2016 15:47:38 -0500 Subject: [PATCH] Fix issue with Regex Routes with upper letters and update Tubular dependency --- .../Unosquare.Labs.EmbedIO.Samples.csproj | 2 +- .../html/scripts/tubular/tubular-bundle.css | 712 ++- .../html/scripts/tubular/tubular-bundle.js | 4975 +++++++---------- .../scripts/tubular/tubular-bundle.min.css | 11 +- .../scripts/tubular/tubular-bundle.min.js | 2 +- .../packages.config | 4 +- .../RegexRoutingTest.cs | 2 +- .../TestObjects/TestController.cs | 2 +- .../TestObjects/TestRegexController.cs | 2 +- Unosquare.Labs.EmbedIO/Extensions.cs | 2 +- .../Modules/WebApiModule.cs | 45 +- 11 files changed, 2385 insertions(+), 3374 deletions(-) diff --git a/Unosquare.Labs.EmbedIO.Samples/Unosquare.Labs.EmbedIO.Samples.csproj b/Unosquare.Labs.EmbedIO.Samples/Unosquare.Labs.EmbedIO.Samples.csproj index c6a60654f..eee09e6c3 100644 --- a/Unosquare.Labs.EmbedIO.Samples/Unosquare.Labs.EmbedIO.Samples.csproj +++ b/Unosquare.Labs.EmbedIO.Samples/Unosquare.Labs.EmbedIO.Samples.csproj @@ -51,7 +51,7 @@ True - ..\packages\Tubular.ServerSide.0.9.47\lib\net45\Unosquare.Tubular.dll + ..\packages\Tubular.ServerSide.0.9.48\lib\net45\Unosquare.Tubular.dll True diff --git a/Unosquare.Labs.EmbedIO.Samples/html/scripts/tubular/tubular-bundle.css b/Unosquare.Labs.EmbedIO.Samples/html/scripts/tubular/tubular-bundle.css index fdc6054a2..280028376 100644 --- a/Unosquare.Labs.EmbedIO.Samples/html/scripts/tubular/tubular-bundle.css +++ b/Unosquare.Labs.EmbedIO.Samples/html/scripts/tubular/tubular-bundle.css @@ -1,426 +1,320 @@ -/*! - * Datepicker for Bootstrap - * - * Copyright 2012 Stefan Petre - * Improvements by Andrew Rowls and Christophe Desguez - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - */ -.datepicker { - top: 0; - left: 0; - padding: 4px; - margin-top: 2px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - - .datepicker:before { - content: ''; - display: inline-block; - border-left: 7px solid transparent; - border-right: 7px solid transparent; - border-bottom: 7px solid #ccc; - border-bottom-color: rgba(0, 0, 0, 0.2); - position: absolute; - top: -7px; - left: 6px; - } - - .datepicker:after { - content: ''; - display: inline-block; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-bottom: 6px solid #ffffff; - position: absolute; - top: -6px; - left: 7px; - } - - .datepicker table { - margin: 0; - } - - .datepicker td, - .datepicker th { - text-align: center; - width: 20px; - height: 20px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - } - - .datepicker td.day:hover { - background: #eeeeee; - cursor: pointer; - } - - .datepicker td.old, - .datepicker td.new { - color: #999999; - } - - .datepicker td.disabled, - .datepicker td.disabled:hover { - background: none; - color: #999999; - cursor: default; - } - - .datepicker td.active, - .datepicker td.active:hover, - .datepicker td.active.disabled, - .datepicker td.active.disabled:hover { - background-color: #006dcc; - background-image: -moz-linear-gradient(top, #0088cc, #0044cc); - background-image: -ms-linear-gradient(top, #0088cc, #0044cc); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); - background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); - background-image: -o-linear-gradient(top, #0088cc, #0044cc); - background-image: linear-gradient(top, #0088cc, #0044cc); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); - border-color: #0044cc #0044cc #002a80; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:dximagetransform.microsoft.gradient(enabled=false); - color: #fff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - } - - .datepicker thead tr:first-child th { - cursor: pointer; - } - - .datepicker thead tr:first-child th:hover { - background: #eeeeee; - } - - .datepicker.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 9999; - float: left; - display: none; - min-width: 160px; - list-style: none; - background-color: #ffffff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; - *border-right-width: 2px; - *border-bottom-width: 2px; - color: #333333; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - line-height: 18px; - } - - .datepicker.dropdown-menu.active { - display: block; - } - - .datepicker.dropdown-menu th, - .datepicker.dropdown-menu td { - padding: 2px 3px; - } - - .datepicker .prev, - .datepicker .next { - font-style: normal; - } - -.tubular-checkbox { - padding-left: 20px; +.checkbox { + padding-left: 20px; } - - .tubular-checkbox label { - display: inline-block; - vertical-align: middle; - position: relative; - padding-left: 5px; - } - - .tubular-checkbox label::before { - content: ""; - display: inline-block; - position: absolute; - width: 17px; - height: 17px; - left: 0; - margin-left: -20px; - border: 1px solid #cccccc; - border-radius: 3px; - background-color: #fff; - -webkit-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; - -o-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; - -moz-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; - transition: border 0.15s ease-in-out, color 0.15s ease-in-out; - } - - .tubular-checkbox label::after { - display: inline-block; - position: absolute; - width: 16px; - height: 16px; - left: 0; - top: 0; - margin-left: -20px; - padding-left: 3px; - padding-top: 1px; - font-size: 11px; - color: #555555; - } - -.text-center .tubular-checkbox label::after { - margin-left: -21px; +.checkbox label { + display: inline-block; + vertical-align: middle; + position: relative; + padding-left: 5px; } - - .tubular-checkbox input[type="checkbox"] { - opacity: 0; - z-index: 1; - cursor: pointer; - } - - .tubular-checkbox input[type="checkbox"]:focus + label::before { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; - } - - .tubular-checkbox input[type="checkbox"]:checked + label::after { - font-family: 'FontAwesome'; - content: "\f00c"; - } - - .tubular-checkbox input[type="checkbox"]:disabled + label { - opacity: 0.65; - } - - .tubular-checkbox input[type="checkbox"]:disabled + label::before { - background-color: #eeeeee; - cursor: not-allowed; - } - - .tubular-checkbox.tubular-checkbox-circle label::before { - border-radius: 50%; - } - - .tubular-checkbox.tubular-checkbox-inline { - margin-top: 0; - } - -.tubular-checkbox-primary input[type="checkbox"]:checked + label::before { - background-color: #337ab7; - border-color: #337ab7; +.checkbox label::before { + content: ""; + display: inline-block; + position: absolute; + width: 17px; + height: 17px; + left: 0; + margin-left: -20px; + border: 1px solid #cccccc; + border-radius: 3px; + background-color: #fff; + -webkit-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; + -o-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; + transition: border 0.15s ease-in-out, color 0.15s ease-in-out; +} +.checkbox label::after { + display: inline-block; + position: absolute; + width: 16px; + height: 16px; + left: 0; + top: 0; + margin-left: -20px; + padding-left: 3px; + padding-top: 1px; + font-size: 11px; + color: #555555; +} +.checkbox input[type="checkbox"], +.checkbox input[type="radio"] { + opacity: 0; + z-index: 1; +} +.checkbox input[type="checkbox"]:focus + label::before, +.checkbox input[type="radio"]:focus + label::before { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.checkbox input[type="checkbox"]:checked + label::after, +.checkbox input[type="radio"]:checked + label::after { + font-family: "FontAwesome"; + content: "\f00c"; +} +.checkbox input[type="checkbox"]:indeterminate + label::after, +.checkbox input[type="radio"]:indeterminate + label::after { + display: block; + content: ""; + width: 10px; + height: 3px; + background-color: #555555; + border-radius: 2px; + margin-left: -16.5px; + margin-top: 7px; } - -.tubular-checkbox-primary input[type="checkbox"]:checked + label::after { - color: #fff; +.checkbox input[type="checkbox"]:disabled + label, +.checkbox input[type="radio"]:disabled + label { + opacity: 0.65; } - -.tubular-checkbox-danger input[type="checkbox"]:checked + label::before { - background-color: #d9534f; - border-color: #d9534f; +.checkbox input[type="checkbox"]:disabled + label::before, +.checkbox input[type="radio"]:disabled + label::before { + background-color: #eeeeee; + cursor: not-allowed; } - -.tubular-checkbox-danger input[type="checkbox"]:checked + label::after { - color: #fff; +.checkbox.checkbox-circle label::before { + border-radius: 50%; } - -.tubular-checkbox-info input[type="checkbox"]:checked + label::before { - background-color: #5bc0de; - border-color: #5bc0de; +.checkbox.checkbox-inline { + margin-top: 0; } -.tubular-checkbox-info input[type="checkbox"]:checked + label::after { - color: #fff; +.checkbox-primary input[type="checkbox"]:checked + label::before, +.checkbox-primary input[type="radio"]:checked + label::before { + background-color: #337ab7; + border-color: #337ab7; } - -.tubular-checkbox-warning input[type="checkbox"]:checked + label::before { - background-color: #f0ad4e; - border-color: #f0ad4e; +.checkbox-primary input[type="checkbox"]:checked + label::after, +.checkbox-primary input[type="radio"]:checked + label::after { + color: #fff; } -.tubular-checkbox-warning input[type="checkbox"]:checked + label::after { - color: #fff; +.checkbox-danger input[type="checkbox"]:checked + label::before, +.checkbox-danger input[type="radio"]:checked + label::before { + background-color: #d9534f; + border-color: #d9534f; } - -.tubular-checkbox-success input[type="checkbox"]:checked + label::before { - background-color: #5cb85c; - border-color: #5cb85c; +.checkbox-danger input[type="checkbox"]:checked + label::after, +.checkbox-danger input[type="radio"]:checked + label::after { + color: #fff; } -.tubular-checkbox-success input[type="checkbox"]:checked + label::after { - color: #fff; +.checkbox-info input[type="checkbox"]:checked + label::before, +.checkbox-info input[type="radio"]:checked + label::before { + background-color: #5bc0de; + border-color: #5bc0de; } - -.tubular-radio { - padding-left: 20px; +.checkbox-info input[type="checkbox"]:checked + label::after, +.checkbox-info input[type="radio"]:checked + label::after { + color: #fff; } - .tubular-radio label { - display: inline-block; - vertical-align: middle; - position: relative; - padding-left: 5px; - } - - .tubular-radio label::before { - content: ""; - display: inline-block; - position: absolute; - width: 17px; - height: 17px; - left: 0; - margin-left: -20px; - border: 1px solid #cccccc; - border-radius: 50%; - background-color: #fff; - -webkit-transition: border 0.15s ease-in-out; - -o-transition: border 0.15s ease-in-out; - -moz-transition: border 0.15s ease-in-out; - transition: border 0.15s ease-in-out; - } - - .tubular-radio label::after { - display: inline-block; - position: absolute; - content: " "; - width: 11px; - height: 11px; - left: 3px; - top: 3px; - margin-left: -20px; - border-radius: 50%; - background-color: #555555; - -webkit-transform: scale(0, 0); - -ms-transform: scale(0, 0); - -o-transform: scale(0, 0); - -moz-transform: scale(0, 0); - transform: scale(0, 0); - -webkit-transition: -webkit-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); - -moz-transition: -moz-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); - -o-transition: -o-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); - transition: transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); - } - - .tubular-radio input[type="radio"] { - opacity: 0; - z-index: 1; - } - - .tubular-radio input[type="radio"]:focus + label::before { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; - } - - .tubular-radio input[type="radio"]:checked + label::after { - -webkit-transform: scale(1, 1); - -ms-transform: scale(1, 1); - -o-transform: scale(1, 1); - -moz-transform: scale(1, 1); - transform: scale(1, 1); - } - - .tubular-radio input[type="radio"]:disabled + label { - opacity: 0.65; - } - - .tubular-radio input[type="radio"]:disabled + label::before { - cursor: not-allowed; - } - - .tubular-radio.tubular-radio-inline { - margin-top: 0; - } - -.tubular-radio-primary input[type="radio"] + label::after { - background-color: #337ab7; +.checkbox-warning input[type="checkbox"]:checked + label::before, +.checkbox-warning input[type="radio"]:checked + label::before { + background-color: #f0ad4e; + border-color: #f0ad4e; +} +.checkbox-warning input[type="checkbox"]:checked + label::after, +.checkbox-warning input[type="radio"]:checked + label::after { + color: #fff; } -.tubular-radio-primary input[type="radio"]:checked + label::before { - border-color: #337ab7; +.checkbox-success input[type="checkbox"]:checked + label::before, +.checkbox-success input[type="radio"]:checked + label::before { + background-color: #5cb85c; + border-color: #5cb85c; +} +.checkbox-success input[type="checkbox"]:checked + label::after, +.checkbox-success input[type="radio"]:checked + label::after { + color: #fff; } -.tubular-radio-primary input[type="radio"]:checked + label::after { - background-color: #337ab7; +.checkbox-primary input[type="checkbox"]:indeterminate + label::before, +.checkbox-primary input[type="radio"]:indeterminate + label::before { + background-color: #337ab7; + border-color: #337ab7; } -.tubular-radio-danger input[type="radio"] + label::after { - background-color: #d9534f; +.checkbox-primary input[type="checkbox"]:indeterminate + label::after, +.checkbox-primary input[type="radio"]:indeterminate + label::after { + background-color: #fff; } -.tubular-radio-danger input[type="radio"]:checked + label::before { - border-color: #d9534f; +.checkbox-danger input[type="checkbox"]:indeterminate + label::before, +.checkbox-danger input[type="radio"]:indeterminate + label::before { + background-color: #d9534f; + border-color: #d9534f; } -.tubular-radio-danger input[type="radio"]:checked + label::after { - background-color: #d9534f; +.checkbox-danger input[type="checkbox"]:indeterminate + label::after, +.checkbox-danger input[type="radio"]:indeterminate + label::after { + background-color: #fff; } -.tubular-radio-info input[type="radio"] + label::after { - background-color: #5bc0de; +.checkbox-info input[type="checkbox"]:indeterminate + label::before, +.checkbox-info input[type="radio"]:indeterminate + label::before { + background-color: #5bc0de; + border-color: #5bc0de; } -.tubular-radio-info input[type="radio"]:checked + label::before { - border-color: #5bc0de; +.checkbox-info input[type="checkbox"]:indeterminate + label::after, +.checkbox-info input[type="radio"]:indeterminate + label::after { + background-color: #fff; } -.tubular-radio-info input[type="radio"]:checked + label::after { - background-color: #5bc0de; +.checkbox-warning input[type="checkbox"]:indeterminate + label::before, +.checkbox-warning input[type="radio"]:indeterminate + label::before { + background-color: #f0ad4e; + border-color: #f0ad4e; } -.tubular-radio-warning input[type="radio"] + label::after { - background-color: #f0ad4e; +.checkbox-warning input[type="checkbox"]:indeterminate + label::after, +.checkbox-warning input[type="radio"]:indeterminate + label::after { + background-color: #fff; } -.tubular-radio-warning input[type="radio"]:checked + label::before { - border-color: #f0ad4e; +.checkbox-success input[type="checkbox"]:indeterminate + label::before, +.checkbox-success input[type="radio"]:indeterminate + label::before { + background-color: #5cb85c; + border-color: #5cb85c; } -.tubular-radio-warning input[type="radio"]:checked + label::after { - background-color: #f0ad4e; +.checkbox-success input[type="checkbox"]:indeterminate + label::after, +.checkbox-success input[type="radio"]:indeterminate + label::after { + background-color: #fff; } -.tubular-radio-success input[type="radio"] + label::after { - background-color: #5cb85c; +.radio { + padding-left: 20px; +} +.radio label { + display: inline-block; + vertical-align: middle; + position: relative; + padding-left: 5px; +} +.radio label::before { + content: ""; + display: inline-block; + position: absolute; + width: 17px; + height: 17px; + left: 0; + margin-left: -20px; + border: 1px solid #cccccc; + border-radius: 50%; + background-color: #fff; + -webkit-transition: border 0.15s ease-in-out; + -o-transition: border 0.15s ease-in-out; + transition: border 0.15s ease-in-out; +} +.radio label::after { + display: inline-block; + position: absolute; + content: " "; + width: 11px; + height: 11px; + left: 3px; + top: 3px; + margin-left: -20px; + border-radius: 50%; + background-color: #555555; + -webkit-transform: scale(0, 0); + -ms-transform: scale(0, 0); + -o-transform: scale(0, 0); + transform: scale(0, 0); + -webkit-transition: -webkit-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); + -moz-transition: -moz-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); + -o-transition: -o-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); + transition: transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); +} +.radio input[type="radio"] { + opacity: 0; + z-index: 1; +} +.radio input[type="radio"]:focus + label::before { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.radio input[type="radio"]:checked + label::after { + -webkit-transform: scale(1, 1); + -ms-transform: scale(1, 1); + -o-transform: scale(1, 1); + transform: scale(1, 1); +} +.radio input[type="radio"]:disabled + label { + opacity: 0.65; +} +.radio input[type="radio"]:disabled + label::before { + cursor: not-allowed; +} +.radio.radio-inline { + margin-top: 0; } -.tubular-radio-success input[type="radio"]:checked + label::before { - border-color: #5cb85c; +.radio-primary input[type="radio"] + label::after { + background-color: #337ab7; +} +.radio-primary input[type="radio"]:checked + label::before { + border-color: #337ab7; +} +.radio-primary input[type="radio"]:checked + label::after { + background-color: #337ab7; } -.tubular-radio-success input[type="radio"]:checked + label::after { - background-color: #5cb85c; +.radio-danger input[type="radio"] + label::after { + background-color: #d9534f; +} +.radio-danger input[type="radio"]:checked + label::before { + border-color: #d9534f; +} +.radio-danger input[type="radio"]:checked + label::after { + background-color: #d9534f; } -input[type="checkbox"].styled:checked + label:after { - font-family: 'FontAwesome'; - content: "\f00c"; +.radio-info input[type="radio"] + label::after { + background-color: #5bc0de; +} +.radio-info input[type="radio"]:checked + label::before { + border-color: #5bc0de; +} +.radio-info input[type="radio"]:checked + label::after { + background-color: #5bc0de; } -input[type="checkbox"] .styled:checked + label::before { - color: #fff; +.radio-warning input[type="radio"] + label::after { + background-color: #f0ad4e; +} +.radio-warning input[type="radio"]:checked + label::before { + border-color: #f0ad4e; +} +.radio-warning input[type="radio"]:checked + label::after { + background-color: #f0ad4e; } -input[type="checkbox"] .styled:checked + label::after { - color: #fff; +.radio-success input[type="radio"] + label::after { + background-color: #5cb85c; +} +.radio-success input[type="radio"]:checked + label::before { + border-color: #5cb85c; +} +.radio-success input[type="radio"]:checked + label::after { + background-color: #5cb85c; } +input[type="checkbox"].styled:checked + label:after, +input[type="radio"].styled:checked + label:after { + font-family: 'FontAwesome'; + content: "\f00c"; +} +input[type="checkbox"] .styled:checked + label::before, +input[type="radio"] .styled:checked + label::before { + color: #fff; +} +input[type="checkbox"] .styled:checked + label::after, +input[type="radio"] .styled:checked + label::after { + color: #fff; +} .chart-legend, .bar-legend, .line-legend, @@ -476,20 +370,21 @@ input[type="checkbox"] .styled:checked + label::after { } /* Tubular Grid General Styles */ -div.tubular-grid { +tb-grid { position: relative; + display: block; } - div.tubular-grid div.row { + tb-grid div.row { margin-top: 4px; margin-bottom: 4px; } - div.tubular-grid div.row:first-child { + tb-grid div.row:first-child { margin-top: 0; } - div.tubular-grid div.row:last-child { + tb-grid div.row:last-child { margin-bottom: 0; } @@ -525,7 +420,7 @@ div.tubular-overlay { div.tubular-overlay > div { position: absolute; - background-color: rgb(180,180,180); + background-color: #b4b4b4; color: #fff; height: 100%; top: 0; @@ -572,7 +467,7 @@ div.tubular-general-overlay { div.tubular-general-overlay > div { position: absolute; - background-color: rgb(180,180,180); + background-color: #b4b4b4; color: #fff; height: 100%; width: 100%; @@ -595,14 +490,6 @@ div.panel > table.tubular-grid-table.table-bordered > thead > tr > th { border-right-width: 0; } -div.panel > table.tubular-grid-table.table-bordered > tbody > tr > td { - border-right: 1px solid #ddd; -} - - div.panel > table.tubular-grid-table.table-bordered > tbody > tr > td:last-child { - border-right-width: 0; - } - table.tubular-grid-table { font-size: 11px; table-layout: fixed; @@ -617,7 +504,7 @@ table.tubular-grid-table { margin: 0; } -th.show-xl, td.show-xl { +.show-xl { display: none; } @@ -646,7 +533,7 @@ table.tubular-grid-table thead tr th.column-md { table.tubular-grid-table thead tr th.column-sm { width: 80px; } - + table.tubular-grid-table thead tr th.column-date { width: 90px; } @@ -657,7 +544,7 @@ table.tubular-grid-table thead tr th.column-md { } @media (min-width: 1400px) { - th.show-xl, td.show-xl { + .show-xl { display: table-cell; } @@ -674,7 +561,6 @@ table.tubular-grid-table thead tr th.column-md { } } -table.tubular-grid-table tr td, table.tubular-grid-table tr th { padding: 2px; } @@ -683,16 +569,6 @@ table.tubular-grid-table thead tr .column-action { width: 75px; } -table.tubular-grid-table tbody tr td { - overflow: hidden; - -ms-text-overflow: ellipsis; - -o-text-overflow: ellipsis; - text-overflow: ellipsis; -} - - table.tubular-grid-table tbody tr td.no-wrap, table.tubular-grid-table tbody tr td.no-wrap span { - white-space: nowrap; - } table.tubular-grid-table tbody tr .cell-menu { overflow: visible; @@ -730,8 +606,7 @@ table.tubular-grid-table thead tr th div.tubular-column-menu { outline: none; } - table.tubular-grid-table thead tr th div.tubular-column-menu button i.fa, - table.tubular-grid-table thead tr th div.tubular-column-menu button .glyphicon { + table.tubular-grid-table thead tr th div.tubular-column-menu button i.fa { font-size: 10px; } @@ -852,4 +727,41 @@ select.checkbox-list:hover option:checked, select.checkbox-list:focus option:che .widget-panel .panel.panel-default { height: 100%; +} + +div.input-group-addon.fa { + font-size: 10px; +} + +/* Components styling */ +tb-edit-button.btn { + padding: 0; +} + +div.panel > table.tubular-grid-table.table-bordered > tbody > tr > td { + border-right: 1px solid #ddd; +} + + div.panel > table.tubular-grid-table.table-bordered > tbody > tr > td:last-child { + border-right-width: 0; + } + + +table.tubular-grid-table tr td { + padding: 2px; +} + +table.tubular-grid-table tbody tr td { + overflow: hidden; + -ms-text-overflow: ellipsis; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; +} + + table.tubular-grid-table tbody tr td.no-wrap, table.tubular-grid-table tbody tr td.no-wrap span { + white-space: nowrap; + } + +li.pagination-first, li.pagination-prev, li.pagination-next, li.pagination-last { + font-family: 'FontAwesome'; } \ No newline at end of file diff --git a/Unosquare.Labs.EmbedIO.Samples/html/scripts/tubular/tubular-bundle.js b/Unosquare.Labs.EmbedIO.Samples/html/scripts/tubular/tubular-bundle.js index 52e3c0513..0c33af936 100644 --- a/Unosquare.Labs.EmbedIO.Samples/html/scripts/tubular/tubular-bundle.js +++ b/Unosquare.Labs.EmbedIO.Samples/html/scripts/tubular/tubular-bundle.js @@ -412,247 +412,32 @@ try { } catch (e) { // Ignore } -/** - * @ngdoc provider - * @name filterWatcher - * @kind function - * - * @description - * store specific filters result in $$cache, based on scope life time(avoid memory leak). - * on scope.$destroy remove it's cache from $$cache container - */ - -angular.module('a8m.filter-watcher', []) - .provider('filterWatcher', function () { - - this.$get = ['$window', '$rootScope', function ($window, $rootScope) { - - /** - * Cache storing - * @type {Object} - */ - var $$cache = {}; - - /** - * Scope listeners container - * scope.$destroy => remove all cache keys - * bind to current scope. - * @type {Object} - */ - var $$listeners = {}; - - /** - * $timeout without triggering the digest cycle - * @type {function} - */ - var $$timeout = $window.setTimeout; - - /** - * @description - * get `HashKey` string based on the given arguments. - * @param fName - * @param args - * @returns {string} - */ - function getHashKey(fName, args) { - return [fName, angular.toJson(args)] - .join('#') - .replace(/"/g, ''); - } - - /** - * @description - * fir on $scope.$destroy, - * remove cache based scope from `$$cache`, - * and remove itself from `$$listeners` - * @param event - */ - function removeCache(event) { - var id = event.targetScope.$id; - forEach($$listeners[id], function (key) { - delete $$cache[key]; - }); - delete $$listeners[id]; - } - - /** - * @description - * for angular version that greater than v.1.3.0 - * it clear cache when the digest cycle is end. - */ - function cleanStateless() { - $$timeout(function () { - if (!$rootScope.$$phase) - $$cache = {}; - }); - } - - /** - * @description - * Store hashKeys in $$listeners container - * on scope.$destroy, remove them all(bind an event). - * @param scope - * @param hashKey - * @returns {*} - */ - function addListener(scope, hashKey) { - var id = scope.$id; - if (isUndefined($$listeners[id])) { - scope.$on('$destroy', removeCache); - $$listeners[id] = []; - } - return $$listeners[id].push(hashKey); - } - - /** - * @description - * return the `cacheKey` or undefined. - * @param filterName - * @param args - * @returns {*} - */ - function $$isMemoized(filterName, args) { - var hashKey = getHashKey(filterName, args); - return $$cache[hashKey]; - } - - /** - * @description - * Test if given object is a Scope instance - * @param obj - * @returns {Boolean} - */ - function isScope(obj) { - return obj && obj.$evalAsync && obj.$watch; - } - - /** - * @description - * store `result` in `$$cache` container, based on the hashKey. - * add $destroy listener and return result - * @param filterName - * @param args - * @param scope - * @param result - * @returns {*} - */ - function $$memoize(filterName, args, scope, result) { - var hashKey = getHashKey(filterName, args); - //store result in `$$cache` container - $$cache[hashKey] = result; - // for angular versions that less than 1.3 - // add to `$destroy` listener, a cleaner callback - if (isScope(scope)) { - addListener(scope, hashKey); - } else { - cleanStateless(); - } - return result; - } - - return { - isMemoized: $$isMemoized, - memoize: $$memoize - } - - }]; - }); -/** - * @ngdoc filter - * @name groupBy - * @kind function - * - * @description - * Create an object composed of keys generated from the result of running each element of a collection, - * each key is an array of the elements. - */ - -angular.module('a8m.group-by', ['a8m.filter-watcher']) - - .filter('groupBy', ['$parse', 'filterWatcher', function ($parse, filterWatcher) { - return function (collection, property) { - - if (!angular.isObject(collection) || angular.isUndefined(property)) { - return collection; - } - - var getterFn = $parse(property); - - return filterWatcher.isMemoized('groupBy', arguments) || - filterWatcher.memoize('groupBy', arguments, this, - _groupBy(collection, getterFn)); - - /** - * groupBy function - * @param collection - * @param getter - * @returns {{}} - */ - function _groupBy(collection, getter) { - var result = {}; - var prop; - - angular.forEach(collection, function (elm) { - prop = getter(elm); - - if (!result[prop]) { - result[prop] = []; - } - result[prop].push(elm); - }); - return result; - } - } - }]); -(function() { +(function(angular) { 'use strict'; /** * @ngdoc module * @name tubular - * @version 0.9.17 * * @description * Tubular module. Entry point to get all the Tubular functionality. * * It depends upon {@link tubular.directives}, {@link tubular.services} and {@link tubular.models}. */ - angular.module('tubular', ['tubular.directives', 'tubular.services', 'tubular.models', 'LocalStorageModule', 'a8m.group-by']) + angular.module('tubular', ['tubular.directives', 'tubular.services', 'tubular.models', 'LocalStorageModule']) .config([ 'localStorageServiceProvider', function(localStorageServiceProvider) { localStorageServiceProvider.setPrefix('tubular'); - - // define console methods if not defined - if (typeof console === "undefined") { - window.console = { - log: function() {}, - debug: function() {}, - error: function() {}, - assert: function() {}, - info: function() {}, - warn: function() {}, - }; - } } ]) - .run(['tubularHttp', 'tubularOData', 'tubularLocalData', - function (tubularHttp, tubularOData, tubularLocalData) { + .run([ + 'tubularHttp', 'tubularOData', 'tubularLocalData', + function(tubularHttp, tubularOData, tubularLocalData) { // register data services tubularHttp.registerService('odata', tubularOData); tubularHttp.registerService('local', tubularLocalData); } ]) - /** - * @ngdoc constants - * @name tubularConst - * - * @description - * The `tubularConst` holds some UI constants. - */ - .constant("tubularConst", { - "upCssClass": "fa-long-arrow-up", - "downCssClass": "fa-long-arrow-down" - }) /** * @ngdoc filter * @name errormessage @@ -668,8 +453,9 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) return function(input) { if (angular.isDefined(input) && angular.isDefined(input.data) && input.data && - angular.isDefined(input.data.ExceptionMessage)) + angular.isDefined(input.data.ExceptionMessage)) { return input.data.ExceptionMessage; + } return input.statusText || "Connection Error"; }; @@ -682,86 +468,71 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @description * `numberorcurrency` is a hack to hold `currency` and `number` in a single filter. */ - .filter('numberorcurrency', [ - '$filter', function($filter) { + .filter("numberorcurrency", [ + "$filter", function($filter) { return function(input, format, symbol, fractionSize) { symbol = symbol || "$"; fractionSize = fractionSize || 2; - if (format === 'C') { - return $filter('currency')(input, symbol, fractionSize); + if (format === "C") { + return $filter("currency")(input, symbol, fractionSize); } - if (format === 'I') { + if (format === "I") { return parseInt(input); } // default to decimal - return $filter('number')(input, fractionSize); + return $filter("number")(input, fractionSize); }; } ]) /** * @ngdoc filter - * @name characters + * @name moment * @kind function * * @description - * `characters` filter truncates a sentence to a number of characters. - * - * Based on https://github.com/sparkalow/angular-truncate/blob/master/src/truncate.js + * `moment` is a filter to call format from moment or, if the input is a Date, call Angular's `date` filter. */ - .filter('characters', function() { - return function(input, chars, breakOnWord) { - if (isNaN(chars)) return input; - if (chars <= 0) return ''; - - if (input && input.length > chars) { - input = input.substring(0, chars); - - if (!breakOnWord) { - var lastspace = input.lastIndexOf(' '); - - //get last space - if (lastspace !== -1) { - input = input.substr(0, lastspace); - } - } else { - while (input.charAt(input.length - 1) === ' ') { - input = input.substr(0, input.length - 1); + .filter("moment", [ + "$filter", function($filter) { + return function(input, format) { + if (angular.isDefined(input) && typeof (input) === "object") { + if (typeof moment == 'function' && input !== null) { + return input.format(format); + } else { + return $filter('date')(input); } } - return input + '…'; - } - return input; - }; - }); -})(); -(function() { + return input; + }; + } + ]); +})(window.angular); +(function (angular) { 'use strict'; /** * @ngdoc module * @name tubular.directives + * @module tubular.directives * * @description - * Tubular Directives module. It contains all the directives. + * Tubular Directives and Components module. * * It depends upon {@link tubular.services} and {@link tubular.models}. */ angular.module('tubular.directives', ['tubular.services', 'tubular.models']) /** - * @ngdoc directive + * @ngdoc component * @name tbGrid - * @restrict E - * + * * @description * The `tbGrid` directive is the base to create any grid. This is the root node where you should start * designing your grid. Don't need to add a `controller`. * - * @scope - * * @param {string} serverUrl Set the HTTP URL where the data comes. * @param {string} serverSaveUrl Set the HTTP URL where the data will be saved. * @param {string} serverDeleteUrl Set the HTTP URL where the data will be saved. @@ -779,470 +550,469 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {bool} savePageSize Set if the grid autosave page size, default true. * @param {bool} saveSearch Set if the grid autosave search, default true. */ - .directive('tbGrid', [ - function() { - return { - template: '
' + - '
' + - '' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: { - serverUrl: '@', - serverSaveUrl: '@', - serverDeleteUrl: '@', - serverSaveMethod: '@', - pageSize: '@?', - onBeforeGetData: '=?', - requestMethod: '@', - dataServiceName: '@?serviceName', - requireAuthentication: '@?', - name: '@?gridName', - editorMode: '@?', - showLoading: '=?', - autoRefresh: '=?', - savePage: '=?', - savePageSize: '=?', - saveSearch: '=?' - }, - controller: [ - '$scope', 'localStorageService', 'tubularPopupService', 'tubularModel', 'tubularHttp', '$routeParams', - function($scope, localStorageService, tubularPopupService, TubularModel, tubularHttp, $routeParams) { - $scope.name = $scope.name || 'tbgrid'; - $scope.tubularDirective = 'tubular-grid'; - $scope.columns = []; - $scope.rows = []; - - $scope.savePage = angular.isUndefined($scope.savePage) ? true : $scope.savePage; - $scope.currentPage = $scope.savePage ? (localStorageService.get($scope.name + "_page") || 1) : 1; - - $scope.savePageSize = angular.isUndefined($scope.savePageSize) ? true : $scope.savePageSize; - $scope.pageSize = angular.isUndefined($scope.pageSize) ? 20 : $scope.pageSize; - $scope.saveSearch = angular.isUndefined($scope.saveSearch) ? true : $scope.saveSearch; - $scope.totalPages = 0; - $scope.totalRecordCount = 0; - $scope.filteredRecordCount = 0; - $scope.requestedPage = $scope.currentPage; - $scope.hasColumnsDefinitions = false; - $scope.requestCounter = 0; - $scope.requestMethod = $scope.requestMethod || 'POST'; - $scope.serverSaveMethod = $scope.serverSaveMethod || 'POST'; - $scope.requestTimeout = 15000; - $scope.currentRequest = null; - $scope.autoSearch = $routeParams.param || ($scope.saveSearch ? (localStorageService.get($scope.name + "_search") || '') : ''); - $scope.search = { - Text: $scope.autoSearch, - Operator: $scope.autoSearch == '' ? 'None' : 'Auto' - }; + .component('tbGrid', { + template: '
' + + '
' + + '
' + + '' + + '
', + transclude: true, + bindings: { + serverUrl: '@', + serverSaveUrl: '@', + serverDeleteUrl: '@', + serverSaveMethod: '@', + pageSize: '=?', + onBeforeGetData: '=?', + requestMethod: '@', + dataServiceName: '@?serviceName', + requireAuthentication: '@?', + name: '@?gridName', + editorMode: '@?', + showLoading: '=?', + autoRefresh: '=?', + savePage: '=?', + savePageSize: '=?', + saveSearch: '=?' + }, + controller: [ + '$scope', 'localStorageService', 'tubularPopupService', 'tubularModel', 'tubularHttp', '$routeParams', + function($scope, localStorageService, tubularPopupService, TubularModel, tubularHttp, $routeParams) { + var $ctrl = this; + + $ctrl.$onInit = function() { + $ctrl.tubularDirective = 'tubular-grid'; + + $ctrl.name = $ctrl.name || 'tbgrid'; + $ctrl.columns = []; + $ctrl.rows = []; + + $ctrl.savePage = angular.isUndefined($ctrl.savePage) ? true : $ctrl.savePage; + $ctrl.currentPage = $ctrl.savePage ? (localStorageService.get($ctrl.name + "_page") || 1) : 1; + + $ctrl.savePageSize = angular.isUndefined($ctrl.savePageSize) ? true : $ctrl.savePageSize; + $ctrl.pageSize = angular.isUndefined($ctrl.pageSize) ? 20 : $ctrl.pageSize; + $ctrl.saveSearch = angular.isUndefined($ctrl.saveSearch) ? true : $ctrl.saveSearch; + $ctrl.totalPages = 0; + $ctrl.totalRecordCount = 0; + $ctrl.filteredRecordCount = 0; + $ctrl.requestedPage = $ctrl.currentPage; + $ctrl.hasColumnsDefinitions = false; + $ctrl.requestCounter = 0; + $ctrl.requestMethod = $ctrl.requestMethod || 'POST'; + $ctrl.serverSaveMethod = $ctrl.serverSaveMethod || 'POST'; + $ctrl.requestTimeout = 15000; + $ctrl.currentRequest = null; + $ctrl.autoSearch = $routeParams.param || ($ctrl.saveSearch ? (localStorageService.get($ctrl.name + "_search") || '') : ''); + $ctrl.search = { + Text: $ctrl.autoSearch, + Operator: $ctrl.autoSearch == '' ? 'None' : 'Auto' + }; - $scope.isEmpty = false; - $scope.tempRow = new TubularModel($scope, {}); - $scope.dataService = tubularHttp.getDataService($scope.dataServiceName); - $scope.requireAuthentication = $scope.requireAuthentication || true; - tubularHttp.setRequireAuthentication($scope.requireAuthentication); - $scope.editorMode = $scope.editorMode || 'none'; - $scope.canSaveState = false; - $scope.groupBy = ''; - $scope.showLoading = angular.isUndefined($scope.showLoading) ? true : $scope.showLoading; - $scope.autoRefresh = angular.isUndefined($scope.autoRefresh) ? true : $scope.autoRefresh; - $scope.serverDeleteUrl = $scope.serverDeleteUrl || $scope.serverSaveUrl; - - $scope.$watch('columns', function() { - if ($scope.hasColumnsDefinitions === false || $scope.canSaveState === false) { - return; - } + $ctrl.isEmpty = false; + $ctrl.tempRow = new TubularModel($scope, $ctrl, {}, $ctrl.dataService); + $ctrl.dataService = tubularHttp.getDataService($ctrl.dataServiceName); + $ctrl.requireAuthentication = $ctrl.requireAuthentication || true; + tubularHttp.setRequireAuthentication($ctrl.requireAuthentication); + $ctrl.editorMode = $ctrl.editorMode || 'none'; + $ctrl.canSaveState = false; + $ctrl.groupBy = ''; + $ctrl.showLoading = angular.isUndefined($ctrl.showLoading) ? true : $ctrl.showLoading; + $ctrl.autoRefresh = angular.isUndefined($ctrl.autoRefresh) ? true : $ctrl.autoRefresh; + $ctrl.serverDeleteUrl = $ctrl.serverDeleteUrl || $ctrl.serverSaveUrl; + + // Emit a welcome message + $scope.$emit('tbGrid_OnGreetParentController', $ctrl); + }; - localStorageService.set($scope.name + "_columns", $scope.columns); - }, true); + $scope.$watch('$ctrl.columns', function() { + if ($ctrl.hasColumnsDefinitions === false || $ctrl.canSaveState === false) { + return; + } - $scope.$watch('serverUrl', function(newVal, prevVal) { - if ($scope.hasColumnsDefinitions === false || $scope.currentRequest || newVal === prevVal) { - return; - } + localStorageService.set($ctrl.name + "_columns", $ctrl.columns); + }, true); - $scope.retrieveData(); - }, true); + $scope.$watch('$ctrl.serverUrl', function(newVal, prevVal) { + if ($ctrl.hasColumnsDefinitions === false || $ctrl.currentRequest || newVal === prevVal) { + return; + } - $scope.saveSearch = function() { - if ($scope.saveSearch) { - if ($scope.search.Text === '') { - localStorageService.remove($scope.name + "_search"); - } else { - localStorageService.set($scope.name + "_search", $scope.search.Text); - } - } - }; + $ctrl.retrieveData(); + }, true); - $scope.addColumn = function(item) { - if (item.Name == null) { - return; - } + $scope.$watch('$ctrl.hasColumnsDefinitions', function(newVal) { + if (newVal !== true) return; - if ($scope.hasColumnsDefinitions !== false) { - throw 'Cannot define more columns. Column definitions have been sealed'; + var isGrouping = false; + // Check columns + angular.forEach($ctrl.columns, function(column) { + if (column.IsGrouping) { + if (isGrouping) { + throw 'Only one column is allowed to grouping'; } - $scope.columns.push(item); - }; - - $scope.newRow = function(template, popup, size) { - $scope.tempRow = new TubularModel($scope, {}, $scope.dataService); - $scope.tempRow.$isNew = true; - $scope.tempRow.$isEditing = true; - $scope.tempRow.$component = $scope; + isGrouping = true; + column.Visible = false; + column.Sortable = true; + column.SortOrder = 1; + $ctrl.groupBy = column.Name; + } + }); - if (angular.isDefined(template)) { - if (angular.isDefined(popup) && popup) { - tubularPopupService.openDialog(template, $scope.tempRow, $scope, size); - } - } - }; + angular.forEach($ctrl.columns, function(column) { + if ($ctrl.groupBy == column.Name) return; - $scope.deleteRow = function (row) { - var urlparts = $scope.serverDeleteUrl.split('?'); - var url = urlparts[0] + "/" + row.$key; + if (column.Sortable && column.SortOrder > 0) { + column.SortOrder++; + } + }); - if (urlparts.length > 1) { - url += '?' + urlparts[1]; - } + $ctrl.retrieveData(); + }); - var request = { - serverUrl: url, - requestMethod: 'DELETE', - timeout: $scope.requestTimeout, - requireAuthentication: $scope.requireAuthentication, - }; + $scope.$watch('$ctrl.pageSize', function() { + if ($ctrl.hasColumnsDefinitions && $ctrl.requestCounter > 0) { + if ($ctrl.savePageSize) { + localStorageService.set($ctrl.name + "_pageSize", $ctrl.pageSize); + } + $ctrl.retrieveData(); + } + }); - $scope.currentRequest = $scope.dataService.retrieveDataAsync(request); + $scope.$watch('$ctrl.requestedPage', function() { + if ($ctrl.hasColumnsDefinitions && $ctrl.requestCounter > 0) { + $ctrl.retrieveData(); + } + }); - $scope.currentRequest.promise.then( - function(data) { - row.$hasChanges = false; - $scope.$emit('tbGrid_OnRemove', data); - }, function(error) { - $scope.$emit('tbGrid_OnConnectionError', error); - }).then(function() { - $scope.currentRequest = null; - $scope.retrieveData(); - }); - }; + $ctrl.saveSearch = function() { + if ($ctrl.saveSearch) { + if ($ctrl.search.Text === '') { + localStorageService.remove($ctrl.name + "_search"); + } else { + localStorageService.set($ctrl.name + "_search", $ctrl.search.Text); + } + } + }; - $scope.verifyColumns = function() { - var columns = localStorageService.get($scope.name + "_columns"); - if (columns == null || columns === "") { - // Nothing in settings, saving initial state - localStorageService.set($scope.name + "_columns", $scope.columns); - return; - } + $ctrl.addColumn = function(item) { + if (item.Name == null) { + return; + } - for (var index in columns) { - if (columns.hasOwnProperty(index)) { - var columnName = columns[index].Name; - var filtered = $scope.columns.filter(function(el) { return el.Name == columnName; }); + if ($ctrl.hasColumnsDefinitions !== false) { + throw 'Cannot define more columns. Column definitions have been sealed'; + } - if (filtered.length === 0) { - continue; - } + $ctrl.columns.push(item); + }; - var current = filtered[0]; - // Updates visibility by now - current.Visible = columns[index].Visible; + $ctrl.newRow = function(template, popup, size, data) { + $ctrl.tempRow = new TubularModel($scope, $ctrl, data || {}, $ctrl.dataService); + $ctrl.tempRow.$isNew = true; + $ctrl.tempRow.$isEditing = true; + $ctrl.tempRow.$component = $ctrl; - // Update sorting - if ($scope.requestCounter < 1) { - current.SortOrder = columns[index].SortOrder; - current.SortDirection = columns[index].SortDirection; - } + if (angular.isDefined(template) && angular.isDefined(popup) && popup) { + tubularPopupService.openDialog(template, $ctrl.tempRow, $ctrl, size); + } + }; - // Update Filters - if (current.Filter != null && current.Filter.Text != null) { - continue; - } + $ctrl.deleteRow = function(row) { + var urlparts = $ctrl.serverDeleteUrl.split('?'); + var url = urlparts[0] + "/" + row.$key; - if (columns[index].Filter != null && columns[index].Filter.Text != null && columns[index].Filter.Operator != 'None') { - current.Filter = columns[index].Filter; - } - } - } - }; + if (urlparts.length > 1) { + url += '?' + urlparts[1]; + } - $scope.retrieveData = function() { - // If the ServerUrl is empty skip data load - if ($scope.serverUrl == '') { - return; - } + var request = { + serverUrl: url, + requestMethod: 'DELETE', + timeout: $ctrl.requestTimeout, + requireAuthentication: $ctrl.requireAuthentication + }; - $scope.canSaveState = true; - $scope.verifyColumns(); + $ctrl.currentRequest = $ctrl.dataService.retrieveDataAsync(request); + + $ctrl.currentRequest.promise.then( + function(data) { + row.$hasChanges = false; + $scope.$emit('tbGrid_OnRemove', data); + }, function(error) { + $scope.$emit('tbGrid_OnConnectionError', error); + }).then(function() { + $ctrl.currentRequest = null; + $ctrl.retrieveData(); + }); + }; - if ($scope.savePageSize) { - $scope.pageSize = (localStorageService.get($scope.name + "_pageSize") || $scope.pageSize); - } + $ctrl.verifyColumns = function() { + var columns = localStorageService.get($ctrl.name + "_columns"); + if (columns == null || columns === "") { + // Nothing in settings, saving initial state + localStorageService.set($ctrl.name + "_columns", $ctrl.columns); + return; + } - if ($scope.pageSize < 10) $scope.pageSize = 20; // default + for (var index in columns) { + if (columns.hasOwnProperty(index)) { + var columnName = columns[index].Name; + var filtered = $ctrl.columns.filter(function(el) { return el.Name == columnName; }); - var skip = ($scope.requestedPage - 1) * $scope.pageSize; + if (filtered.length === 0) { + continue; + } - if (skip < 0) skip = 0; + var current = filtered[0]; + // Updates visibility by now + current.Visible = columns[index].Visible; - var request = { - serverUrl: $scope.serverUrl, - requestMethod: $scope.requestMethod || 'POST', - timeout: $scope.requestTimeout, - requireAuthentication: $scope.requireAuthentication, - data: { - Count: $scope.requestCounter, - Columns: $scope.columns, - Skip: skip, - Take: parseInt($scope.pageSize), - Search: $scope.search, - TimezoneOffset: new Date().getTimezoneOffset() - } - }; + // Update sorting + if ($ctrl.requestCounter < 1) { + current.SortOrder = columns[index].SortOrder; + current.SortDirection = columns[index].SortDirection; + } - if ($scope.currentRequest !== null) { - // This message is annoying when you connect errors to toastr - //$scope.currentRequest.cancel('tubularGrid(' + $scope.$id + '): new request coming.'); - return; + // Update Filters + if (current.Filter != null && current.Filter.Text != null) { + continue; } - if (angular.isUndefined($scope.onBeforeGetData) === false) { - $scope.onBeforeGetData(); + if (columns[index].Filter != null && columns[index].Filter.Text != null && columns[index].Filter.Operator != 'None') { + current.Filter = columns[index].Filter; } + } + } + }; - $scope.$emit('tbGrid_OnBeforeRequest', request, $scope); + $ctrl.retrieveData = function() { + // If the ServerUrl is empty skip data load + if ($ctrl.serverUrl == '') { + return; + } - $scope.currentRequest = $scope.dataService.retrieveDataAsync(request); + $ctrl.canSaveState = true; + $ctrl.verifyColumns(); - $scope.currentRequest.promise.then( - function(data) { - $scope.requestCounter += 1; + if ($ctrl.savePageSize) { + $ctrl.pageSize = (localStorageService.get($ctrl.name + "_pageSize") || $ctrl.pageSize); + } - if (angular.isUndefined(data) || data == null) { - $scope.$emit('tbGrid_OnConnectionError', { - statusText: "Data is empty", - status: 0 - }); + if ($ctrl.pageSize < 10) $ctrl.pageSize = 20; // default - return; - } + var skip = ($ctrl.requestedPage - 1) * $ctrl.pageSize; - $scope.dataSource = data; + if (skip < 0) skip = 0; - $scope.rows = data.Payload.map(function(el) { - var model = new TubularModel($scope, el, $scope.dataService); - model.$component = $scope; + var request = { + serverUrl: $ctrl.serverUrl, + requestMethod: $ctrl.requestMethod || 'POST', + timeout: $ctrl.requestTimeout, + requireAuthentication: $ctrl.requireAuthentication, + data: { + Count: $ctrl.requestCounter, + Columns: $ctrl.columns, + Skip: skip, + Take: parseInt($ctrl.pageSize), + Search: $ctrl.search, + TimezoneOffset: new Date().getTimezoneOffset() + } + }; - model.editPopup = function(template, size) { - tubularPopupService.openDialog(template, model, $scope, size); - }; + if ($ctrl.currentRequest !== null) { + // This message is annoying when you connect errors to toastr + //$ctrl.currentRequest.cancel('tubularGrid(' + $ctrl.$id + '): new request coming.'); + return; + } - return model; - }); + if (angular.isUndefined($ctrl.onBeforeGetData) === false) { + $ctrl.onBeforeGetData(); + } - $scope.$emit('tbGrid_OnDataLoaded', $scope); + $scope.$emit('tbGrid_OnBeforeRequest', request, $ctrl); - $scope.aggregationFunctions = data.AggregationPayload; - $scope.currentPage = data.CurrentPage; - $scope.totalPages = data.TotalPages; - $scope.totalRecordCount = data.TotalRecordCount; - $scope.filteredRecordCount = data.FilteredRecordCount; - $scope.isEmpty = $scope.filteredRecordCount === 0; + $ctrl.currentRequest = $ctrl.dataService.retrieveDataAsync(request); - if ($scope.savePage) { - localStorageService.set($scope.name + "_page", $scope.currentPage); - } - }, function(error) { - $scope.requestedPage = $scope.currentPage; - $scope.$emit('tbGrid_OnConnectionError', error); - }).then(function() { - $scope.currentRequest = null; - }); - }; + $ctrl.currentRequest.promise.then( + function(data) { + $ctrl.requestCounter += 1; - $scope.$watch('hasColumnsDefinitions', function(newVal) { - if (newVal !== true) return; + if (angular.isUndefined(data) || data == null) { + $scope.$emit('tbGrid_OnConnectionError', { + statusText: "Data is empty", + status: 0 + }); - var isGrouping = false; - // Check columns - angular.forEach($scope.columns, function(column) { - if (column.IsGrouping) { - if (isGrouping) { - throw 'Only one column is allowed to grouping'; - } + return; + } - isGrouping = true; - column.Visible = false; - column.Sortable = true; - column.SortOrder = 1; - $scope.groupBy = column.Name; - } - }); + $ctrl.dataSource = data; - angular.forEach($scope.columns, function(column) { - if ($scope.groupBy == column.Name) return; + $ctrl.rows = data.Payload.map(function(el) { + var model = new TubularModel($scope, $ctrl, el, $ctrl.dataService); + model.$component = $ctrl; - if (column.Sortable && column.SortOrder > 0) { - column.SortOrder++; - } + model.editPopup = function (template, size) { + tubularPopupService.openDialog(template, new TubularModel($scope, $ctrl, el, $ctrl.dataService), $ctrl, size); + }; + + return model; }); - $scope.retrieveData(); - }); + $scope.$emit('tbGrid_OnDataLoaded', $ctrl); - $scope.$watch('pageSize', function() { - if ($scope.hasColumnsDefinitions && $scope.requestCounter > 0) { - if ($scope.savePageSize) { - localStorageService.set($scope.name + "_pageSize", $scope.pageSize); - } - $scope.retrieveData(); - } - }); + $ctrl.aggregationFunctions = data.AggregationPayload; + $ctrl.currentPage = data.CurrentPage; + $ctrl.totalPages = data.TotalPages; + $ctrl.totalRecordCount = data.TotalRecordCount; + $ctrl.filteredRecordCount = data.FilteredRecordCount; + $ctrl.isEmpty = $ctrl.filteredRecordCount === 0; - $scope.$watch('requestedPage', function() { - // TODO: we still need to inter-lock failed, initial and paged requests - if ($scope.hasColumnsDefinitions && $scope.requestCounter > 0) { - $scope.retrieveData(); + if ($ctrl.savePage) { + localStorageService.set($ctrl.name + "_page", $ctrl.currentPage); } - }); - - $scope.sortColumn = function(columnName, multiple) { - var filterColumn = $scope.columns.filter(function(el) { - return el.Name === columnName; - }); + }, function(error) { + $ctrl.requestedPage = $ctrl.currentPage; + $scope.$emit('tbGrid_OnConnectionError', error); + }).then(function() { + $ctrl.currentRequest = null; + }); + }; - if (filterColumn.length === 0) return; + $ctrl.sortColumn = function(columnName, multiple) { + var filterColumn = $ctrl.columns.filter(function(el) { + return el.Name === columnName; + }); - var column = filterColumn[0]; + if (filterColumn.length === 0) return; - if (column.Sortable === false) return; + var column = filterColumn[0]; - // need to know if it's currently sorted before we reset stuff - var currentSortDirection = column.SortDirection; - var toBeSortDirection = currentSortDirection === 'None' ? 'Ascending' : currentSortDirection === 'Ascending' ? 'Descending' : 'None'; + if (column.Sortable === false) return; - // the latest sorting takes less priority than previous sorts - if (toBeSortDirection === 'None') { - column.SortOrder = -1; - column.SortDirection = 'None'; - } else { - column.SortOrder = Number.MAX_VALUE; - column.SortDirection = toBeSortDirection; - } + // need to know if it's currently sorted before we reset stuff + var currentSortDirection = column.SortDirection; + var toBeSortDirection = currentSortDirection === 'None' ? 'Ascending' : currentSortDirection === 'Ascending' ? 'Descending' : 'None'; - // if it's not a multiple sorting, remove the sorting from all other columns - if (multiple === false) { - angular.forEach($scope.columns.filter(function(col) { return col.Name !== columnName; }), function(col) { - col.SortOrder = -1; - col.SortDirection = 'None'; - }); - } + // the latest sorting takes less priority than previous sorts + if (toBeSortDirection === 'None') { + column.SortOrder = -1; + column.SortDirection = 'None'; + } else { + column.SortOrder = Number.MAX_VALUE; + column.SortDirection = toBeSortDirection; + } - // take the columns that actually need to be sorted in order to reindex them - var currentlySortedColumns = $scope.columns.filter(function(col) { - return col.SortOrder > 0; - }); + // if it's not a multiple sorting, remove the sorting from all other columns + if (multiple === false) { + angular.forEach($ctrl.columns.filter(function(col) { return col.Name !== columnName; }), function(col) { + col.SortOrder = -1; + col.SortDirection = 'None'; + }); + } - // reindex the sort order - currentlySortedColumns.sort(function(a, b) { - return a.SortOrder == b.SortOrder ? 0 : a.SortOrder > b.SortOrder; - }); + // take the columns that actually need to be sorted in order to reindex them + var currentlySortedColumns = $ctrl.columns.filter(function(col) { + return col.SortOrder > 0; + }); - currentlySortedColumns.forEach(function(col, index) { - col.SortOrder = index + 1; - }); + // reindex the sort order + currentlySortedColumns.sort(function(a, b) { + return a.SortOrder == b.SortOrder ? 0 : a.SortOrder > b.SortOrder; + }); - $scope.$broadcast('tbGrid_OnColumnSorted'); + currentlySortedColumns.forEach(function(col, index) { + col.SortOrder = index + 1; + }); - $scope.retrieveData(); - }; + $scope.$broadcast('tbGrid_OnColumnSorted'); - $scope.selectedRows = function() { - var rows = localStorageService.get($scope.name + "_rows"); - if (rows == null || rows === "") { - rows = []; - } + $ctrl.retrieveData(); + }; - return rows; - }; + $ctrl.selectedRows = function() { + var rows = localStorageService.get($ctrl.name + "_rows"); + if (rows == null || rows === "") { + rows = []; + } - $scope.clearSelection = function() { - angular.forEach($scope.rows, function(value) { - value.$selected = false; - }); + return rows; + }; - localStorageService.set($scope.name + "_rows", []); - }; + $ctrl.clearSelection = function() { + angular.forEach($ctrl.rows, function(value) { + value.$selected = false; + }); - $scope.isEmptySelection = function() { - return $scope.selectedRows().length === 0; - }; + localStorageService.set($ctrl.name + "_rows", []); + }; - $scope.selectFromSession = function(row) { - row.$selected = $scope.selectedRows().filter(function(el) { - return el.$key === row.$key; - }).length > 0; - }; + $ctrl.isEmptySelection = function() { + return $ctrl.selectedRows().length === 0; + }; - $scope.changeSelection = function(row) { - if (angular.isUndefined(row)) return; + $ctrl.selectFromSession = function(row) { + row.$selected = $ctrl.selectedRows().filter(function(el) { + return el.$key === row.$key; + }).length > 0; + }; - row.$selected = !row.$selected; + $ctrl.changeSelection = function(row) { + if (angular.isUndefined(row)) return; - var rows = $scope.selectedRows(); + row.$selected = !row.$selected; - if (row.$selected) { - rows.push({ $key: row.$key }); - } else { - rows = rows.filter(function(el) { - return el.$key !== row.$key; - }); - } + var rows = $ctrl.selectedRows(); - localStorageService.set($scope.name + "_rows", rows); - }; + if (row.$selected) { + rows.push({ $key: row.$key }); + } else { + rows = rows.filter(function(el) { + return el.$key !== row.$key; + }); + } - $scope.getFullDataSource = function(callback) { - $scope.dataService.retrieveDataAsync({ - serverUrl: $scope.serverUrl, - requestMethod: $scope.requestMethod || 'POST', - timeout: $scope.requestTimeout, - requireAuthentication: $scope.requireAuthentication, - data: { - Count: $scope.requestCounter, - Columns: $scope.columns, - Skip: 0, - Take: -1, - Search: { - Text: '', - Operator: 'None' - } - } - }).promise.then( - function(data) { - callback(data.Payload); - }, function(error) { - $scope.$emit('tbGrid_OnConnectionError', error); - }).then(function() { - $scope.currentRequest = null; - }); - }; + localStorageService.set($ctrl.name + "_rows", rows); + }; - $scope.visibleColumns = function() { - return $scope.columns.filter(function(el) { return el.Visible; }).length; - }; + $ctrl.getFullDataSource = function(callback) { + $ctrl.dataService.retrieveDataAsync({ + serverUrl: $ctrl.serverUrl, + requestMethod: $ctrl.requestMethod || 'POST', + timeout: $ctrl.requestTimeout, + requireAuthentication: $ctrl.requireAuthentication, + data: { + Count: $ctrl.requestCounter, + Columns: $ctrl.columns, + Skip: 0, + Take: -1, + Search: { + Text: '', + Operator: 'None' + } + } + }).promise.then( + function(data) { + callback(data.Payload); + }, function(error) { + $scope.$emit('tbGrid_OnConnectionError', error); + }).then(function() { + $ctrl.currentRequest = null; + }); + }; - $scope.$emit('tbGrid_OnGreetParentController', $scope); - } - ] - }; - } - ]) + $ctrl.visibleColumns = function() { + return $ctrl.columns.filter(function(el) { return el.Visible; }).length; + }; + } + ] + }) /** * @ngdoc directive * @name tbGridTable + * @module tubular.directives * @restrict E * * @description @@ -1264,7 +1034,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) scope: true, controller: [ '$scope', function($scope) { - $scope.$component = $scope.$parent.$parent; + $scope.$component = $scope.$parent.$parent.$ctrl; $scope.tubularDirective = 'tubular-grid-table'; } ] @@ -1274,6 +1044,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) /** * @ngdoc directive * @name tbColumnDefinitions + * @module tubular.directives * @restrict E * * @description @@ -1312,6 +1083,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) /** * @ngdoc directive * @name tbColumn + * @module tubular.directives * @restrict E * * @description @@ -1320,8 +1092,6 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * * This directive is replace by a `th` HTML element. * - * @scope - * * @param {string} name Set the column name. * @param {string} label Set the column label, if empty column's name is used. * @param {boolean} sortable Set if column is sortable. @@ -1334,7 +1104,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {boolean} isGrouping Define a group key. */ .directive('tbColumn', [ - 'tubularGridColumnModel', function (ColumnModel) { + function () { return { require: '^tbColumnDefinitions', template: '', @@ -1343,13 +1113,24 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) transclude: true, scope: { visible: '=', - label: '@?' + label: '@', + name: '@', + sortable: '=?', + sortOrder: '=?', + isKey: '=?', + searchable: '=?', + columnType: '@?', + isGrouping: '=?', + aggregate: '@?', + metaAggregate: '@?', + sortDirection: '@?' }, controller: [ '$scope', function ($scope) { $scope.column = { Label: '' }; $scope.$component = $scope.$parent.$parent.$component; $scope.tubularDirective = 'tubular-column'; + $scope.label = angular.isDefined($scope.label) ? $scope.label : ($scope.name || '').replace(/([a-z])([A-Z])/g, '$1 $2'); $scope.sortColumn = function (multiple) { $scope.$component.sortColumn($scope.column.Name, multiple); @@ -1361,30 +1142,53 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) } }); - $scope.$watch('label', function () { + $scope.$watch('label', function() { $scope.column.Label = $scope.label; // this broadcast here is used for backwards compatibility with tbColumnHeader requiring a scope.label value on its own $scope.$broadcast('tbColumn_LabelChanged', $scope.label); - }) - } - ], - compile: function compile() { - return { - pre: function (scope, lElement, lAttrs) { - lAttrs.label = angular.isDefined(lAttrs.label) ? lAttrs.label : (lAttrs.name || '').replace(/([a-z])([A-Z])/g, '$1 $2'); + }); - var column = new ColumnModel(lAttrs); - scope.$component.addColumn(column); - scope.column = column; - scope.label = column.Label; - } - }; - } + var column = new function () { + this.Name = $scope.name || null; + this.Label = $scope.label || null; + this.Sortable = $scope.sortable; + this.SortOrder = parseInt($scope.sortOrder) || -1; + this.SortDirection = function () { + if (angular.isUndefined($scope.sortDirection)) { + return 'None'; + } + + if ($scope.sortDirection.toLowerCase().indexOf('asc') === 0) { + return 'Ascending'; + } + + if ($scope.sortDirection.toLowerCase().indexOf('desc') === 0) { + return 'Descending'; + } + + return 'None'; + }(); + + this.IsKey = $scope.isKey === "true"; + this.Searchable = angular.isDefined($scope.searchable) ? $scope.searchable : false; + this.Visible = $scope.visible === "false" ? false : true; + this.Filter = null; + this.DataType = $scope.columnType || "string"; + this.IsGrouping = $scope.isGrouping === "true"; + this.Aggregate = $scope.aggregate || "none"; + this.MetaAggregate = $scope.metaAggregate || "none"; + }; + + $scope.$component.addColumn(column); + $scope.column = column; + $scope.label = column.Label; + } + ] }; - } - ]) + }]) /** * @ngdoc directive + * @module tubular.directives * @name tbColumnHeader * @restrict E * @@ -1397,33 +1201,32 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @scope */ .directive('tbColumnHeader', [ - '$compile', function ($compile) { - + function() { return { require: '^tbColumn', template: '' + - '{{ $parent.column.Label }}' + - ' ' + - ' ' + - '', + '{{ $parent.column.Label }}' + + ' ' + + ' ' + + '', restrict: 'E', replace: true, transclude: true, scope: false, controller: [ - '$scope', function ($scope) { - $scope.sortColumn = function ($event) { + '$scope', function($scope) { + $scope.sortColumn = function($event) { $scope.$parent.sortColumn($event.ctrlKey); }; // this listener here is used for backwards compatibility with tbColumnHeader requiring a scope.label value on its own - $scope.$on('tbColumn_LabelChanged', function ($event, value) { + $scope.$on('tbColumn_LabelChanged', function($event, value) { $scope.label = value; - }) + }); } ], - link: function ($scope, $element, $attrs, controller) { - if ($element.find('[ng-transclude] *').length > 0) { - $element.find('span.column-header-default').remove(); + link: function($scope, $element) { + if ($element.find('ng-transclude').length > 0) { + $element.find('span')[0].remove(); } if (!$scope.$parent.column.Sortable) { @@ -1436,6 +1239,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) /** * @ngdoc directive * @name tbRowSet + * @module tubular.directives * @restrict E * * @description @@ -1467,6 +1271,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) /** * @ngdoc directive * @name tbFootSet + * @module tubular.directives * @restrict E * * @description @@ -1477,7 +1282,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @scope */ .directive('tbFootSet', [ - function () { + function() { return { require: '^tbGrid', @@ -1487,7 +1292,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) transclude: true, scope: false, controller: [ - '$scope', function ($scope) { + '$scope', function($scope) { $scope.$component = $scope.$parent.$component || $scope.$parent.$parent.$component; $scope.tubularDirective = 'tubular-foot-set'; } @@ -1498,6 +1303,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) /** * @ngdoc directive * @name tbRowTemplate + * @module tubular.directives * @restrict E * * @description @@ -1514,7 +1320,6 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) function() { return { - // TODO: I can't choose one require: ['^tbRowSet', '^tbFootSet'], template: '', @@ -1527,7 +1332,6 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) }, controller: [ '$scope', function($scope) { - // TODO: Rename this directive $scope.tubularDirective = 'tubular-rowset'; $scope.fields = []; $scope.hasFieldsDefinitions = false; @@ -1571,9 +1375,11 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) }; } ]) + /** * @ngdoc directive * @name tbCellTemplate + * @module tubular.directives * @restrict E * * @description @@ -1587,7 +1393,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {string} columnName Setting the related column, by passing the name, the cell can share attributes (like visibility) with the column. */ .directive('tbCellTemplate', [ - function() { + function () { return { require: '^tbRowTemplate', @@ -1599,7 +1405,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) columnName: '@?' }, controller: [ - '$scope', function($scope) { + '$scope', function ($scope) { $scope.column = { Visible: true }; $scope.columnName = $scope.columnName || null; $scope.$component = $scope.$parent.$parent.$component; @@ -1611,7 +1417,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) if ($scope.columnName != null) { var columnModel = $scope.$component.columns - .filter(function(el) { return el.Name === $scope.columnName; }); + .filter(function (el) { return el.Name === $scope.columnName; }); if (columnModel.length > 0) { $scope.column = columnModel[0]; @@ -1622,22 +1428,34 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) }; } ]); -})(); -(function() { +})(window.angular); +(function (angular) { 'use strict'; + if (typeof moment == 'function') { + moment.fn.toJSON = function() { return this.format(); } + } + + var canUseHtml5Date = function() { + var input = document.createElement('input'); + input.setAttribute('type', 'date'); + + var notADateValue = 'not-a-date'; + input.setAttribute('value', notADateValue); + + return (input.value !== notADateValue); + }(); + angular.module('tubular.directives') /** - * @ngdoc directive + * @ngdoc component * @name tbSimpleEditor - * @restrict E - * + * @module tubular.directives + * * @description - * The `tbSimpleEditor` directive is the basic input to show in a grid or form. + * The `tbSimpleEditor` component is the basic input to show in a grid or form. * It uses the `TubularModel` to retrieve column or field information. * - * @scope - * * @param {string} name Set the field name. * @param {object} value Set the value. * @param {boolean} isEditing Indicate if the field is showing editor. @@ -1652,71 +1470,94 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {number} max Set the maximum characters. * @param {string} regex Set the regex validation text. * @param {string} regexErrorMessage Set the regex validation error message. + * @param {string} match Set the field name to match values. */ - .directive('tbSimpleEditor', [ - 'tubularEditorService', '$filter', function(tubularEditorService, $filter) { - - return { - template: '
' + - '{{value}}' + - '' + - '' + - '{{error}}' + - '{{help}}' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: angular.extend({ regex: '@?', regexErrorMessage: '@?' }, tubularEditorService.defaultScope), - controller: [ - '$scope', function($scope) { - $scope.validate = function () { - if (angular.isDefined($scope.regex) && $scope.regex != null && angular.isDefined($scope.value) && $scope.value != null) { - var patt = new RegExp($scope.regex); - - if (patt.test($scope.value) === false) { - $scope.$valid = false; - $scope.state.$errors = [$scope.regexErrorMessage || $filter('translate')('EDITOR_REGEX_DOESNT_MATCH')]; - return; - } - } + .component('tbSimpleEditor', { + template: '
' + + '{{$ctrl.value}}' + + '' + + '' + + '{{error}}' + + '{{$ctrl.help}}' + + '
', + bindings: { + regex: '@?', + regexErrorMessage: '@?', + value: '=?', + isEditing: '=?', + editorType: '@', + showLabel: '=?', + label: '@?', + required: '=?', + min: '=?', + max: '=?', + name: '@', + placeholder: '@?', + readOnly: '=?', + help: '@?', + match: '@?' + }, + controller: [ + 'tubularEditorService', '$scope', '$filter', function (tubularEditorService, $scope, $filter) { + var $ctrl = this; + + $ctrl.validate = function () { + if (angular.isDefined($ctrl.regex) && $ctrl.regex != null && angular.isDefined($ctrl.value) && $ctrl.value != null && $ctrl.value != '') { + var patt = new RegExp($ctrl.regex); + + if (patt.test($ctrl.value) === false) { + $ctrl.$valid = false; + $ctrl.state.$errors = [$ctrl.regexErrorMessage || $filter('translate')('EDITOR_REGEX_DOESNT_MATCH')]; + return; + } + } - if (angular.isDefined($scope.min) && angular.isDefined($scope.value) && $scope.value != null) { - if ($scope.value.length < parseInt($scope.min)) { - $scope.$valid = false; - $scope.state.$errors = [$filter('translate')('EDITOR_MIN_CHARS', $scope.min)]; - return; - } - } + if (angular.isDefined($ctrl.match) && $ctrl.match) { + if ($ctrl.value != $scope.$component.model[$ctrl.match]) { + var label = $filter('filter')($scope.$component.fields, { name: $ctrl.match }, true)[0].label; + $ctrl.$valid = false; + $ctrl.state.$errors = [$filter('translate')('EDITOR_MATCH', label)]; + return; + } + } - if (angular.isDefined($scope.max) && angular.isDefined($scope.value) && $scope.value != null) { - if ($scope.value.length > parseInt($scope.max)) { - $scope.$valid = false; - $scope.state.$errors = [$filter('translate')('EDITOR_MAX_CHARS', $scope.max)]; - return; - } - } - }; + if (angular.isDefined($ctrl.min) && angular.isDefined($ctrl.value) && $ctrl.value != null) { + if ($ctrl.value.length < parseInt($ctrl.min)) { + $ctrl.$valid = false; + $ctrl.state.$errors = [$filter('translate')('EDITOR_MIN_CHARS', $ctrl.min)]; + return; + } + } - tubularEditorService.setupScope($scope); + if (angular.isDefined($ctrl.max) && angular.isDefined($ctrl.value) && $ctrl.value != null) { + if ($ctrl.value.length > parseInt($ctrl.max)) { + $ctrl.$valid = false; + $ctrl.state.$errors = [$filter('translate')('EDITOR_MAX_CHARS', $ctrl.max)]; + return; + } } - ] - }; - } - ]) + }; + + $ctrl.$onInit = function() { + tubularEditorService.setupScope($scope, null, $ctrl, false); + }; + } + ] + }) /** - * @ngdoc directive + * @ngdoc component * @name tbNumericEditor - * @restrict E + * @module tubular.directives * * @description - * The `tbNumericEditor` directive is numeric input, similar to `tbSimpleEditor` - * but can render an addon to the input visual element. + * The `tbNumericEditor` component is numeric input, similar to `tbSimpleEditor` + * but can render an add-on to the input visual element. * - * It uses the `TubularModel` to retrieve column or field information. + * When you need a numeric editor but without the visual elements you can use + * `tbSimpleEditor` with the `editorType` attribute with value `number`. * - * @scope + * This component uses the `TubularModel` to retrieve the model information. * * @param {string} name Set the field name. * @param {object} value Set the value. @@ -1732,69 +1573,78 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {number} max Set the maximum value. * @param {number} step Set the step setting, default 'any'. */ - .directive('tbNumericEditor', [ - 'tubularEditorService', '$filter', function (tubularEditorService, $filter) { - - return { - template: '
' + - '{{value | numberorcurrency: format }}' + - '' + - '
' + - '
{{format == \'C\' ? \'$\' : \'.\'}}
' + - '' + - '

{{value | numberorcurrency: format}}

' + - '
' + - '{{error}}' + - '{{help}}' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: angular.extend({ step: '=?' }, tubularEditorService.defaultScope), - controller: [ - '$scope', function ($scope) { - $scope.DataType = "numeric"; + .component('tbNumericEditor', { + template: '
' + + '{{$ctrl.value | numberorcurrency: format }}' + + '' + + '
' + + '
' + + '' + + '
' + + '' + + '

{{$ctrl.value | numberorcurrency: format}}

' + + '
' + + '{{error}}' + + '{{$ctrl.help}}' + + '
', + bindings: { + value: '=?', + isEditing: '=?', + showLabel: '=?', + label: '@?', + required: '=?', + format: '@?', + min: '=?', + max: '=?', + name: '@', + placeholder: '@?', + readOnly: '=?', + help: '@?', + step: '=?' + }, + controller: [ + 'tubularEditorService', '$scope', '$filter', function (tubularEditorService, $scope, $filter) { + var $ctrl = this; + $ctrl.validate = function () { + if (angular.isDefined($ctrl.min) && angular.isDefined($ctrl.value) && $ctrl.value != null) { + $ctrl.$valid = $ctrl.value >= $ctrl.min; + if (!$ctrl.$valid) { + $ctrl.state.$errors = [$filter('translate')('EDITOR_MIN_NUMBER', $ctrl.min)]; + } + } - $scope.validate = function() { - if (angular.isDefined($scope.min) && angular.isDefined($scope.value) && $scope.value != null) { - $scope.$valid = $scope.value >= $scope.min; - if (!$scope.$valid) { - $scope.state.$errors = [$filter('translate')('EDITOR_MIN_NUMBER', $scope.min)]; - } - } + if (!$ctrl.$valid) { + return; + } - if (!$scope.$valid) { - return; - } + if (angular.isDefined($ctrl.max) && angular.isDefined($ctrl.value) && $ctrl.value != null) { + $ctrl.$valid = $ctrl.value <= $ctrl.max; + if (!$ctrl.$valid) { + $ctrl.state.$errors = [$filter('translate')('EDITOR_MAX_NUMBER', $ctrl.max)]; + } + } + }; - if (angular.isDefined($scope.max) && angular.isDefined($scope.value) && $scope.value != null) { - $scope.$valid = $scope.value <= $scope.max; - if (!$scope.$valid) { - $scope.state.$errors = [$filter('translate')('EDITOR_MAX_NUMBER', $scope.max)]; - } - } - }; + $ctrl.$onInit = function() { + $ctrl.DataType = "numeric"; - tubularEditorService.setupScope($scope, 0); - } - ] - }; - } - ]) + tubularEditorService.setupScope($scope, 0, $ctrl, false); + }; + } + ] + }) /** - * @ngdoc directive + * @ngdoc component * @name tbDateTimeEditor - * @restrict E + * @module tubular.directives * * @description - * The `tbDateTimeEditor` directive is date/time input. It uses the `datetime-local` HTML5 attribute, but if this - * components fails it falls back to a jQuery datepicker. + * The `tbDateTimeEditor` component is date/time input. It uses the `datetime-local` HTML5 attribute, but if this + * components fails it falls back to Angular UI Bootstrap Datepicker (time functionality is unavailable). * * It uses the `TubularModel` to retrieve column or field information. * - * @scope - * * @param {string} name Set the field name. * @param {object} value Set the value. * @param {boolean} isEditing Indicate if the field is showing editor. @@ -1807,65 +1657,100 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {number} min Set the minimum value. * @param {number} max Set the maximum value. */ - .directive('tbDateTimeEditor', [ - 'tubularEditorService', function(tubularEditorService) { + .component('tbDateTimeEditor', { + template: '
' + + '{{ $ctrl.value | date: format }}' + + '' + + (canUseHtml5Date ? + '' : + '
' + + '' + + '' + + '' + + '' + + '
') + + '{{error}}' + + '' + + '{{$ctrl.help}}' + + '
', + bindings: { + value: '=?', + isEditing: '=?', + showLabel: '=?', + label: '@?', + required: '=?', + format: '@?', + min: '=?', + max: '=?', + name: '@', + readOnly: '=?', + help: '@?' + }, + controller: [ + '$scope', '$element', 'tubularEditorService', '$filter', function ($scope, $element, tubularEditorService, $filter) { + var $ctrl = this; - return { - template: '
' + - '{{ value | date: format }}' + - '' + - '' + - '' + - '{{error}}' + - '' + - '{{help}}' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: tubularEditorService.defaultScope, - controller: tubularEditorService.dateEditorController('yyyy-MM-dd HH:mm'), - compile: function compile() { - return { - post: function(scope, lElement) { - var inp = $(lElement).find("input[type=datetime-local]")[0]; - if (inp.type !== 'datetime-local') { - $(inp).datepicker({ - dateFormat: scope.format.toLowerCase().split(' ')[0] - }) - .datepicker("setDate", scope.value) - .on("dateChange", function(e) { - scope.$apply(function() { - scope.value = e.date; + // This could be $onChange?? + $scope.$watch(function () { + return $ctrl.value; + }, function (val) { + if (typeof (val) === 'string') { + $ctrl.value = new Date(val); + } + }); - if (angular.isDefined(scope.$parent.Model)) { - scope.$parent.Model.$hasChanges = true; - } - }); - }); - } + $ctrl.validate = function () { + if (angular.isDefined($ctrl.min)) { + if (Object.prototype.toString.call($ctrl.min) !== "[object Date]") { + $ctrl.min = new Date($ctrl.min); } - }; - } - }; - } - ]) + + $ctrl.$valid = $ctrl.value >= $ctrl.min; + + if (!$ctrl.$valid) { + $ctrl.state.$errors = [$filter('translate')('EDITOR_MIN_DATE', $filter('date')($ctrl.min, $ctrl.format))]; + } + } + + if (!$ctrl.$valid) { + return; + } + + if (angular.isDefined($ctrl.max)) { + if (Object.prototype.toString.call($ctrl.max) !== "[object Date]") { + $ctrl.max = new Date($ctrl.max); + } + + $ctrl.$valid = $ctrl.value <= $ctrl.max; + + if (!$ctrl.$valid) { + $ctrl.state.$errors = [$filter('translate')('EDITOR_MAX_DATE', $filter('date')($ctrl.max, $ctrl.format))]; + } + } + }; + + $ctrl.$onInit = function () { + $ctrl.DataType = "date"; + tubularEditorService.setupScope($scope, $ctrl.format, $ctrl); + }; + } + ] + }) /** - * @ngdoc directive + * @ngdoc component * @name tbDateEditor - * @restrict E + * @module tubular.directives * * @description - * The `tbDateEditor` directive is date input. It uses the `datetime-local` HTML5 attribute, but if this - * components fails it falls back to a jQuery datepicker. + * The `tbDateEditor` component is date input. It uses the `datetime-local` HTML5 attribute, but if this + * components fails it falls back to a Angular UI Bootstrap Datepicker. * * Similar to `tbDateTimeEditor` but without a timepicker. * * It uses the `TubularModel` to retrieve column or field information. * - * @scope - * * @param {string} name Set the field name. * @param {object} value Set the value. * @param {boolean} isEditing Indicate if the field is showing editor. @@ -1878,63 +1763,121 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {number} min Set the minimum value. * @param {number} max Set the maximum value. */ - .directive('tbDateEditor', [ - 'tubularEditorService', function(tubularEditorService) { - - return { - template: '
' + - '{{ value | date: format }}' + - '' + - '' + - '' + - '{{error}}' + - '' + - '{{help}}' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: tubularEditorService.defaultScope, - controller: tubularEditorService.dateEditorController('yyyy-MM-dd'), - compile: function compile() { - return { - post: function(scope, lElement) { - var inp = $(lElement).find("input[type=date]")[0]; - if (inp.type !== 'date') { - $(inp).datepicker({ - dateFormat: scope.format.toLowerCase() - }) - .datepicker("setDate", scope.value) - .on("dateChange", function(e) { - scope.$apply(function() { - scope.value = e.date; - - if (angular.isDefined(scope.$parent.Model)) { - scope.$parent.Model.$hasChanges = true; - } - }); - }); - } - } - }; - } - }; - } - ]) + .component('tbDateEditor', { + template: '
' + + '{{ $ctrl.value | moment: $ctrl.format }}' + + '' + + (canUseHtml5Date ? + '' : + '
' + + '' + + '' + + '' + + '' + + '
') + + '' + + '{{error}}' + + '' + + '{{$ctrl.help}}' + + '
', + bindings: { + value: '=?', + isEditing: '=?', + showLabel: '=?', + label: '@?', + required: '=?', + format: '@?', + min: '=?', + max: '=?', + name: '@', + readOnly: '=?', + help: '@?' + }, + controller: [ + '$scope', '$element', 'tubularEditorService', '$filter', function ($scope, $element, tubularEditorService, $filter) { + var $ctrl = this; + + $scope.$watch(function() { + return $ctrl.value; + }, function (val) { + if (angular.isUndefined(val)) return; + + if (typeof (val) === 'string') { + $ctrl.value = typeof moment == 'function' ? moment(val) : new Date(val); + } + + if (angular.isUndefined($ctrl.dateValue)) { + if (typeof moment == 'function') { + var tmpDate = $ctrl.value.toObject(); + $ctrl.dateValue = new Date(tmpDate.years, tmpDate.months, tmpDate.date, tmpDate.hours, tmpDate.minutes, tmpDate.seconds); + } else { + $ctrl.dateValue = $ctrl.value; + } + + $scope.$watch(function() { + return $ctrl.dateValue; + }, function(val) { + if (angular.isDefined(val)) { + $ctrl.value = typeof moment == 'function' ? moment(val) : new Date(val); + } + }); + } + }); + + $ctrl.validate = function () { + if (angular.isDefined($ctrl.min)) { + if (Object.prototype.toString.call($ctrl.min) !== "[object Date]") { + $ctrl.min = new Date($ctrl.min); + } + + $ctrl.$valid = $ctrl.dateValue >= $ctrl.min; + + if (!$ctrl.$valid) { + $ctrl.state.$errors = [$filter('translate')('EDITOR_MIN_DATE', $filter('date')($ctrl.min, $ctrl.format))]; + } + } + + if (!$ctrl.$valid) { + return; + } + + if (angular.isDefined($ctrl.max)) { + if (Object.prototype.toString.call($ctrl.max) !== "[object Date]") { + $ctrl.max = new Date($ctrl.max); + } + + $ctrl.$valid = $ctrl.dateValue <= $ctrl.max; + + if (!$ctrl.$valid) { + $ctrl.state.$errors = [$filter('translate')('EDITOR_MAX_DATE', $filter('date')($ctrl.max, $ctrl.format))]; + } + } + }; + + $ctrl.$onInit = function() { + $ctrl.DataType = "date"; + tubularEditorService.setupScope($scope, $ctrl.format, $ctrl); + + if (typeof moment == 'function' && angular.isUndefined($ctrl.format)) { + $ctrl.format = "MMM D, Y"; + } + }; + } + ] + }) /** - * @ngdoc directive + * @ngdoc component * @name tbDropdownEditor - * @restrict E + * @module tubular.directives * * @description - * The `tbDropdownEditor` directive is drowpdown editor, it can get information from a HTTP + * The `tbDropdownEditor` component is drowpdown editor, it can get information from a HTTP * source or it can be an object declared in the attributes. * * It uses the `TubularModel` to retrieve column or field information. * - * @scope - * * @param {string} name Set the field name. * @param {object} value Set the value. * @param {boolean} isEditing Indicate if the field is showing editor. @@ -1950,97 +1893,123 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {string} optionKey Set the property to get the keys. * @param {string} defaultValue Set the default value. */ - .directive('tbDropdownEditor', [ - 'tubularEditorService', function(tubularEditorService) { + .component('tbDropdownEditor', { + template: '
' + + '{{ $ctrl.value }}' + + '' + + '' + - '' + - '{{error}}' + - '' + - '{{help}}' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: angular.extend({ options: '=?', optionsUrl: '@', optionsMethod: '@?', optionLabel: '@?', optionKey: '@?' }, tubularEditorService.defaultScope), - controller: [ - '$scope', function($scope) { - tubularEditorService.setupScope($scope); - $scope.dataIsLoaded = false; - $scope.selectOptions = "d for d in options"; + $ctrl.$onInit = function() { + tubularEditorService.setupScope($scope, null, $ctrl); + $ctrl.dataIsLoaded = false; + $ctrl.selectOptions = "d for d in $ctrl.options"; - if (angular.isDefined($scope.optionLabel)) { - $scope.selectOptions = "d." + $scope.optionLabel + " for d in options"; + if (angular.isDefined($ctrl.optionLabel)) { + $ctrl.selectOptions = "d." + $ctrl.optionLabel + " for d in options"; - if (angular.isDefined($scope.optionKey)) { - $scope.selectOptions = 'd.' + $scope.optionKey + ' as ' + $scope.selectOptions; + if (angular.isDefined($ctrl.optionTrack)) { + $ctrl.selectOptions = 'd as d.' + $ctrl.optionLabel + ' for d in options track by d.' + $ctrl.optionTrack; + } + else { + if (angular.isDefined($ctrl.optionKey)) { + $ctrl.selectOptions = 'd.' + $ctrl.optionKey + ' as ' + $ctrl.selectOptions; } } + } - $scope.$watch('value', function(val) { - $scope.$emit('tbForm_OnFieldChange', $scope.$component, $scope.name, val); - }); - - $scope.loadData = function() { - if ($scope.dataIsLoaded) { - return; - } + if (angular.isDefined($ctrl.optionsUrl)) { + $scope.$watch('optionsUrl', function(val, prev) { + if (val === prev) return; - if (angular.isUndefined($scope.$component) || $scope.$component == null) { - throw 'You need to define a parent Form or Grid'; - } + $ctrl.dataIsLoaded = false; + $ctrl.loadData(); + }); - var currentRequest = $scope.$component.dataService.retrieveDataAsync({ - serverUrl: $scope.optionsUrl, - requestMethod: $scope.optionsMethod || 'GET' + if ($ctrl.isEditing) { + $ctrl.loadData(); + } else { + $scope.$watch('$ctrl.isEditing', function () { + if ($ctrl.isEditing) { + $ctrl.loadData(); + } }); + } + } + }; - var value = $scope.value; - $scope.value = ''; - - currentRequest.promise.then( - function(data) { - $scope.options = data; - $scope.dataIsLoaded = true; - // TODO: Add an attribute to define if autoselect is OK - var possibleValue = $scope.options && $scope.options.length > 0 ? - angular.isDefined($scope.optionKey) ? $scope.options[0][$scope.optionKey] : $scope.options[0] - : ''; - $scope.value = value || $scope.defaultValue || possibleValue; - }, function(error) { - $scope.$emit('tbGrid_OnConnectionError', error); - }); - }; + $scope.$watch(function() { + return $ctrl.value; + }, function(val) { + $scope.$emit('tbForm_OnFieldChange', $ctrl.$component, $ctrl.name, val, $scope.options); + }); - if (angular.isDefined($scope.optionsUrl)) { - $scope.$watch('optionsUrl', function() { - $scope.dataIsLoaded = false; - $scope.loadData(); - }); + $ctrl.loadData = function() { + if ($ctrl.dataIsLoaded) { + return; + } - if ($scope.isEditing) { - $scope.loadData(); - } else { - $scope.$watch('isEditing', function() { - if ($scope.isEditing) { - $scope.loadData(); - } - }); - } - } + if (angular.isUndefined($ctrl.$component) || $ctrl.$component == null) { + throw 'You need to define a parent Form or Grid'; } - ] - }; - } - ]) + + var currentRequest = $ctrl.$component.dataService.retrieveDataAsync({ + serverUrl: $ctrl.optionsUrl, + requestMethod: $ctrl.optionsMethod || 'GET' + }); + + var value = $ctrl.value; + $ctrl.value = ''; + + currentRequest.promise.then( + function(data) { + $ctrl.options = data; + $ctrl.dataIsLoaded = true; + // TODO: Add an attribute to define if autoselect is OK + var possibleValue = $ctrl.options && $ctrl.options.length > 0 ? + angular.isDefined($ctrl.optionKey) ? $ctrl.options[0][$ctrl.optionKey] : $ctrl.options[0] + : ''; + $ctrl.value = value || $ctrl.defaultValue || possibleValue; + + // Set the field dirty + var formScope = $ctrl.getFormField(); + if (formScope) formScope.$setDirty(); + }, function(error) { + $scope.$emit('tbGrid_OnConnectionError', error); + }); + }; + } + ] + }) /** * @ngdoc directive * @name tbTypeaheadEditor + * @module tubular.directives * @restrict E * * @description @@ -2065,38 +2034,52 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {string} css Set the CSS classes for the input. */ .directive('tbTypeaheadEditor', [ - 'tubularEditorService', '$q', function(tubularEditorService, $q) { + 'tubularEditorService', '$q', '$compile', function (tubularEditorService, $q, $compile) { return { - template: '
' + - '{{ value }}' + - '' + - '
' + - ' ' + - '
' + - '' + - '
' + - '' + - '{{error}}' + - '' + - '{{help}}' + - '
', restrict: 'E', replace: true, - transclude: true, - scope: angular.extend({ + scope: { + value: '=?', + isEditing: '=?', + showLabel: '=?', + label: '@?', + required: '=?', + name: '@', + placeholder: '@?', + readOnly: '=?', + help: '@?', options: '=?', optionsUrl: '@', optionsMethod: '@?', optionLabel: '@?', css: '@?' - }, tubularEditorService.defaultScope), + }, + link: function (scope, element) { + var template = '
' + + '{{ value }}' + + '' + + '
' + + ' ' + + '
' + + '' + + '
' + + '' + + '{{error}}' + + '' + + '{{help}}' + + '
'; + + var linkFn = $compile(template); + var content = linkFn(scope); + element.append(content); + }, controller: [ - '$scope', function($scope) { + '$scope', function ($scope) { tubularEditorService.setupScope($scope); $scope.selectOptions = "d for d in getValues($viewValue)"; $scope.lastSet = []; @@ -2105,15 +2088,15 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) $scope.selectOptions = "d as d." + $scope.optionLabel + " for d in getValues($viewValue)"; } - $scope.$watch('value', function(val) { - $scope.$emit('tbForm_OnFieldChange', $scope.$component, $scope.name, val); + $scope.$watch('value', function (val) { + $scope.$emit('tbForm_OnFieldChange', $scope.$component, $scope.name, val, $scope.options); $scope.tooltip = val; if (angular.isDefined(val) && val != null && angular.isDefined($scope.optionLabel)) { $scope.tooltip = val[$scope.optionLabel]; } }); - $scope.getValues = function(val) { + $scope.getValues = function (val) { if (angular.isDefined($scope.optionsUrl)) { if (angular.isUndefined($scope.$component) || $scope.$component == null) { throw 'You need to define a parent Form or Grid'; @@ -2124,7 +2107,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) requestMethod: $scope.optionsMethod || 'GET' }).promise; - p.then(function(data) { + p.then(function (data) { $scope.lastSet = data; return data; }); @@ -2132,7 +2115,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) return p; } - return $q(function(resolve) { + return $q(function (resolve) { $scope.lastSet = $scope.options; resolve($scope.options); }); @@ -2143,47 +2126,44 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) } ]) /** - * @ngdoc directive + * @ngdoc component * @name tbHiddenField - * @restrict E + * @module tubular.directives * * @description - * The `tbHiddenField` directive represents a hidden field. + * The `tbHiddenField` component represents a hidden field. * * It uses the `TubularModel` to retrieve column or field information. * - * @scope * @param {string} name Set the field name. * @param {object} value Set the value. */ - .directive('tbHiddenField', [ - 'tubularEditorService', function(tubularEditorService) { + .component('tbHiddenField', { + template: '', + bindings: { + value: '=?', + name: '@' + }, + controller: [ + 'tubularEditorService', '$scope', function (tubularEditorService, $scope) { + var $ctrl = this; - return { - template: '', - restrict: 'E', - replace: true, - transclude: true, - scope: tubularEditorService.defaultScope, - controller: [ - '$scope', function($scope) { - tubularEditorService.setupScope($scope); - } - ] - }; - } - ]) + $ctrl.$onInit = function() { + tubularEditorService.setupScope($scope, null, $ctrl, true); + }; + } + ] + }) /** - * @ngdoc directive + * @ngdoc component * @name tbCheckboxField - * @restrict E + * @module tubular.directives * * @description - * The `tbCheckboxField` directive represents a checkbox field. + * The `tbCheckboxField` component represents a checkbox field. * * It uses the `TubularModel` to retrieve column or field information. * - * @scope * @param {string} name Set the field name. * @param {object} value Set the value. * @param {object} checkedValue Set the checked value. @@ -2193,55 +2173,57 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {string} label Set the field's label otherwise the name is used. * @param {string} help Set the help text. */ - .directive('tbCheckboxField', [ - 'tubularEditorService', function(tubularEditorService) { + .component('tbCheckboxField', { + template: '
' + + ' ' + + '' + + '' + + '{{error}}' + + '' + + '{{help}}' + + '
', + bindings: { + value: '=?', + isEditing: '=?', + editorType: '@', + showLabel: '=?', + label: '@?', + required: '=?', + name: '@', + readOnly: '=?', + help: '@?', + checkedValue: '=?', + uncheckedValue: '=?' + }, + controller: [ + 'tubularEditorService', '$scope', function (tubularEditorService, $scope) { + var $ctrl = this; - return { - template: '
' + - '{{value ? checkedValue : uncheckedValue}}' + - ' ' + - '' + - '' + - '{{error}}' + - '' + - '{{help}}' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: angular.extend({ - checkedValue: '=?', - uncheckedValue: '=?' - }, tubularEditorService.defaultScope), - controller: [ - '$scope', '$element', function ($scope) { - $scope.required = false; // overwrite required to false always - $scope.checkedValue = angular.isDefined($scope.checkedValue) ? $scope.checkedValue : true; - $scope.uncheckedValue = angular.isDefined($scope.uncheckedValue) ? $scope.uncheckedValue : false; + $ctrl.$onInit = function () { + $ctrl.required = false; // overwrite required to false always + $ctrl.checkedValue = angular.isDefined($ctrl.checkedValue) ? $ctrl.checkedValue : true; + $ctrl.uncheckedValue = angular.isDefined($ctrl.uncheckedValue) ? $ctrl.uncheckedValue : false; - tubularEditorService.setupScope($scope); - } - ] - }; - } - ]) + tubularEditorService.setupScope($scope, null, $ctrl, true); + }; + } + ] + }) /** - * @ngdoc directive + * @ngdoc component * @name tbTextArea - * @restrict E + * @module tubular.directives * * @description - * The `tbTextArea` directive represents a textarea field. + * The `tbTextArea` component represents a textarea field. * Similar to `tbSimpleEditor` but with a `textarea` HTML element instead of `input`. * * It uses the `TubularModel` to retrieve column or field information. - * - * @scope - * + * * @param {string} name Set the field name. * @param {object} value Set the value. * @param {boolean} isEditing Indicate if the field is showing editor. @@ -2254,93 +2236,282 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {number} min Set the minimum characters. * @param {number} max Set the maximum characters. */ - .directive('tbTextArea', [ - 'tubularEditorService', '$filter', function (tubularEditorService, $filter) { + .component('tbTextArea', { + template: '
' + + '{{$ctrl.value}}' + + '' + + '' + + '' + + '{{error}}' + + '' + + '{{$ctrl.help}}' + + '
', + bindings: { + value: '=?', + isEditing: '=?', + showLabel: '=?', + label: '@?', + placeholder: '@?', + required: '=?', + min: '=?', + max: '=?', + name: '@', + readOnly: '=?', + help: '@?' + }, + controller: [ + 'tubularEditorService', '$scope', '$filter', function (tubularEditorService, $scope, $filter) { + var $ctrl = this; + + $ctrl.validate = function () { + if (angular.isDefined($ctrl.min) && angular.isDefined($ctrl.value) && $ctrl.value != null) { + if ($ctrl.value.length < parseInt($ctrl.min)) { + $ctrl.$valid = false; + $ctrl.state.$errors = [$filter('translate')('EDITOR_MIN_CHARS', +$ctrl.min)]; + return; + } + } - return { - template: '
' + - '{{value}}' + - '' + - '' + - '' + - '{{error}}' + - '' + - '{{help}}' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: tubularEditorService.defaultScope, - controller: [ - '$scope', function($scope) { - $scope.validate = function() { - if (angular.isDefined($scope.min) && angular.isDefined($scope.value) && $scope.value != null) { - if ($scope.value.length < parseInt($scope.min)) { - $scope.$valid = false; - $scope.state.$errors = [$filter('translate')('EDITOR_MIN_CHARS', +$scope.min)]; - return; - } - } + if (angular.isDefined($ctrl.max) && angular.isDefined($ctrl.value) && $ctrl.value != null) { + if ($ctrl.value.length > parseInt($ctrl.max)) { + $ctrl.$valid = false; + $ctrl.state.$errors = [$filter('translate')('EDITOR_MAX_CHARS', +$ctrl.max)]; + return; + } + } + }; - if (angular.isDefined($scope.max) && angular.isDefined($scope.value) && $scope.value != null) { - if ($scope.value.length > parseInt($scope.max)) { - $scope.$valid = false; - $scope.state.$errors = [$filter('translate')('EDITOR_MAX_CHARS', +$scope.max)]; - return; - } - } - }; - tubularEditorService.setupScope($scope); - } - ] - }; - } - ]); -})(); -(function() { + $ctrl.$onInit = function() { + tubularEditorService.setupScope($scope, null, $ctrl, false); + }; + } + ] + }); +})(window.angular); +(function (angular) { 'use strict'; + function setupFilter($scope, $element, $compile, $filter, $ctrl) { + var filterOperators = { + 'string': { + 'None': $filter('translate')('OP_NONE'), + 'Equals': $filter('translate')('OP_EQUALS'), + 'NotEquals': $filter('translate')('OP_NOTEQUALS'), + 'Contains': $filter('translate')('OP_CONTAINS'), + 'NotContains': $filter('translate')('OP_NOTCONTAINS'), + 'StartsWith': $filter('translate')('OP_STARTSWITH'), + 'NotStartsWith': $filter('translate')('OP_NOTSTARTSWITH'), + 'EndsWith': $filter('translate')('OP_ENDSWITH'), + 'NotEndsWith': $filter('translate')('OP_NOTENDSWITH') + }, + 'numeric': { + 'None': $filter('translate')('OP_NONE'), + 'Equals': $filter('translate')('OP_EQUALS'), + 'Between': $filter('translate')('OP_BETWEEN'), + 'Gte': '>=', + 'Gt': '>', + 'Lte': '<=', + 'Lt': '<' + }, + 'date': { + 'None': $filter('translate')('OP_NONE'), + 'Equals': $filter('translate')('OP_EQUALS'), + 'NotEquals': $filter('translate')('OP_NOTEQUALS'), + 'Between': $filter('translate')('OP_BETWEEN'), + 'Gte': '>=', + 'Gt': '>', + 'Lte': '<=', + 'Lt': '<' + }, + 'datetime': { + 'None': $filter('translate')('OP_NONE'), + 'Equals': $filter('translate')('OP_EQUALS'), + 'NotEquals': $filter('translate')('OP_NOTEQUALS'), + 'Between': $filter('translate')('OP_BETWEEN'), + 'Gte': '>=', + 'Gt': '>', + 'Lte': '<=', + 'Lt': '<' + }, + 'datetimeutc': { + 'None': $filter('translate')('OP_NONE'), + 'Equals': $filter('translate')('OP_EQUALS'), + 'NotEquals': $filter('translate')('OP_NOTEQUALS'), + 'Between': $filter('translate')('OP_BETWEEN'), + 'Gte': '>=', + 'Gt': '>', + 'Lte': '<=', + 'Lt': '<' + }, + 'boolean': { + 'None': $filter('translate')('OP_NONE'), + 'Equals': $filter('translate')('OP_EQUALS'), + 'NotEquals': $filter('translate')('OP_NOTEQUALS') + } + }; + + $ctrl.filter = { + Text: $ctrl.text || null, + Argument: $ctrl.argument ? [$ctrl.argument] : null, + Operator: $ctrl.operator || "Contains", + OptionsUrl: $ctrl.optionsUrl || null, + HasFilter: !($ctrl.text == null), + Name: $scope.$parent.$parent.column.Name + }; + + $ctrl.filterTitle = $ctrl.title || $filter('translate')('CAPTION_FILTER'); + + $scope.$watch(function() { + var columns = $ctrl.$component.columns.filter(function($element) { + return $element.Name === $ctrl.filter.Name; + }); + + return columns.length !== 0 ? columns[0] : null; + }, function(val) { + if (val && val != null) { + if ($ctrl.filter.HasFilter != val.Filter.HasFilter) { + $ctrl.filter.HasFilter = val.Filter.HasFilter; + $ctrl.filter.Text = val.Filter.Text; + $ctrl.retrieveData(); + } + } + }, true); + + $ctrl.retrieveData = function() { + var columns = $ctrl.$component.columns.filter(function($element) { + return $element.Name === $ctrl.filter.Name; + }); + + if (columns.length !== 0) { + columns[0].Filter = $ctrl.filter; + } + + $ctrl.$component.retrieveData(); + $ctrl.close(); + }; + + $ctrl.clearFilter = function() { + if ($ctrl.filter.Operator !== 'Multiple') { + $ctrl.filter.Operator = 'None'; + } + + $ctrl.filter.Text = ''; + $ctrl.filter.Argument = []; + $ctrl.filter.HasFilter = false; + $ctrl.retrieveData(); + }; + + $ctrl.applyFilter = function() { + $ctrl.filter.HasFilter = true; + $ctrl.retrieveData(); + }; + + $ctrl.close = function() { + $ctrl.isOpen = false; + }; + + $ctrl.checkEvent = function(keyEvent) { + if (keyEvent.which === 13) { + $ctrl.applyFilter(); + keyEvent.preventDefault(); + } + }; + + var columns = $ctrl.$component.columns.filter(function($element) { + return $element.Name === $ctrl.filter.Name; + }); + + $scope.$watch('$ctrl.filter.Operator', function (val) { + if (val === 'None') $ctrl.filter.Text = ''; + }); + + if (columns.length === 0) return; + + $scope.$watch('$ctrl.filter', function (n) { + if (columns[0].Filter.Text !== n.Text) { + n.Text = columns[0].Filter.Text; + + if (columns[0].Filter.Operator !== n.Operator) { + n.Operator = columns[0].Filter.Operator; + } + } + + $ctrl.filter.HasFilter = columns[0].Filter.HasFilter; + }); + + columns[0].Filter = $ctrl.filter; + $ctrl.dataType = columns[0].DataType; + $ctrl.filterOperators = filterOperators[$ctrl.dataType]; + + if ($ctrl.dataType === 'date' || $ctrl.dataType === 'datetime' || $ctrl.dataType === 'datetimeutc') { + $ctrl.filter.Argument = [new Date()]; + + if ($ctrl.filter.Operator === 'Contains') { + $ctrl.filter.Operator = 'Equals'; + } + } + + if ($ctrl.dataType === 'numeric' || $ctrl.dataType === 'boolean') { + $ctrl.filter.Argument = [1]; + + if ($ctrl.filter.Operator === 'Contains') { + $ctrl.filter.Operator = 'Equals'; + } + } + }; + angular.module('tubular.directives') - /** - * @ngdoc directive + /** + * @ngdoc component * @name tbColumnFilterButtons - * @restrict E + * @module tubular.directives * * @description - * The `tbColumnFilterButtons` is an internal directive, and it is used to show basic filtering buttons. + * The `tbColumnFilterButtons` is an internal component, and it is used to show basic filtering buttons. */ - .directive('tbColumnFilterButtons', [function () { - return { - template: '
' + - '{{\'CAPTION_APPLY\' | translate}} ' + - '' + - '
', - restrict: 'E', - replace: true, - transclude: true - }; - }]) + .component('tbColumnFilterButtons', { + require: { + $columnFilter: '^?tbColumnFilter', + $columnDateTimeFilter: '^?tbColumnDateTimeFilter', + $columnOptionsFilter: '^?tbColumnOptionsFilter' + }, + template: '
' + + '{{\'CAPTION_APPLY\' | translate}} ' + + '' + + '
', + controller: ['$scope', + function ($scope) { + var $ctrl = this; + + $ctrl.$onInit = function() { + // Set currentFilter to either one of the parent components or for when this template is being rendered by $compile + $ctrl.currentFilter = $ctrl.$columnFilter || $ctrl.$columnDateTimeFilter || $ctrl.$columnOptionsFilter || $scope.$parent.$ctrl; + }; + } + ] + }) /** - * @ngdoc directive + * @ngdoc component * @name tbColumnSelector - * @restrict E + * @module tubular.directives * * @description * The `tbColumnSelector` is a button to show columns selector popup. */ - .directive('tbColumnSelector', [function () { - return { - template: '', - restrict: 'E', - replace: true, - transclude: true, - controller: ['$scope', '$uibModal', function ($scope, $modal) { - $scope.$component = $scope.$parent; - - $scope.openColumnsSelector = function () { - var model = $scope.$component.columns; + .component('tbColumnSelector', { + require: { + $component: '^tbGrid' + }, + template: '', + controller: [ + '$scope', '$uibModal', function ($scope, $modal) { + var $ctrl = this; + + $ctrl.openColumnsSelector = function () { + var model = $ctrl.$component.columns; var dialog = $modal.open({ template: '' + '' + '' + '', @@ -2372,12 +2542,13 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) ] }); }; - }] - }; - }]) + } + ] + }) /** * @ngdoc directive * @name tbColumnFilter + * @module tubular.directives * @restrict E * * @description @@ -2385,55 +2556,42 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * * The parent scope will provide information about the data type. * + * @param {string} title Set the popover title. * @param {string} text Set the search text. * @param {string} operator Set the initial operator, default depends on data type. + * @param {object} argument Set the argument. */ - .directive('tbColumnFilter', [ - 'tubularGridFilterService', function(tubularGridFilterService) { + .component('tbColumnFilter', { + require: { + $component: '^tbGrid' + }, + template: '
' + + '' + + '
', + bindings: { + text: '@', + argument: '@', + operator: '@', + title: '@' + }, + controller: [ + '$scope', '$element', '$compile', '$filter', 'tubularTemplateService', function ($scope, $element, $compile, $filter, tubularTemplateService) { + var $ctrl = this; - return { - require: '^tbColumn', - template: '
' + - '' + - '
' + - '' + - '

{{filterTitle}}

' + - '
' + - ' ' + - '' + - '
' + - ' ' + - '
' + - '' + - '
' + - '' + - '
' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: false, - compile: function compile() { - return { - pre: function(scope, lElement, lAttrs) { - tubularGridFilterService.applyFilterFuncs(scope, lElement, lAttrs); - }, - post: function(scope, lElement, lAttrs) { - tubularGridFilterService.createFilterModel(scope, lAttrs); - } - }; - } - }; - } - ]) + $ctrl.$onInit = function () { + $ctrl.templateName = tubularTemplateService.tbColumnFilterPopoverTemplateName; + setupFilter($scope, $element, $compile, $filter, $ctrl); + }; + } + ] + }) /** * @ngdoc directive * @name tbColumnDateTimeFilter + * @module tubular.directives * @restrict E * * @description @@ -2445,172 +2603,124 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) * @param {object} argument Set the search object (if the search is text use text attribute). * @param {string} operator Set the initial operator, default depends on data type. */ - .directive('tbColumnDateTimeFilter', [ - 'tubularGridFilterService', function(tubularGridFilterService) { - - return { - require: '^tbColumn', - template: '
' + - '' + - '
' + - '' + - '

{{filterTitle}}

' + - '
' + - '' + - ' ' + - '' + - '
' + - '' + - '
' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: false, - controller: [ - '$scope', function($scope) { - $scope.filter = {}; - - $scope.format = 'yyyy-MM-dd'; - } - ], - compile: function compile() { - return { - pre: function(scope, lElement, lAttrs) { - tubularGridFilterService.applyFilterFuncs(scope, lElement, lAttrs, function() { - var inp = $(lElement).find("input[type=date]")[0]; - - if (inp.type !== 'date') { - $(inp).datepicker({ - dateFormat: scope.format.toLowerCase() - }).on("dateChange", function(e) { - scope.filter.Text = e.date; - }); - } - - var inpLev = $(lElement).find("input[type=date]")[1]; + .component('tbColumnDateTimeFilter', { + require: { + $component: '^tbGrid' + }, + template: '
' + + '' + + '
', + bindings: { + text: '@', + argument: '@', + operator: '@', + title: '@' + }, + controller: [ + '$scope', '$element', '$compile', '$filter', 'tubularTemplateService', function ($scope, $element, $compile, $filter, tubularTemplateService) { + var $ctrl = this; - if (inpLev.type !== 'date') { - $(inpLev).datepicker({ - dateFormat: scope.format.toLowerCase() - }).on("dateChange", function(e) { - scope.filter.Argument = [e.date]; - }); - } - }); - }, - post: function(scope, lElement, lAttrs) { - tubularGridFilterService.createFilterModel(scope, lAttrs); - } - }; - } - }; - } - ]) + $ctrl.$onInit = function() { + $ctrl.templateName = tubularTemplateService.tbColumnDateTimeFilterPopoverTemplateName; + setupFilter($scope, $element, $compile, $filter, $ctrl); + }; + } + ] + }) /** - * @ngdoc directive + * @ngdoc component * @name tbColumnOptionsFilter + * @module tubular.directives * @restrict E * * @description * The `tbColumnOptionsFilter` directive is a filter with an dropdown listing all the possible values to filter. * - * @scope - * * @param {object} argument Set the search object. * @param {string} operator Set the initial operator, default depends on data type. * @param {string} optionsUrl Set the URL to retrieve options */ - .directive('tbColumnOptionsFilter', [ - 'tubularGridFilterService', function(tubularGridFilterService) { + .component('tbColumnOptionsFilter', { + require: { + $component: '^tbGrid' + }, + template: '
' + + '' + + '
', + bindings: { + text: '@', + argument: '@', + operator: '@', + optionsUrl: '@', + title: '@' + }, + controller: [ + '$scope', '$element', '$compile', '$filter', 'tubularTemplateService', function ($scope, $element, $compile, $filter, tubularTemplateService) { + var $ctrl = this; - return { - require: '^tbColumn', - template: '
' + - '' + - '
' + - '' + - '

{{::filterTitle}}

' + - '
' + - '' + - '
' + - '' + - '
' + - '
', - restrict: 'E', - replace: true, - transclude: true, - scope: false, - controller: [ - '$scope', function($scope) { - $scope.dataIsLoaded = false; - - $scope.getOptionsFromUrl = function () { - if ($scope.dataIsLoaded) { - $scope.$apply(); - return; - } + $ctrl.getOptionsFromUrl = function () { + if ($ctrl.dataIsLoaded) { + $scope.$apply(); + return; + } - var currentRequest = $scope.$component.dataService.retrieveDataAsync({ - serverUrl: $scope.filter.OptionsUrl, - requestMethod: 'GET' - }); + var currentRequest = $ctrl.$component.dataService.retrieveDataAsync({ + serverUrl: $ctrl.filter.OptionsUrl, + requestMethod: 'GET' + }); - currentRequest.promise.then( - function(data) { - $scope.optionsItems = data; - $scope.dataIsLoaded = true; - }, function(error) { - $scope.$emit('tbGrid_OnConnectionError', error); - }); - }; - } - ], - compile: function compile() { - return { - pre: function(scope, lElement, lAttrs) { - tubularGridFilterService.applyFilterFuncs(scope, lElement, lAttrs, function() { - scope.getOptionsFromUrl(); - }); - }, - post: function(scope, lElement, lAttrs) { - tubularGridFilterService.createFilterModel(scope, lAttrs); + currentRequest.promise.then( + function (data) { + $ctrl.optionsItems = data; + $ctrl.dataIsLoaded = true; + }, function (error) { + $scope.$emit('tbGrid_OnConnectionError', error); + }); + }; - scope.filter.Operator = 'Multiple'; - } - }; - } - }; - } - ]); -})(); -(function () { + $ctrl.$onInit = function() { + $ctrl.dataIsLoaded = false; + $ctrl.templateName = tubularTemplateService.tbColumnOptionsFilterPopoverTemplateName; + setupFilter($scope, $element, $compile, $filter, $ctrl); + $ctrl.getOptionsFromUrl(); + + $ctrl.filter.Operator = 'Multiple'; + }; + } + ] + }); +})(window.angular); +(function (angular) { 'use strict'; angular.module('tubular.directives') /** * @ngdoc directive * @name tbForm + * @module tubular.directives * @restrict E * * @description - * The `tbForm` directive is the base to create any form. You can define a `dataService` and a + * The `tbForm` directive is the base to create any form powered by Tubular. Define `dataService` and * `modelKey` to auto-load a record. The `serverSaveUrl` can be used to create a new or update * an existing record. * - * @scope + * Please don't bind a controller directly to the `tbForm`, Angular will throw an exception. If you want + * to extend the form behavior put a controller in a upper node like a div. + * + * The `save` method can be forced to update a model against the REST service, otherwise if the Model + * doesn't detect any change will ignore the save call. * * @param {string} serverUrl Set the HTTP URL where the data comes. * @param {string} serverSaveUrl Set the HTTP URL where the data will be saved. * @param {string} serverSaveMethod Set HTTP Method to save data. * @param {object} model The object model to show in the form. - * @param {boolean} isNew Set if the form is for create a new record. * @param {string} modelKey Defines the fields to use like Keys. * @param {string} formName Defines the form name. * @param {string} serviceName Define Data service (name) to retrieve data, defaults `tubularHttp`. @@ -2633,7 +2743,6 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) serverUrl: '@', serverSaveUrl: '@', serverSaveMethod: '@', - isNew: '@', modelKey: '@?', dataServiceName: '@?serviceName', requireAuthentication: '=?', @@ -2672,7 +2781,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) data[key] = value; }); - $scope.model = new TubularModel($scope, data, $scope.dataService); + $scope.model = new TubularModel($scope, $scope, data, $scope.dataService); $scope.bindFields(); } @@ -2692,7 +2801,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) $scope.modelKey !== '') { $scope.dataService.getByKey($scope.serverUrl, $scope.modelKey).promise.then( function (data) { - $scope.model = new TubularModel($scope, data, $scope.dataService); + $scope.model = new TubularModel($scope, $scope, data, $scope.dataService); $scope.bindFields(); }, function (error) { $scope.$emit('tbForm_OnConnectionError', error); @@ -2708,7 +2817,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) dataService = $scope.model.$component.dataService; } - $scope.model = new TubularModel(innerScope, data, dataService); + $scope.model = new TubularModel(innerScope, innerScope, data, dataService); $scope.bindFields(); $scope.model.$isNew = true; }, function (error) { @@ -2720,18 +2829,18 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) } if (angular.isUndefined($scope.model)) { - $scope.model = new TubularModel($scope, {}, $scope.dataService); + $scope.model = new TubularModel($scope, $scope, {}, $scope.dataService); } $scope.bindFields(); }; - $scope.save = function () { + $scope.save = function (forceUpdate) { if (!$scope.model.$valid()) { return; } - $scope.currentRequest = $scope.model.save(); + $scope.currentRequest = $scope.model.save(forceUpdate); if ($scope.currentRequest === false) { $scope.$emit('tbForm_OnSavingNoChanges', $scope); @@ -2747,6 +2856,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) } $scope.$emit('tbForm_OnSuccessfulSave', data, $scope); + $scope.clear(); }, function (error) { $scope.$emit('tbForm_OnConnectionError', error, $scope); }) @@ -2756,8 +2866,8 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) }); }; - $scope.update = function () { - $scope.save(); + $scope.update = function (forceUpdate) { + $scope.save(forceUpdate); }; $scope.create = function () { @@ -2767,6 +2877,7 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) $scope.cancel = function () { $scope.$emit('tbForm_OnCancel', $scope.model); + $scope.clear(); }; $scope.clear = function () { @@ -2803,150 +2914,128 @@ angular.module('a8m.group-by', ['a8m.filter-watcher']) }; } ]); -})(); -(function() { +})(window.angular); +(function(angular) { 'use strict'; angular.module('tubular.directives') /** - * @ngdoc directive + * @ngdoc component * @name tbTextSearch - * @restrict E + * @module tubular.directives * * @description - * The `tbTextSearch` directive is visual component to enable free-text search in a grid. - * - * @scope + * The `tbTextSearch` is visual component to enable free-text search in a grid. * * @param {number} minChars How many chars before to search, default 3. + * @param {string} placeholder The placeholder text, defaults `UI_SEARCH` i18n resource. */ - .directive('tbTextSearch', [function() { - return { - require: '^tbGrid', - template: - '