diff --git a/dist/mw-uikit.js b/dist/mw-uikit.js new file mode 100644 index 00000000..6d08f879 --- /dev/null +++ b/dist/mw-uikit.js @@ -0,0 +1,7057 @@ +(function (root, angular) { + 'use strict'; + + angular.module('mwUI', [ + 'mwUI.Backbone', + 'mwUI.ExceptionHandler', + 'mwUI.Form', + 'mwUI.Inputs', + 'mwUI.i18n', + 'mwUI.Layout', + 'mwUI.List', + 'mwUI.Menu', + 'mwUI.Modal', + 'mwUI.ResponseHandler', + 'mwUI.Toast', + 'mwUI.ResponseToastHandler', + 'mwUI.Utils', + 'mwUI.UiComponents' + ]) + + .config(['i18nProvider', 'mwIconProvider', function (i18nProvider, mwIconProvider) { + i18nProvider.addLocale('de_DE', 'Deutsch', 'de_DE.json'); + i18nProvider.addLocale('en_US', 'English (US)', 'en_US.json'); + + mwIconProvider.addIconSet({ + id: 'mwUI', + classPrefix: 'fa', + iconsUrl:'uikit/mw_ui_icons.json' + }, true); + + }]) + + .run(['i18n', function(i18n){ + i18n.setLocale('en_US'); + }]); + + //This is only for backwards compatibility and should not be used + window.mCAP = window.mCAP || {}; + + root.mwUI = {}; + + //Will be replaced with the actual version number duringh the build process; + //DO NOT TOUCH + root.mwUI.VERSION = '1.0.12-b478'; + +angular.module("mwUI").run(["$templateCache", function($templateCache) { 'use strict'; + + $templateCache.put('uikit/mw-exception-handler/modals/templates/mw_exception_modal.html', + "

{{'ExceptionHandler.mwExceptionModal.unknownError' | i18n}}

{{exception}}

{{'ExceptionHandler.mwExceptionModal.userMessage' | i18n}}

{{'ExceptionHandler.mwExceptionModal.thanks' | i18n}}
" + ); + + + $templateCache.put('uikit/mw-form/directives/templates/mw_checkbox_wrapper.html', + "
" + ); + + + $templateCache.put('uikit/mw-form/directives/templates/mw_error_messages.html', + "
" + ); + + + $templateCache.put('uikit/mw-form/directives/templates/mw_form_actions.html', + "
" + ); + + + $templateCache.put('uikit/mw-form/directives/templates/mw_input_wrapper.html', + "
0]\" ng-transclude>
0]\">
" + ); + + + $templateCache.put('uikit/mw-inputs/directives/templates/mw_checkbox_group.html', + "
" + ); + + + $templateCache.put('uikit/mw-inputs/directives/templates/mw_radio_group.html', + "
" + ); + + + $templateCache.put('uikit/mw-inputs/directives/templates/mw_select_box.html', + "" + ); + + + $templateCache.put('uikit/mw-inputs/directives/templates/mw_toggle.html', + "
" + ); + + + $templateCache.put('uikit/mw-layout/directives/templates/mw_footer.html', + "" + ); + + + $templateCache.put('uikit/mw-layout/directives/templates/mw_header.html', + "

{{title}}

" + ); + + + $templateCache.put('uikit/mw-layout/directives/templates/mw_sidebar.html', + "
" + ); + + + $templateCache.put('uikit/mw-layout/directives/templates/mw_sub_nav.html', + "
" + ); + + + $templateCache.put('uikit/mw-layout/directives/templates/mw_sub_nav_pill.html', + "
  • " + ); + + + $templateCache.put('uikit/mw-layout/directives/templates/mw_ui.html', + "
    " + ); + + + $templateCache.put('uikit/mw-list/directives/templates/mw_list_body_row_checkbox.html', + " " + ); + + + $templateCache.put('uikit/mw-list/directives/templates/mw_list_footer.html', + "

    {{ 'List.mwListFooter.noneFound' | i18n }}

    " + ); + + + $templateCache.put('uikit/mw-list/directives/templates/mw_list_head.html', + "
    0,'has-search-bar':searchAttribute}\">
    {{'List.mwListHead.selectAll' | i18n }} 0\" class=\"clickable clear\" ng-click=\"selectable.unSelectAll()\"> {{'List.mwListHead.clearSelection' | i18n}}
    0\" class=\"clickable\" ng-click=\"toggleShowSelected()\">{{'List.mwListHead.itemSelected' | i18n:{name: getModelAttribute(selectable.getSelected().first())} }} 1\">{{'List.mwListHead.itemsSelected' | i18n:{name: collectionName, count: selectedAmount} }}
    {{'List.mwListHead.itemAmount' | i18n:{name: collectionName, count: getTotalAmount()} }}
    {{'List.mwListHead.notAvailable' | i18n}} {{getModelAttribute(item)}}
    " + ); + + + $templateCache.put('uikit/mw-list/directives/templates/mw_list_header.html', + " " + ); + + + $templateCache.put('uikit/mw-menu/directives/templates/mw_menu_divider.html', + "
    " + ); + + + $templateCache.put('uikit/mw-menu/directives/templates/mw_menu_entry.html', + "
    " + ); + + + $templateCache.put('uikit/mw-menu/directives/templates/mw_menu_top_bar.html', + "" + ); + + + $templateCache.put('uikit/mw-menu/directives/templates/mw_menu_top_drop_down_item.html', + "
    {{entry.get('label')}}
    " + ); + + + $templateCache.put('uikit/mw-menu/directives/templates/mw_menu_top_entries.html', + "" + ); + + + $templateCache.put('uikit/mw-menu/directives/templates/mw_menu_top_item.html', + "
    {{entry.get('label')}}
    " + ); + + + $templateCache.put('uikit/mw-menu/directives/templates/mw_sidebar_menu.html', + "
    " + ); + + + $templateCache.put('uikit/mw-modal/directives/templates/mw_modal.html', + "

    {{ title }}

    " + ); + + + $templateCache.put('uikit/mw-modal/directives/templates/mw_modal_body.html', + "
    " + ); + + + $templateCache.put('uikit/mw-modal/directives/templates/mw_modal_confirm.html', + "
    " + ); + + + $templateCache.put('uikit/mw-modal/directives/templates/mw_modal_footer.html', + "
    " + ); + + + $templateCache.put('uikit/mw-toast/directives/templates/mw_toasts.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_alert.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_arrow_link.html', + "" + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_badge.html', + "" + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_bread_crumb.html', + "
    {{title}}
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_bread_crumbs_holder.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_collapsible.html', + "
    {{title}}
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_hide_on_request.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_icon.html', + " " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_indefinite_loading.html', + "
    {{'UiComponents.mwIndefiniteLoading.loading' | i18n | uppercase}}
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_option_group.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_panel.html', + "

    {{title}}

    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_spinner.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_star_rating.html', + "" + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_tab_bar.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_tab_pane.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_text_collapsible.html', + "
    {{ text() }} {{ showLessOrMore() | i18n }}
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_timeline.html', + "

    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_timeline_entry.html', + "
  • " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_timeline_fieldset.html', + "
    {{mwTitle}}
    {{ hiddenEntriesText() | i18n:{count:entries.length} }}
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_view_change_loader.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_wizard.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_wizard_navigation.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_wizard_progress.html', + "
    " + ); + + + $templateCache.put('uikit/mw-ui-components/directives/templates/mw_wizard_step.html', + "
    " + ); + + + $templateCache.put('uikit/mw-utils/modals/templates/mw_leave_confirmation_modal.html', + "

    {{ text }}

    " + ); + + + $templateCache.put('uikit/templates/mwSidebarBb/mwSidebarInput.html', + "
    " + ); + + + $templateCache.put('uikit/mw-exception-handler/i18n/de_DE.json', + "{ \"ExceptionHandler\": { \"mwExceptionModal\": { \"title\": \"Es ist etwas schiefgelaufen\", \"unknownError\": \"Leider ist ein unvorhergesehener Fehler aufgetreten. Sie können uns einen Fehlerbericht senden, sodass wir diesen schnellst möglich beseitigen können. Vielen Dank.\", \"userMessage\": \"Sie können uns zusätzlich ihre letzten Schritte beschreiben, sodass wir den Fehler schneller nachstellen können.\", \"userMessagePlaceholder\": \"(Optional)\", \"report\": \"Fehler melden\", \"thanks\": \"Vielen Dank für Ihre Rückmeldung. Wir werden uns umgehend um diesen Fehler kümmern.\" } } }" + ); + + + $templateCache.put('uikit/mw-exception-handler/i18n/en_US.json', + "{ \"ExceptionHandler\": { \"mwExceptionModal\": { \"title\": \"Something went wrong\", \"unknownError\": \"Unfortunatly something went wrong. You can report this error so we can fix it. Thank you.\", \"userMessage\": \"You can leave some additional information to make it easier for us to reproduce the error\", \"userMessagePlaceholder\": \"(Optional)\", \"report\": \"Report error\", \"thanks\": \"Thanks for your feedback. We will have a look at this error as soon as possible.\" } } }" + ); + + + $templateCache.put('uikit/mw-form/i18n/de_DE.json', + "{ \"mwForm\": { \"leaveConfirmation\": \"Ihre Änderungen wurden noch nicht gespeichert. Wenn Sie diese Seite verlassen gehen diese verloren!\", \"formActions\": { \"save\": \"Speichern\", \"cancel\": \"Abbrechen\" } }, \"mwErrorMessages\": { \"required\": \"ist ein Pflichtfeld\", \"hasToBeValidEmail\": \"muss eine valide E-Mail Adresse sein\", \"hasToMatchPattern\": \"muss dem Muster entsprechen\", \"hasToBeValidUrl\": \"muss eine valide URL sein\", \"hasToBeValidPhoneNumber\": \"muss eine gültige Telefonnummer sein\", \"hasToBeMin\": \"muss mindestens {{min}} sein\", \"hasToBeMinLength\": \"muss mindestens {{ngMinlength}} Zeichen haben\", \"hasToBeSmaller\": \"darf maximal {{max}} sein\", \"hasToBeSmallerLength\": \"darf maximal {{ngMaxlength}} Zeichen haben\" } }" + ); + + + $templateCache.put('uikit/mw-form/i18n/en_US.json', + "{ \"mwForm\": { \"leaveConfirmation\": \"Your changes haven't been saved yet. If you leave this page all changes will be discarded!\", \"formActions\": { \"save\": \"Save\", \"cancel\": \"Cancel\" } }, \"mwErrorMessages\": { \"required\": \"is required\", \"hasToBeValidEmail\": \"has to be a valid e-mail\", \"hasToMatchPattern\": \"has to match the pattern\", \"hasToBeValidUrl\": \"has to be a valid URL\", \"hasToBeValidPhoneNumber\": \"has to be a valid phone number\", \"hasToBeMin\": \"has to be at least {{min}}\", \"hasToBeMinLength\": \"has to have a least {{ngMinlength}} chars\", \"hasToBeSmaller\": \"must not be greater than {{max}}\", \"hasToBeSmallerLength\": \"must not have more chars than {{ngMaxlength}}\" } }" + ); + + + $templateCache.put('uikit/mw-inputs/i18n/de_DE.json', + "{ \"mwSelectBox\": { \"pleaseSelect\": \"Option auswählen\" } }" + ); + + + $templateCache.put('uikit/mw-inputs/i18n/en_US.json', + "{ \"mwSelectBox\": { \"pleaseSelect\": \"Select an option\" } }" + ); + + + $templateCache.put('uikit/mw-list/i18n/de_DE.json', + "{ \"List\": { \"mwListHead\": { \"items\": \"Einträge\", \"selectAll\": \"Alle selektieren\", \"clearSelection\": \"Selektion aufheben\", \"itemSelected\": \"{{name}} ist selektiert\", \"itemsSelected\": \"{{count}} {{name}} sind selektiert\", \"itemAmount\": \"{{count}} {{name}}\", \"searchFor\": \"{{name}} suchen\", \"notAvailable\": \"N/V\", \"notAvailableTooltip\": \"Der Eintrag ist nicht verfügbar. Eventuell wurde dieser gelöscht.\" }, \"mwListFooter\": { \"noneFound\": \"Es wurden keine Einträge gefunden\" } } }" + ); + + + $templateCache.put('uikit/mw-list/i18n/en_US.json', + "{ \"List\": { \"mwListHead\": { \"items\": \"Items\", \"selectAll\": \"Select all\", \"clearSelection\": \"Clear selection\", \"itemSelected\": \"{{name}} is selected\", \"itemsSelected\": \"{{count}} {{name}} are selected\", \"itemAmount\": \"{{count}} {{name}}\", \"searchFor\": \"Search for {{name}}\", \"notAvailable\": \"N/V\", \"notAvailableTooltip\": \"The entry is not available anymore. Maybe is has been deleted.\" }, \"mwListFooter\": { \"noneFound\": \"No entries have been found.\" } } }" + ); + + + $templateCache.put('uikit/mw-modal/i18n/de_DE.json', + "{ \"Modal\": { \"mwModalConfirm\": { \"areYouSure\": \"Sind Sie sich sicher?\" } } }" + ); + + + $templateCache.put('uikit/mw-modal/i18n/en_US.json', + "{ \"Modal\": { \"mwModalConfirm\": { \"areYouSure\": \"Are you sure?\" } } }" + ); + + + $templateCache.put('uikit/mw-ui-components/i18n/de_DE.json', + "{ \"UiComponents\": { \"mwToggle\": { \"on\": \"An\", \"off\": \"Aus\" }, \"mwTimelineFieldset\": { \"entriesHiddenSingular\": \"1 Eintrag ist ausgeblendet\", \"entriesHiddenPlural\": \"{{count}} Einträge sind ausgeblendet\" }, \"mwTextCollapsible\": { \"showMore\": \"mehr anzeigen\", \"showLess\": \"weniger anzeigen\" }, \"mwButtonHelp\": { \"isDisabledBecause\": \"Dieser Button ist deaktiviert weil:\" }, \"mwIndefiniteLoading\": { \"loading\": \"Lade Daten...\" } } }" + ); + + + $templateCache.put('uikit/mw-ui-components/i18n/en_US.json', + "{ \"UiComponents\": { \"mwToggle\": { \"on\": \"On\", \"off\": \"Off\" }, \"mwTimelineFieldset\": { \"entriesHiddenSingular\": \"One entry is hidden\", \"entriesHiddenPlural\": \"{{count}} entries are hidden\" }, \"mwTextCollapsable\": { \"showMore\": \"show more\", \"showLess\": \"show less\" }, \"mwButtonHelp\": { \"isDisabledBecause\": \"This button is currently disabled because:\" }, \"mwIndefiniteLoading\": { \"loading\": \"Loading data...\" } } }" + ); + + + $templateCache.put('uikit/mw-utils/i18n/de_DE.json', + "{ \"Utils\": { \"ok\": \"Ok\", \"cancel\": \"Abbrechen\", \"mwLeaveConfirmationModal\": { \"title\": \"Möchten Sie wirklich die aktuelle Seite verlassen?\", \"continue\": \"Fortfahren\", \"stay\": \"Auf Seite bleiben\" } } }" + ); + + + $templateCache.put('uikit/mw-utils/i18n/en_US.json', + "{ \"Utils\": { \"ok\": \"Ok\", \"cancel\": \"Cancel\", \"mwLeaveConfirmationModal\": { \"title\": \"Do you really want to leave the current page?\", \"continue\": \"Continue\", \"stay\": \"Stay on this page\" } } }" + ); + + + $templateCache.put('uikit/mw_ui_icons.json', + "{ \"check\": \"fa-check\", \"angleLeft\": \"fa-angle-left\", \"angleRight\": \"fa-angle-right\", \"angleUp\": \"fa-angle-up\", \"angleDown\": \"fa-angle-down\", \"caretRight\": \"fa-caret-right\", \"sort\": \"fa-sort\", \"sortAsc\": \"fa-sort-asc\", \"sortDesc\": \"fa-sort-desc\", \"warning\": \"fa-warning\", \"cross\": \"fa-times\", \"chevronUpCircle\": \"fa-chevron-circle-up\", \"chevronDownCircle\": \"fa-chevron-circle-down\", \"question\": \"fa-question\", \"questionCircle\": \"fa-question-circle-o\" }" + ); +}]); + +/** + * Created by zarges on 17/02/16. + */ +angular.module('mwUI.Utils', ['mwUI.i18n','mwUI.Modal']); + +window.mwUI.Utils = {}; +window.mwUI.Utils.shims = {}; + +angular.module('mwUI.Utils') + + .directive('mwAppendRouteClass', function () { + return { + link: function (scope, el) { + var orgClasses = el.attr('class'); + var removeClassesFromPreviousRoute = function () { + el.attr('class', orgClasses); + }; + + scope.$on('$routeChangeSuccess', function (event, current) { + removeClassesFromPreviousRoute(); + if (current && current.cssClasses) { + el.addClass(current.cssClasses); + } + }); + } + }; + }); +angular.module('mwUI.Utils') + + .directive('mwDraggable', ['$timeout', function ($timeout) { + return { + restrict: 'A', + scope: { + mwDragData: '=', + //We can not use camelcase because *-start is a reserved word from angular! + mwDragstart: '&', + mwDragend: '&', + mwDropEffect: '@' + }, + link: function (scope, el) { + + el.attr('draggable', true); + el.addClass('draggable', true); + + if (scope.mwDragstart) { + el.on('dragstart', function (event) { + event.originalEvent.dataTransfer.setData('text', JSON.stringify(scope.mwDragData)); + event.originalEvent.dataTransfer.effectAllowed = scope.mwDropEffect; + $timeout(function () { + scope.mwDragstart({event: event, dragData: scope.mwDragData}); + }); + }); + } + + + el.on('dragend', function (event) { + if (scope.mwDragend) { + $timeout(function () { + scope.mwDragend({event: event}); + }); + } + }); + } + }; + }]); +angular.module('mwUI.Utils') + + .directive('mwDroppable', ['$timeout', function ($timeout) { + return { + restrict: 'A', + scope: { + mwDropData: '=', + mwDragenter: '&', + mwDragleave: '&', + mwDragover: '&', + mwDrop: '&', + disableDrop: '=' + }, + link: function (scope, el) { + + el.addClass('droppable'); + + var getDragData = function (event) { + var text = event.originalEvent.dataTransfer.getData('text'); + if (text) { + return JSON.parse(text); + } + }; + + if (scope.mwDragenter) { + el.on('dragenter', function (event) { + if (scope.disableDrop !== true) { + el.addClass('drag-over'); + } + $timeout(function () { + scope.mwDragenter({event: event}); + }); + }); + } + + if (scope.mwDragleave) { + el.on('dragleave', function (event) { + el.removeClass('drag-over'); + $timeout(function () { + scope.mwDragleave({event: event}); + }); + }); + } + + if (scope.mwDrop) { + el.on('drop', function (event) { + el.removeClass('drag-over'); + if (event.stopPropagation) { + event.stopPropagation(); // stops the browser executing other event listeners which are maybe deined in parent elements. + } + var data = getDragData(event); + $timeout(function () { + scope.mwDrop({ + event: event, + dragData: data, + dropData: scope.mwDropData + }); + }); + return false; + }); + } + + // Necessary. Allows us to drop. + var handleDragOver = function (ev) { + if (scope.disableDrop !== true) { + if (ev.preventDefault) { + ev.preventDefault(); + } + return false; + } + }; + el.on('dragover', handleDragOver); + + if (scope.mwDragover) { + el.on('dragover', function (event) { + $timeout(function () { + scope.mwDragover({event: event}); + }); + }); + } + + scope.$on('$destroy', function () { + el.off(); + }); + } + }; + }]); +angular.module('mwUI.Utils') + + .directive('mwInfiniteScroll', ['$window', function ($window) { + return { + restrict: 'A', + link: function (scope, el, attrs) { + + var collection, + loading = false, + throttledScrollFn, + scrollContainerEl, + scrollContentEl, + loadedPages = 0; + + if (attrs.mwListCollection) { + collection = scope.$eval(attrs.mwListCollection).getCollection(); + } else if (attrs.collection) { + collection = scope.$eval(attrs.collection); + } else { + console.warn('No collection was found for the infinite scroll pleas pass it as scope attribute'); + } + + if (!collection || (collection && !collection.filterable)) { + return; + } + + var loadNextPage = function () { + if (!loading && collection.filterable.hasNextPage()) { + loading = true; + loadedPages++; + return collection.filterable.loadNextPage().then(function () { + loading = false; + }); + } + }; + + // The threshold is controlled by how many pages are loaded + // The first pagination request should be done quite early so the user does not recognize that something + // is loaded. + // As scrollbar is getting longer and longer the threshold has to be also increased. + // Threshold starts at 40% and is increased by 10% until the max threshold of 90% is reached + var getLoadThreshold = function(){ + var minThreshold = 4, + maxThreshold = 9; + + return Math.min(minThreshold+loadedPages,maxThreshold)/10; + }; + + var scrollFn = function () { + var contentHeight = scrollContentEl[0].clientHeight || scrollContentEl.height(), + totalHeight = contentHeight - scrollContainerEl.height(), + threshold = getLoadThreshold(); + + if ( scrollContainerEl.scrollTop() / totalHeight > threshold) { + loadNextPage(); + } + }; + + if(attrs.scrollContainerSelector || attrs.scrollContentSelector){ + // Custom element defined by selectors. The scrollContainer is the element that has the scrollbar + // The scrollContent is the element inside the scroll container that should be scrolled + // At least one of them has to be defined and if you define both they must not be the same + if(attrs.scrollContainerSelector && el.parents(attrs.scrollContainerSelector).length > 0){ + scrollContainerEl = el.parents(attrs.scrollContainerSelector).first(); + } else if(attrs.scrollContainerSelector && el.parents(attrs.scrollContainerSelector).length===0){ + throw new Error ('No parent of the infinite scroll element with the selector '+attrs.scrollContainerSelector+' could be found!'); + } else { + scrollContainerEl = el; + } + + if(attrs.scrollContentSelector && el.find(attrs.scrollContentSelector).length > 0){ + scrollContentEl = el.find(attrs.scrollContentSelector).first(); + } else if(attrs.scrollContentSelector && el.find(attrs.scrollContentSelector).length === 0){ + throw new Error ('No child of the infinite scroll element with the selector '+attrs.scrollContentSelector+' could be found!'); + } else { + scrollContentEl = el; + } + } else if (el.parents('.modal').length) { + //element in modal + scrollContainerEl = el.parents('*[mw-modal-body]'); + scrollContentEl = el.parents('.modal-body'); + } else { + //element in window + scrollContainerEl = angular.element($window); + scrollContentEl = angular.element(document); + } + + if(scrollContainerEl === scrollContentEl){ + throw new Error('The scrollContainerElement can not be the same as the actual scrollContentElement'); + } + + throttledScrollFn = _.throttle(scrollFn, 500); + + // Register scroll callback + scrollContainerEl.on('scroll', throttledScrollFn); + + // Deregister scroll callback if scope is destroyed + scope.$on('$destroy', function () { + scrollContainerEl.off('scroll', throttledScrollFn); + }); + } + }; + }]); +angular.module('mwUI.Utils') + + .directive('mwLeaveConfirmation', ['$window', '$rootScope', 'LeaveConfirmationModal', function ($window, $rootScope, LeaveConfirmationModal) { + return { + scope: { + alertBeforeLeave: '=mwLeaveConfirmation', + text: '@' + }, + link: function (scope) { + + var confirmationModal = new LeaveConfirmationModal(); + + // Prevent the original event so the routing will not be completed + // Save the url where it should be navigated to in a temp variable + var showConfirmModal = function (nextUrl) { + confirmationModal.setScopeAttributes({ + nextUrl: nextUrl, + text: scope.text, + leaveCallback: function () { + scope.changeLocationOff(); + }, + stayCallback: function () { + + } + }); + confirmationModal.show(); + }; + + //In case that just a hashchange event was triggered + scope.changeLocationOff = $rootScope.$on('$locationChangeStart', function (ev, nextUrl) { + if (scope.alertBeforeLeave) { + ev.preventDefault(); + showConfirmModal(nextUrl); + } + }); + + //In case that the user clicks the refresh/back button or makes a hard url change + $window.onbeforeunload = function () { + if (scope.alertBeforeLeave) { + return scope.text; + } + }; + + if (!angular.isDefined(scope.text)) { + throw new Error('Please specify a text in the text attribute'); + } + + scope.$on('$destroy', scope.changeLocationOff); + } + }; + }]); + +angular.module('mwUI.Utils') + + .directive('mwPreventDefault', function () { + return { + restrict: 'A', + link: function (scope, elm, attr) { + if (!attr.mwPreventDefault) { + throw new Error('Directive mwPreventDefault: This directive must have an event name as attribute e.g. mw-prevent-default="click"'); + } + elm.on(attr.mwPreventDefault, function (event) { + event.preventDefault(); + }); + } + }; + }); +angular.module('mwUI.Utils') + + .directive('mwStopPropagation', function () { + return { + restrict: 'A', + link: function (scope, elm, attr) { + if (!attr.mwStopPropagation) { + throw new Error('Directive mwStopPropagation: This directive must have an event name as attribute e.g. mw-stop-propagation="keyup"'); + } + elm.on(attr.mwStopPropagation, function (event) { + event.stopPropagation(); + }); + } + }; + }); + +angular.module('mwUI.Utils') + + .filter('reduceStringTo', function () { + return function (input, count) { + if(count && input && input.length > count) { + return input.substr(0, count) + '...'; + } + return input; + }; + }); + +angular.module('mwUI.Utils') + + .factory('LeaveConfirmationModal', ['Modal', function (Modal) { + return Modal.prepare({ + templateUrl: 'uikit/mw-utils/modals/templates/mw_leave_confirmation_modal.html', + controller: 'LeaveConfirmationModalController' + }); + }]) + + .controller('LeaveConfirmationModalController', ['$scope', function ($scope) { + $scope.stay = function () { + $scope.stayCallback(); + $scope.hideModal(); + }; + + // User really wants to navigate to that page which was saved before in a temp variable + $scope.continue = function () { + if ($scope.nextUrl) { + //hide the modal and navigate to the page + $scope.leaveCallback(); + $scope.hideModal().then(function () { + document.location.href=$scope.nextUrl; + }); + } else { + throw new Error('NextUrl has to be set!'); + } + }; + }]); + +'use strict'; + +angular.module('mwUI.Utils') + + .provider('BrowserTitleHandler', function () { + + var _keepOriginalTitle = true, + _currentPagetitle = '', + _originalTitle = null; + + var _setOriginalTitle = function () { + var titleEl = angular.element('head title'); + if (titleEl && !_originalTitle) { + _originalTitle = titleEl.text(); + } + }; + + this.setNewTitle = function (title) { + var titleEl = angular.element('head title'); + if (titleEl) { + titleEl.text(title); + } + if (!_originalTitle) { + _originalTitle = title; + } + }; + + this.setKeepOriginalTitle = function (keepTitle) { + _keepOriginalTitle = keepTitle; + }; + + var provider = this; + + this.$get = function () { + return { + getOriginalTitle: function () { + if (!_originalTitle) { + _setOriginalTitle(); + } + return _originalTitle; + }, + setTitle: function (title) { + _currentPagetitle = title; + provider.setNewTitle(this.getTitle()); + }, + getTitle: function () { + if (_currentPagetitle) { + if (_keepOriginalTitle) { + return this.getOriginalTitle() + '—' + _currentPagetitle; + } else { + return _currentPagetitle; + } + } else { + return this.getOriginalTitle(); + } + } + }; + }; + }); + + +angular.module('mwUI.Utils') + + .service('callbackHandler', ['$injector', function($injector){ + return { + execFn: function(cb, params, scope){ + if(params && angular.isArray(params)){ + return cb.apply(scope, params); + } else { + return cb.call(scope, params); + } + }, + getFn: function(cb){ + if(angular.isString(cb)){ + return $injector.get(cb); + } else if(angular.isFunction(cb)){ + return cb; + } else { + throw new Error('First argument has to be either a valid service or function'); + } + }, + exec: function(cb, params, scope){ + return this.execFn(this.getFn(cb), params, scope); + } + }; + }]); + +var deepExtendObject = function (target, source) { + for (var key in source) { + if (key in target && _.isObject(target[key]) && _.isObject(source[key])) { + deepExtendObject(target[key], source[key]); + } else if (_.isObject(target[key]) && !_.isObject(source[key])) { + throw new Error('Target[' + key + '] is an object but source[' + key + '] is from type '+typeof source[key]+'! You can not overwrite an object with type '+typeof source[key]); + } else { + target[key] = source[key]; + } + } + return target; +}; + +window.mwUI.Utils.shims.deepExtendObject = deepExtendObject; +'use strict'; +window.mwUI.Utils.shims.domObserver = function (el, callback, config) { + + var observer = new MutationObserver(function (mutations) { + callback.call(this, mutations); + }), + node = (el instanceof angular.element) ? el[0] : el; + + config = config || { + attributes: true, + childList: true, characterData: true + }; + + observer.observe(node, config); + + return observer; +}; +/** + * Created by zarges on 17/02/16. + */ +window.mwUI.Utils.shims.routeToRegExp = function(route){ + var optionalParam = /\((.*?)\)/g, + namedParam = /(\(\?)?:\w+/g, + splatParam = /\*\w?/g, + escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + if(!_.isString(route)){ + throw new Error('The route ' + JSON.stringify(route) + 'has to be a URL'); + } + + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function (match, optional) { + return optional ? match : '([^/?]+)'; + }) + .replace(splatParam, '([^?]*?)'); + return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); +}; +var shownDeprecationWarnings = []; + +var deprecationWarning = function(message){ + if(shownDeprecationWarnings.indexOf(message) === -1){ + console.warn(message); + shownDeprecationWarnings.push(message); + } +}; + +window.mwUI.Utils.shims.deprecationWarning = deprecationWarning; + +angular.module('mwUI.Utils').config(['i18nProvider', function(i18nProvider){ + i18nProvider.addResource('mw-utils/i18n', 'uikit'); +}]); + +window.mwUI.Backbone = { + hostName: '', + basePath: '', + Selectable: {}, + Utils: {}, + use$http: true +}; + +angular.module('mwUI.Backbone', []); + +mwUI.Backbone.Utils.concatUrlParts = function () { + var urlParts = _.toArray(arguments), cleanedUrlParts = []; + + //remove empty strings + urlParts = _.compact(urlParts); + + _.each(urlParts, function (url, index) { + if (index === 0) { + //remove only trailing slash + url = url.replace(/\/$/g, ''); + } else { + //Removing leading and trailing slash + url = url.replace(/^\/|\/$/g, ''); + } + cleanedUrlParts.push(url); + }); + + return cleanedUrlParts.join('/'); +}; +mwUI.Backbone.Utils.getUrl = function(instance){ + var hostName, basePath, endpoint; + + if(instance instanceof mwUI.Backbone.Model || instance instanceof mwUI.Backbone.Collection){ + hostName = _.result(instance, 'hostName') || ''; + basePath = _.result(instance, 'basePath') || ''; + endpoint = _.result(instance, 'endpoint'); + } else { + throw new Error('An instance of a collection or a model has to be passed as argument to the function'); + } + + if (!endpoint || endpoint.length === 0) { + throw new Error('An endpoint has to be specified'); + } + + return window.mwUI.Backbone.Utils.concatUrlParts(hostName, basePath, endpoint); +}; +mwUI.Backbone.Utils.request = function (url, method, options, instance) { + options = options || {}; + var requestOptions = { + url: url, + type: method + }, hostName; + + if (instance) { + requestOptions.instance = instance; + } + + if (url && !url.match(/\/\//)) { + if (instance instanceof mwUI.Backbone.Model || instance instanceof mwUI.Backbone.Collection) { + hostName = _.result(instance, 'hostName'); + } else { + hostName = mwUI.Backbone.hostName || ''; + } + requestOptions.url = mwUI.Backbone.Utils.concatUrlParts(hostName, url); + } + + return Backbone.ajax(_.extend(requestOptions, options)); +}; + +mwUI.Backbone.NestedModel = Backbone.NestedModel = Backbone.Model.extend({ + + nested: function () { + return {}; + }, + + _prepare: function () { + var nestedAttributes = this.nested(), + instanceObject = {}; + for (var key in nestedAttributes) { + if (typeof nestedAttributes[key] === 'function') { + var instance = new nestedAttributes[key](); + + instance.parent = this; + instanceObject[key] = instance; + } else { + throw new Error('Nested attribute ' + key + ' is not a valid constructor. Do not set an instance as nested attribute.'); + } + } + + return instanceObject; + }, + + _setNestedModel: function (key, value) { + if (_.isObject(value)) { + this.get(key).set(value); + } else { + var id = this.get(key).idAttribute; + this.get(key).set(id, value); + } + }, + + _setNestedCollection: function (key, value) { + if (_.isObject(value) && !_.isArray(value)) { + this.get(key).add(value); + } else if (_.isArray(value)) { + value.forEach(function (val) { + this._setNestedCollection(key, val); + }.bind(this)); + } else { + var id = this.get(key).model.prototype.idAttribute, + obj = {}; + + obj[id] = value; + this.get(key).add(obj); + } + }, + + _setNestedAttributes: function (obj) { + + for (var key in obj) { + var nestedAttrs = this.nested(), + value = obj[key], + nestedValue = nestedAttrs[key]; + + if (nestedValue && !(value instanceof nestedValue) && this.get(key)) { + + if (this.get(key) instanceof Backbone.Model) { + this._setNestedModel(key, value); + } else if (this.get(key) instanceof Backbone.Collection) { + this._setNestedCollection(key, value); + } + + delete obj[key]; + } + } + + return obj; + }, + + _nestedModelToJson: function (model) { + var result; + + if (model instanceof Backbone.NestedModel) { + result = model._prepareDataForServer(); + } else { + result = model.toJSON(); + } + + return result; + }, + + _prepareDataForServer: function () { + var attrs = _.extend({}, this.attributes), + nestedAttrs = this.nested(); + + for (var key in nestedAttrs) { + var nestedAttr = this.get(key); + + if (nestedAttr instanceof Backbone.Model) { + attrs[key] = this._nestedModelToJson(nestedAttr); + } else if (nestedAttr instanceof Backbone.Collection) { + var result = []; + + nestedAttr.each(function (model) { + result.push(this._nestedModelToJson(model)); + }.bind(this)); + + attrs[key] = result; + } + } + + return this.compose(attrs); + }, + + constructor: function (attributes, options) { + options = options || {}; + if (options.parse) { + attributes = this.parse(attributes); + options.parse = false; + } + this.attributes = this._prepare(); + this.set(attributes); + attributes = this.attributes; + return Backbone.Model.prototype.constructor.call(this, attributes, options); + }, + + set: function (attributes, options) { + var obj = {}; + + if (_.isString(attributes)) { + obj[attributes] = options; + } else if (_.isObject(attributes)) { + obj = attributes; + } + + if(!_.isObject(options)){ + options = null; + } + + obj = this._setNestedAttributes(obj); + + return Backbone.Model.prototype.set.call(this, obj, options); + }, + + compose: function (attrs) { + return attrs; + }, + + toJSON: function (options) { + // When options are set toJSON is called from the sync method so it is called before the object is send to the server + // We use this to transform our data before we are sending it to the server + // It is the counterpart of parse for the server + if (options) { + return this._prepareDataForServer(); + } else { + return Backbone.Model.prototype.toJSON.apply(this, arguments); + } + }, + + clear: function (options) { + var attrs = {}; + + for (var key in this.attributes){ + if(this.get(key) instanceof Backbone.Model){ + this.get(key).clear(); + } else if(this.get(key) instanceof Backbone.Collection){ + this.get(key).reset(); + } else { + attrs[key] = void 0; + } + } + + return this.set(attrs, _.extend({}, options, {unset: true})); + } +}); + + +/*jshint unused:false */ +mwUI.Backbone.Selectable.Model = function (modelInstance, options) { + + var _model = modelInstance, + _selected = options.selected || false; + + this.isInCollection = false; + + this.hasDisabledFn = (typeof options.isDisabled === 'function') || false; + + this.isDisabled = function () { + if (this.hasDisabledFn) { + return options.isDisabled.apply(modelInstance, arguments); + } + return false; + }; + + this.isSelected = function () { + return _selected; + }; + + this.select = function (options) { + options = options || {}; + if ( (!this.isDisabled() || options.force) && !this.isSelected()) { + _selected = true; + if(!options.silent){ + this.trigger('change change:select',modelInstance,this); + } + } + }; + + this.unSelect = function (options) { + options = options || {}; + if(this.isSelected()){ + _selected = false; + if(!options.silent){ + this.trigger('change change:unselect',modelInstance,this); + } + } + }; + + this.toggleSelect = function () { + if (this.isSelected()) { + this.unSelect(); + } else { + this.select(); + } + }; + + var main = function(){ + if (!(_model instanceof Backbone.Model)) { + throw new Error('First parameter has to be the instance of a model'); + } + }; + + main.call(this); +}; + +_.extend(mwUI.Backbone.Selectable.Model.prototype, Backbone.Events); +mwUI.Backbone.SelectableModel = Backbone.SelectableModel = Backbone.Model.extend({ + selectable: true, + selectableOptions: function(){ + return { + selected: false, + isDisabled: null + }; + }, + selectableModelConstructor: function(options){ + if (this.selectable) { + this.selectable = new mwUI.Backbone.Selectable.Model(this, this.selectableOptions.call(this, options)); + } + this.on('destroy', function(){ + //Decrement counter of parent collection when model is destroyed + if (this.collection && this.collection.filterable && this.collection.filterable.getTotalAmount() > 0) { + this.collection.filterable.setTotalAmount(this.collection.filterable.getTotalAmount() - 1); + } + }); + }, + constructor: function (attributes, options) { + var superConstructor = Backbone.Model.prototype.constructor.call(this, attributes, options); + this.selectableModelConstructor(options); + return superConstructor; + } + +}); +mwUI.Backbone.Model = mwUI.Backbone.NestedModel.extend({ + selectable: true, + hostName: function(){ + return mwUI.Backbone.hostName; + }, + basePath: function(){ + return mwUI.Backbone.basePath; + }, + endpoint: null, + selectableOptions: mwUI.Backbone.SelectableModel.prototype.selectableOptions, + urlRoot: function () { + return mwUI.Backbone.Utils.getUrl(this); + }, + constructor: function () { + var superConstructor = mwUI.Backbone.NestedModel.prototype.constructor.apply(this, arguments); + mwUI.Backbone.SelectableModel.prototype.selectableModelConstructor.apply(this, arguments); + return superConstructor; + }, + getEndpoint: function () { + return this.urlRoot(); + }, + setEndpoint: function (endpoint) { + this.endpoint = endpoint; + }, + sync: function (method, model, options) { + options.instance = this; + return mwUI.Backbone.NestedModel.prototype.sync.call(this, method, model, options); + }, + request: function (url, method, options) { + return mwUI.Backbone.Utils.request(url, method, options, this); + } +}); + + +mwUI.Backbone.Filter = function () { + // If it is an invalid value return null otherwise the provided object + var returnNullOrObjectFor = function (value, object) { + return (_.isUndefined(value) || value === null || value === '' || value.length===0 || (_.isArray(value) && _.compact(value).length===0)) ? null : object; + }; + + var returnNullOrObjectForMultipleValues = function (values, object) { + var hasValue = false; + if(!_.isObject(values)){ + console.log(values); + throw new Error('The argument values has to be an object'); + } + for(var key in values){ + if(returnNullOrObjectFor(values[key], true)){ + hasValue = true; + } else { + delete object[key]; + } + } + return hasValue ? object : null; + }; + + return { + containsString: function (fieldName, value) { + return returnNullOrObjectFor(value, { + type: 'containsString', + fieldName: fieldName, + contains: value + }); + }, + + string: function (fieldName, value) { + return returnNullOrObjectFor(value, { + type: 'string', + fieldName: fieldName, + value: value + }); + }, + + and: function (filters) { + return this.logOp(filters, 'AND'); + }, + + nand: function (filters) { + return this.logOp(filters, 'NAND'); + }, + + or: function (filters) { + return this.logOp(filters, 'OR'); + }, + + logOp: function (filters, operator) { + filters = _.without(filters, null); // Removing null values from existing filters + + return filters.length === 0 ? null : { // Ignore logOps with empty filters + type: 'logOp', + operation: operator, + filters: filters + }; + }, + + boolean: function (fieldName, value) { + return returnNullOrObjectFor(value, { + type: 'boolean', + fieldName: fieldName, + value: value + }); + }, + + stringMap: function (fieldName, key, value) { + if(value === '%%'){ + value = ''; + } + return returnNullOrObjectFor(value, { + type: 'stringMap', + fieldName: fieldName, + value: value, + key: key + }); + }, + + stringEnum: function (fieldName, values) { + return returnNullOrObjectFor(values, { + type: 'stringEnum', + fieldName: fieldName, + values: _.flatten(values) + }); + }, + + long: function (fieldName, value) { + return returnNullOrObjectFor(value, { + type: 'long', + fieldName: fieldName, + value: value + }); + }, + + like: function (fieldName, value) { + return returnNullOrObjectFor(value, { + type: 'like', + fieldName: fieldName, + like: value + }); + }, + + notNull: function (fieldName) { + return returnNullOrObjectFor(true, { + type: 'null', + fieldName: fieldName + }); + }, + + dateRange: function(fieldName, min, max){ + min = min ? +new Date(min) : null; + max = max ? +new Date(max) : null; + return returnNullOrObjectForMultipleValues({min: min, max: max}, { + type: 'dateRange', + fieldName: fieldName, + min: min, + max: max + }); + }, + + longRange: function(fieldName, min, max){ + return returnNullOrObjectForMultipleValues({min: min, max: max}, { + type: 'longRange', + fieldName: fieldName, + min: min, + max: max + }); + } + }; + +}; + +/*jshint unused:false */ +mwUI.Backbone.Filterable = function (collectionInstance, options) { + + options = options || {}; + + var _collection = collectionInstance, + _limit = options.limit, + _offset = _limit ? options.offset : false, + _page = options.page || 1, + _perPage = options.perPage || 30, + _customUrlParams = options.customUrlParams || {}, + _initialFilterValues = options.filterValues || {}, + _filterDefinition = options.filterDefinition, + _sortOrder = options.sortOrder, + _totalAmount, + _lastFilter; + + var _getClone = function (obj) { + return JSON.parse(JSON.stringify(obj)); + }; + + this.filterValues = {}; + this.customUrlParams = {}; + this.fields = options.fields; + this.filterIsSet = false; + + this.hasFilterChanged = function (filter) { + return JSON.stringify(filter) !== JSON.stringify(_lastFilter); + }; + + this.getRequestParams = function (options) { + options = options || {}; + options.params = options.params || {}; + // Filter functionality + var filter = this.getFilters(); + if (filter) { + options.params.filter = filter; + } + + //reset pagination if filter values change + if (this.hasFilterChanged(filter)) { + _page = 1; + } + + // Pagination functionality + if (_perPage && _page && (_limit || _.isUndefined(_limit))) { + options.params.limit = _perPage; + + // Calculate offset + options.params.offset = _page > 1 ? _perPage * (_page - 1) : 0; + } + + // Sort order + if (_sortOrder && _sortOrder.length > 0) { + options.params.sortOrder = _sortOrder; + } + + // Fallback to limit and offset if they're set manually, overwrites pagination settings + if (_limit || _offset) { + options.params.limit = _limit; + options.params.offset = _offset; + } + + if (_limit === false) { + delete options.params.limit; + } + + if (this.fields && this.fields.length > 0) { + options.params.field = this.fields; + } + + // Custom URL parameters + if (this.customUrlParams) { + _.extend(options.params, _.result(this, 'customUrlParams')); + } + + //always set non paged parameter + options.params.getNonpagedCount = true; + + _lastFilter = filter; + + return options.params; + }; + + this.getInitialFilterValues = function () { + return _initialFilterValues; + }; + + this.setInitialFilterValues = function (filterValues) { + for (var key in filterValues) { + // Make sure to overwrite the current filter value when it is an initial filter value + if (this.filterValues[key] === _initialFilterValues[key]) { + this.filterValues[key] = filterValues[key]; + } + } + _.extend(_initialFilterValues, filterValues); + // when a filter is set it should use this value otherwise it should use the initial value so + // all properties of initial filter values that also exist in the current filter values will be overwritten + this.filterValues = _.extend({}, _initialFilterValues, this.filterValues); + }; + + this.setLimit = function (limit) { + _limit = limit; + _offset = _offset || 0; + }; + + this.setTotalAmount = function (totalAmount) { + _totalAmount = totalAmount; + }; + + this.getTotalAmount = function () { + return _totalAmount; + }; + + this.loadPreviousPage = function () { + _page -= 1; + return _collection.fetch({remove: false}); + }; + + this.hasPreviousPage = function () { + return _page >= 1; + }; + + this.loadNextPage = function () { + _page += 1; + return _collection.fetch({remove: false}); + }; + + this.hasNextPage = function () { + return _totalAmount && _totalAmount > _collection.length; + }; + + this.getPage = function () { + return _page; + }; + + this.setPage = function (page) { + _page = page; + }; + + this.getTotalPages = function () { + return Math.floor(_totalAmount / _perPage); + }; + + this.setSortOrder = function (sortOrder) { + _page = 1; + _sortOrder = sortOrder; + collectionInstance.trigger('change:sortOrder', sortOrder); + }; + + this.getSortOrder = function () { + return _sortOrder; + }; + + this.setFilters = function (filterMap, options) { + options = options || {}; + + _.forEach(filterMap, function (value, key) { + if (_.has(this.filterValues, key)) { + this.filterValues[key] = value; + var filterValue = {}; + filterValue[key] = value; + if(_.isUndefined(options.silent) || !options.silent){ + collectionInstance.trigger('change:filterValue', filterValue); + } + } else { + throw new Error('Filter named \'' + key + '\' not found, did you add it to filterValues of the model?'); + } + }, this); + + this.setPage(1); + this.filterIsSet = true; + }; + + this.getFilters = function () { + if (_.isFunction(_filterDefinition)) { + return _filterDefinition.apply(this); + } + }; + + this.resetFilters = function () { + this.filterValues = _getClone(_initialFilterValues); + this.customUrlParams = _customUrlParams; + this.filterIsSet = false; + }; + + (function _main() { + if (!(_collection instanceof Backbone.Collection)) { + throw new Error('First parameter has to be the instance of a collection'); + } + + if (options.filterValues) { + _initialFilterValues = _getClone(options.filterValues); + } + + this.resetFilters(); + }.bind(this)()); +}; +mwUI.Backbone.FilterableCollection = Backbone.FilterableCollection = Backbone.Collection.extend({ + selectable: true, + filterableOptions: function () { + return { + limit: undefined, + offset: false, + page: 1, + perPage: 30, + filterValues: {}, + customUrlParams: {}, + filterDefinition: function () {}, + fields: [], + sortOrder: '' + }; + }, + filterableCollectionConstructor: function (options) { + if (this.filterable) { + this.filterable = new mwUI.Backbone.Filterable(this, this.filterableOptions.call(this, options)); + } + }, + constructor: function (attributes, options) { + var superConstructor = Backbone.Collection.prototype.constructor.call(this, attributes, options); + this.filterableCollectionConstructor(options); + return superConstructor; + }, + fetch: function (options) { + options = options || {}; + + if (this.filterable) { + var filterableParams = this.filterable.getRequestParams(options); + + if(window.mwUI.Backbone.use$http){ + //$http is using options.params to generate GET query params + options.params = options.params || {}; + _.extend(options.params, filterableParams); + } else { + //jquery ajax does not have a params a params option attribute for query params + options.data = options.data || {}; + _.extend(options.data, filterableParams); + } + } + + return Backbone.Collection.prototype.fetch.call(this, options); + } +}); +mwUI.Backbone.Selectable.Collection = function (collectionInstance, options) { + var _collection = collectionInstance, + _options = options || {}, + _modelHasDisabledFn = true, + _isSingleSelection = _options.isSingleSelection || false, + _addPreSelectedToCollection = _options.addPreSelectedToCollection || false, + _unSelectOnRemove = _options.unSelectOnRemove, + _preSelected = options.preSelected, + _hasPreSelectedItems = !!options.preSelected, + _selected = new (mwUI.Backbone.Collection.extend({ + selectable: false, + filterable: false + }))(); + + var _preselect = function () { + if (_preSelected instanceof Backbone.Model) { + _isSingleSelection = true; + this.preSelectModel(_preSelected); + } else if (_preSelected instanceof Backbone.Collection) { + _isSingleSelection = false; + this.preSelectCollection(_preSelected); + } else { + throw new Error('The option preSelected has to be either a Backbone Model or Collection'); + } + }; + + var _selectWhenModelIsSelected = function (model) { + if (!_selected.get(model)) { + this.select(model); + } + }; + + var _unSelectWhenModelIsUnSelected = function (model) { + if (_selected.get(model)) { + this.unSelect(model); + } + }; + + var _unSelectWhenModelIsUnset = function (model, opts) { + opts = opts || {}; + if (opts.unset || !model.id || model.id.length < 1) { + this.unSelect(model); + } + }; + + var _bindModelOnSelectListener = function (model) { + model.selectable.off('change:select', _selectWhenModelIsSelected, this); + model.selectable.on('change:select', _selectWhenModelIsSelected, this); + }; + + var _bindModelOnUnSelectListener = function (model) { + model.selectable.off('change:unselect', _unSelectWhenModelIsUnSelected, this); + model.selectable.on('change:unselect', _unSelectWhenModelIsUnSelected, this); + }; + + var _setModelSelectableOptions = function (model, options) { + if (model && model.selectable) { + var selectedModel = _selected.get(model); + + if (selectedModel) { + if (_collection.get(model)) { + model.selectable.isInCollection = true; + selectedModel.selectable.isInCollection = true; + } else { + model.selectable.isInCollection = false; + selectedModel.selectable.isInCollection = false; + } + model.selectable.select(options); + selectedModel.selectable.select(options); + } else { + model.selectable.unSelect(options); + } + + _bindModelOnSelectListener.call(this, model); + _bindModelOnUnSelectListener.call(this, model); + } + }; + + var _updatePreSelectedModel = function (preSelectedModel, model) { + if (_hasPreSelectedItems) { + if (this.isSingleSelection()) { + _preSelected = model; + } else { + _preSelected.remove(preSelectedModel, {silent: true}); + _preSelected.add(model, {silent: true}); + } + } + }; + + var _updateSelectedModel = function (model) { + var selectedModel = this.getSelected().get(model); + if (selectedModel) { + this.unSelect(selectedModel, {silent: true}); + this.select(model, {silent: true}); + _updatePreSelectedModel.call(this, selectedModel, model); + _setModelSelectableOptions.call(this, selectedModel, {silent: true}); + } + }; + + this.getSelected = function () { + return _selected; + }; + + this.getDisabled = function () { + var disabled = new Backbone.Collection(); + if (_modelHasDisabledFn) { + _collection.each(function (model) { + if (model.selectable && model.selectable.isDisabled()) { + disabled.add(model); + } + }); + } + + return disabled; + }; + + /** + * + * @param model + */ + this.select = function (model, options) { + options = options || {}; + if (model instanceof Backbone.Model) { + if (!(model instanceof _collection.model)) { + model = new _collection.model(model.toJSON()); + } + + if (!model.selectable || (model.selectable.isDisabled() && !options.force)) { + return; + } + + if (_isSingleSelection) { + this.unSelectAll(); + } + + if (_collection.get(model)) { + model = _collection.get(model); + } + + model.on('change', _unSelectWhenModelIsUnset, this); + + _selected.add(model, options); + _setModelSelectableOptions.call(this, model, options); + if (!options.silent) { + this.trigger('change change:add', model, this); + } + } else { + throw new Error('The first argument has to be a Backbone Model'); + } + }; + + this.selectAll = function () { + _collection.each(function (model) { + this.select(model); + }, this); + }; + + this.unSelect = function (model, options) { + options = options || {}; + model.off('change', _unSelectWhenModelIsUnset, this); + _selected.remove(model, options); + _setModelSelectableOptions.call(this, model, options); + if (!options.silent) { + this.trigger('change change:remove', model, this); + } + }; + + this.unSelectAll = function () { + this.getSelected().secureEach(function(model){ + this.unSelect(model); + }, this); + }; + + this.toggleSelectAll = function () { + if (this.allSelected()) { + this.unSelectAll(); + } else { + this.selectAll(); + } + }; + + this.allSelected = function () { + var disabledModelsAmount = this.getDisabled().length; + + return this.getSelected().length === _collection.length - disabledModelsAmount; + }; + + this.allDisabled = function () { + return this.getDisabled().length === _collection.length; + }; + + this.isSingleSelection = function () { + return _isSingleSelection; + }; + + this.reset = function () { + this.unSelectAll(); + _preselect.call(this); + }; + + this.preSelectModel = function (model) { + if (model.id) { + + _hasPreSelectedItems = true; + + if (!_collection.get(model) && _addPreSelectedToCollection) { + _collection.add(model); + } else if (_collection.get(model)) { + model = _collection.get(model); + } + + this.select(model, {force: true, silent: true}); + } + }; + + this.preSelectCollection = function (collection) { + collection.each(function (model) { + this.preSelectModel(model); + }, this); + + collection.on('add', function (model) { + this.preSelectModel(model); + }, this); + + collection.on('remove', function (model) { + this.unSelect(model); + }, this); + + }; + + this.setCollectionFromSelection = function (collection) { + var selected = this.getSelected(); + if (collection instanceof mwUI.Backbone.Collection) { + collection.replace(selected.toJSON()); + } else { + throw new Error('[Selectable] The passed collection is not an instance of mwUI.Backbone.Collection'); + } + return collection; + }; + + this.setModelFromSelection = function (model) { + var selected = this.getSelected(); + if (model instanceof Backbone.Model) { + if (selected.length === 0) { + model.clear(); + } else { + model.set(selected.first().toJSON()); + } + } else { + throw new Error('[Selectable] The passed model is not an instance of Backbone.Model'); + } + return model; + }; + + this.useSelectionFor = function (modelOrCollection) { + if (modelOrCollection instanceof Backbone.Model) { + return this.setModelFromSelection(modelOrCollection); + } else if(modelOrCollection instanceof Backbone.Collection){ + return this.setCollectionFromSelection(modelOrCollection); + } + }; + + var main = function () { + if (!(_collection instanceof Backbone.Collection)) { + throw new Error('The first parameter has to be from type Backbone.Collection'); + } + + _collection.on('add', function (model) { + _modelHasDisabledFn = model.selectable.hasDisabledFn; + _setModelSelectableOptions.call(this, model); + _updateSelectedModel.call(this, model); + }, this); + + _collection.on('remove', function (model) { + if (_unSelectOnRemove) { + this.unSelect(model); + } else { + _setModelSelectableOptions.call(this, model); + } + }, this); + + _collection.on('reset', function () { + if (_unSelectOnRemove) { + this.unSelectAll(); + } else { + this.getSelected().each(function (model) { + _setModelSelectableOptions.call(this, model); + }, this); + } + }, this); + + if (_hasPreSelectedItems) { + _preselect.call(this); + } + }; + + main.call(this); + +}; + +_.extend(mwUI.Backbone.Selectable.Collection.prototype, Backbone.Events); +mwUI.Backbone.SelectableCollection = Backbone.SelectableCollection = Backbone.Collection.extend({ + selectable: true, + selectableOptions: function(){ + return { + isSingleSelection: false, + addPreSelectedToCollection: false, + unSelectOnRemove: false, + preSelected: new Backbone.Collection() + }; + }, + selectableCollectionConstructor: function(options){ + if (this.selectable) { + this.selectable = new mwUI.Backbone.Selectable.Collection(this, this.selectableOptions.call(this,options)); + } + }, + constructor: function (attributes, options) { + var superConstructor = Backbone.Collection.prototype.constructor.call(this, attributes, options); + this.selectableCollectionConstructor(options); + return superConstructor; + } +}); +mwUI.Backbone.Collection = Backbone.Collection.extend({ + selectable: true, + filterable: true, + hostName: function () { + return mwUI.Backbone.hostName; + }, + basePath: function () { + return mwUI.Backbone.basePath; + }, + endpoint: null, + selectableOptions: mwUI.Backbone.SelectableCollection.prototype.selectableOptions, + filterableOptions: mwUI.Backbone.FilterableCollection.prototype.filterableOptions, + model: mwUI.Backbone.Model, + secureEach: function (callback, ctx) { + // This method can be used when items are removed from the collection during the each loop + // When doing this in the normal each method you will get referencing issues—in java terms you + // would get a ConcurrentModificationException + _.pluck(this.models, 'cid').forEach(function (cid, index) { + var model = this.get(cid, index); + callback.call(ctx, model, index, this.models); + }.bind(this)); + }, + url: function () { + return window.mwUI.Backbone.Utils.getUrl(this); + }, + getEndpoint: function () { + return this.url(); + }, + setEndpoint: function (endpoint) { + this.endpoint = endpoint; + }, + replace: function (models) { + this.reset(models); + this.trigger('replace', this); + }, + constructor: function () { + var superConstructor = Backbone.Collection.prototype.constructor.apply(this, arguments); + mwUI.Backbone.SelectableCollection.prototype.selectableCollectionConstructor.apply(this, arguments); + mwUI.Backbone.FilterableCollection.prototype.filterableCollectionConstructor.apply(this, arguments); + return superConstructor; + }, + fetch: function () { + return mwUI.Backbone.FilterableCollection.prototype.fetch.apply(this, arguments); + }, + request: function (url, method, options) { + return window.mwUI.Backbone.Utils.request(url, method, options, this); + } +}); + +angular.module('mwUI.Backbone') + + .directive('mwCollection', function () { + return { + require: ['?ngModel', '?^form'], + link: function (scope, el, attrs, ctrls) { + var collection, ngModelCtrl, formCtrl; + + if(ctrls.length>0){ + ngModelCtrl = ctrls[0]; + } + if(ctrls.length>1){ + formCtrl = ctrls[1]; + } + + var updateNgModel = function () { + if(collection.length>0){ + ngModelCtrl.$setViewValue(collection); + ngModelCtrl.$render(); + } else { + ngModelCtrl.$setViewValue(null); + ngModelCtrl.$render(); + } + }; + + + var init = function () { + collection = scope.$eval(attrs.mwCollection); + + if (collection) { + updateNgModel(); + collection.on('add remove reset', updateNgModel); + ngModelCtrl.$setPristine(); + if(formCtrl){ + formCtrl.$setPristine(ngModelCtrl); + } + } + }; + + if (ngModelCtrl) { + if (scope.mwModel) { + init(); + } else { + var off = scope.$watch('mwCollection', function () { + off(); + init(); + }); + } + } + } + }; + }); +angular.module('mwUI.Backbone') + + .directive('mwModel', function () { + return { + require: ['?ngModel', '?^form'], + link: function (scope, el, attrs, ctrls) { + var model, modelAttr, ngModelCtrl, formCtrl; + + if(ctrls.length>0){ + ngModelCtrl = ctrls[0]; + } + if(ctrls.length>1){ + formCtrl = ctrls[1]; + } + + var updateNgModel = function () { + var val = model.get(modelAttr); + + ngModelCtrl.$formatters.forEach(function (formatFn) { + val = formatFn(val); + }); + + ngModelCtrl.$setViewValue(val); + ngModelCtrl.$render(); + }; + + var updateBackboneModel = function () { + var obj = {}; + + obj[modelAttr] = ngModelCtrl.$modelValue; + model.set(obj, {fromNgModel: true}); + }; + + var getModelAttrName = function () { + var mwModelAttrOption = attrs.mwModelAttr, + mwModelAttrFromNgModel = attrs.ngModel; + + if (mwModelAttrOption && mwModelAttrOption.length > 0) { + return mwModelAttrOption; + } else if (angular.isUndefined(mwModelAttrOption) && mwModelAttrFromNgModel) { + return mwModelAttrFromNgModel.split('.').pop(); + } + }; + + var init = function () { + model = scope.$eval(attrs.mwModel); + modelAttr = getModelAttrName(); + + if (ngModelCtrl && model && modelAttr) { + + model.on('change:' + modelAttr, function (model, val, options) { + if (!options.fromNgModel) { + updateNgModel(); + } + }); + + ngModelCtrl.$viewChangeListeners.push(updateBackboneModel); + ngModelCtrl.$parsers.push(function (val) { + updateBackboneModel(); + return val; + }); + + if (model.get(modelAttr) && ngModelCtrl.$modelValue && model.get(modelAttr) !== ngModelCtrl.$modelValue) { + throw new Error('The ng-model and the backbone model can not have different values during initialization!'); + } else if (model.get(modelAttr)) { + updateNgModel(); + ngModelCtrl.$setPristine(); + if(formCtrl){ + formCtrl.$setPristine(ngModelCtrl); + } + } else if (ngModelCtrl.$modelValue) { + updateBackboneModel(); + } + } + }; + + if (scope.mwModel && getModelAttrName()) { + init(); + } else { + var offModel = scope.$watch('mwModel', function () { + offModel(); + init(); + }); + + var offModelAttr = scope.$watch('mwModelAttr', function () { + offModelAttr(); + init(); + }); + } + } + }; + }); + +var _backboneAjax = Backbone.ajax, + _backboneSync = Backbone.sync, + _$http, + _$q; + +angular.module('mwUI.Backbone') + + .run(['$http', '$q', function ($http, $q) { + _$http = $http; + _$q = $q; + }]); + +Backbone.ajax = function (options) { + if (mwUI.Backbone.use$http && _$http) { + // Set HTTP Verb as 'method' + options.method = options.type; + // Use angulars $http implementation for requests + return _$http.apply(angular, arguments).then(function(resp){ + if (options.success && typeof options.success === 'function') { + options.success(resp); + } + return resp; + }, function(resp){ + if (options.error && typeof options.error === 'function') { + options.error(resp); + } + return _$q.reject(resp); + }); + } else { + return _backboneAjax.apply(this, arguments); + } +}; + +Backbone.sync = function (method, model, options) { + // we have to set the flag to wait true otherwise all cases were you want to delete mutliple entries will break + // https://github.com/jashkenas/backbone/issues/3534 + // This flag means that the server has to confirm the creation/deletion before the model will be added/removed to the + // collection + options = options || {}; + if (_.isUndefined(options.wait)) { + options.wait = true; + } + // Instead of the response object we are returning the backbone model in the promise + return _backboneSync.call(Backbone, method, model, options).then(function () { + return model; + }); +}; +angular.module('mwUI.ExceptionHandler', ['mwUI.Modal', 'mwUI.i18n', 'mwUI.UiComponents', 'mwUI.Utils']); + +angular.module('mwUI.ExceptionHandler') + + .factory('ExceptionModal', ['Modal', function (Modal) { + return Modal.prepare({ + templateUrl: 'uikit/mw-exception-handler/modals/templates/mw_exception_modal.html', + controller: 'ExceptionModalController' + }); + }]) + + .controller('ExceptionModalController', ['$scope', '$q', 'Wizard', function ($scope, $q, Wizard) { + $scope.exception = null; + + $scope.wizard = Wizard.createWizard('exception'); + + $scope.report = function(){ + $q.when($scope.successCallback()).then(function(){ + $scope.wizard.next(); + }); + }; + + $scope.cancel = function(){ + if($scope.errorCallback){ + $q.when($scope.errorCallback()).then(function(){ + $scope.hideModal(); + }); + } else { + $scope.hideModal(); + } + }; + + $scope.close = function(){ + $scope.hideModal(); + }; + }]); + +angular.module('mwUI.ExceptionHandler') + + .provider('$exceptionHandler', function () { + + var _handlers = []; + + return { + + registerHandler: function(callback){ + _handlers.push(callback); + }, + + $get: ['callbackHandler', '$log', function (callbackHandler, $log) { + return function (exception,cause) { + exception = exception || ''; + cause = cause || ''; + + try{ + _handlers.forEach(function(callback){ + callbackHandler.exec(callback, [exception.toString(), cause.toString()]); + }); + } catch (err){ + $log.error(err); + } + + $log.error(exception); + }; + }] + }; + + }); +angular.module('mwUI.ExceptionHandler') + + .provider('exceptionHandlerModal', function () { + + var showExceptionModal = true, + options = { + displayException: false, + userCanEnterMessage: false, + successCallback: null, + errorCallback: null + }; + + return { + + disableExceptionModal: function () { + showExceptionModal = false; + }, + + setModalOptions: function (opts) { + _.extend(options, opts); + }, + + $get: ['callbackHandler', 'ExceptionModal', function (callbackHandler, ExceptionModal) { + + var exceptionModal = new ExceptionModal(); + + var hideNgView = function () { + var ngView = angular.element.find('div[ng-view]'); + + if (ngView) { + angular.element(ngView).hide(); + } + }; + + return function (exception, cause) { + if (showExceptionModal) { + + if (options.successCallback) { + var succCb = options.successCallback; + options.successCallback = function () { + return callbackHandler.exec(succCb, [exception.toString(), cause.toString()]); + }; + } + + if (options.errorCallback) { + var errCb = options.errorCallback; + options.errorCallback = function () { + callbackHandler.exec(errCb, [exception.toString(), cause.toString()]); + }; + } + + exceptionModal.setScopeAttributes(_.extend({}, options, { + exception: exception.toString(), + cause: cause + })); + + hideNgView(); + exceptionModal.show(); + } + }; + }] + }; + + }); + +angular.module('mwUI.ExceptionHandler').config(['$exceptionHandlerProvider', 'i18nProvider', function ($exceptionHandlerProvider, i18nProvider) { + $exceptionHandlerProvider.registerHandler('exceptionHandlerModal'); + i18nProvider.addResource('mw-exception-handler/i18n', 'uikit'); +}]); +angular.module('mwUI.Form', ['mwUI.i18n','mwUI.Modal','mwUI.Utils']); + +angular.module('mwUI.Form') + + .directive('mwFormActions', ['$route', function ($route) { + return { + scope: { + save: '&', + cancel: '&', + showSave: '=', + showCancel: '=' + }, + templateUrl: 'uikit/mw-form/directives/templates/mw_form_actions.html', + link: function (scope, elm, attr) { + scope.form = elm.inheritedData('$formController'); + scope.viewModel = { + isLoading: false, + hasSave: angular.isDefined(attr.save), + hasCancel: angular.isDefined(attr.cancel), + showSave: true, + showCancel: true + }; + + scope.$watch('showSave', function (val) { + if (angular.isDefined(val)) { + scope.viewModel.showSave = val; + } + }); + + scope.$watch('showCancel', function (val) { + if (angular.isDefined(val)) { + scope.viewModel.showCancel = val; + } + }); + + var showLoadingSpinner = function () { + scope.viewModel.isLoading = true; + }; + + var hideLoadingSpinner = function () { + scope.viewModel.isLoading = false; + }; + + var setFormPristineAndEvaluate = function (exec) { + if (scope.form) { + scope.form.$setPristine(); + } + var execFn = scope.$eval(exec); + if (execFn && execFn.then) { + showLoadingSpinner(); + execFn.then(hideLoadingSpinner, hideLoadingSpinner); + } + }; + + scope.saveFacade = function () { + setFormPristineAndEvaluate(scope.save); + }; + + scope.cancelFacade = function () { + if (!angular.isDefined(attr.cancel)) { + setFormPristineAndEvaluate(function () { + return $route.reload(); + }); + } else { + setFormPristineAndEvaluate(scope.cancel); + } + }; + } + }; + }]); +angular.module('mwUI.Form') + + .directive('mwErrorMessages', ['mwValidationMessages', function (mwValidationMessages) { + return { + require: '^ngModelErrors', + templateUrl: 'uikit/mw-form/directives/templates/mw_error_messages.html', + link: function(scope, el, attrs, ngModelErrorsCtrl){ + scope.errors = ngModelErrorsCtrl.getErrors; + + scope.getMessageForError = function(errorModel){ + return mwValidationMessages.getMessageFor(errorModel.get('error'),errorModel.get('attrs')); + }; + } + }; + }]); +var extendForm = function () { + return { + restrict: 'E', + link: function (scope, el) { + el.addClass('form-horizontal'); + el.attr('novalidate', 'true'); + } + }; +}; + +angular.module('mwUI.Form') + + .directive('form', extendForm); +angular.module('mwUI.Form') + + .directive('mwFormLeaveConfirmation', ['$compile', function ($compile) { + return { + require: '^form', + link: function (scope, elm, attr, formCtrl) { + + var confirmation = $compile('' + + '
    ' + + '
    ')(scope), + isActive = true; + + scope.showConfirmation = function () { + return formCtrl.$dirty && isActive; + }; + + elm.on('submit', function () { + isActive = false; + }); + + elm.on('input', function () { + isActive = true; + }); + + elm.append(confirmation); + + scope.$on('$destroy', function () { + isActive = false; + }); + } + }; + }]); +angular.module('mwUI.Form') + + .directive('mwCheckboxWrapper', function () { + return { + transclude: true, + scope: { + label: '@', + tooltip: '@' + }, + templateUrl: 'uikit/mw-form/directives/templates/mw_checkbox_wrapper.html' + }; + }); +angular.module('mwUI.Form') + + .directive('mwInputWrapper', function () { + return { + transclude: true, + scope: { + label: '@', + tooltip: '@', + hideErrors: '=' + }, + templateUrl: 'uikit/mw-form/directives/templates/mw_input_wrapper.html', + controller: function () { + var modelState = { + dirty: true, + valid: true, + touched: false + }, + inputState = { + required: false, + focused: false + }, + inputType = 'text'; + + var setObj = function (obj, val) { + if (angular.isObject(val)) { + _.extend(obj, val); + } else { + throw new Error('State has to be an object'); + } + }; + + // Will be called by ngModel modification in mw-form/directives/ng-model + this.setModelState = function (newState) { + setObj(modelState, newState); + }; + + this.getModelState = function () { + return modelState; + }; + + // Will be called by ngModel modification in mw-form/directives/ng-model + this.setInputState = function (newState) { + setObj(inputState, newState); + }; + + this.getInputState = function () { + return inputState; + }; + + // Will be called by mwInputDefaults in mw-inputs/directives + this.setType = function(type){ + inputType = type; + }; + + this.getType = function(){ + return inputType; + }; + }, + link: function (scope, el, attrs, ctrl) { + + scope.isValid = function () { + return ctrl.getModelState().valid; + }; + + scope.isDirty = function () { + return ctrl.getModelState().dirty; + }; + + scope.isTouched = function () { + return ctrl.getModelState().touched; + }; + + scope.isRequired = function () { + return ctrl.getInputState().required; + }; + + scope.isFocused = function () { + return ctrl.getInputState().focused; + }; + + scope.hasError = function () { + return !scope.hideErrors && !scope.isValid() && scope.isDirty(); + }; + + scope.hasRequiredError = function () { + return scope.isRequired() && !scope.isValid(); + }; + + scope.getType = ctrl.getType; + } + }; + }); +angular.module('mwUI.Form') + + .directive('ngModel', function () { + return { + require: ['ngModel', '?^ngModelErrors', '?^mwInputWrapper'], + link: function (scope, el, attrs, ctrls) { + var ngModelCtrl = ctrls[0], + ngModelErrorsCtrl = ctrls[1], + mwInputWrapperCtrl = ctrls[2], + inputId = _.uniqueId('input_el'); + + var setErrors = function (newErrorObj, oldErrorObj) { + var newErrors = _.keys(newErrorObj), + oldErrors = _.keys(oldErrorObj), + removeErrors = _.difference(oldErrors, newErrors); + + ngModelErrorsCtrl.addErrorsForInput(newErrors, inputId, _.clone(attrs)); + ngModelErrorsCtrl.removeErrorsForInput(removeErrors, inputId, _.clone(attrs)); + }; + + var setModelState = function () { + mwInputWrapperCtrl.setModelState({ + dirty: ngModelCtrl.$dirty, + valid: ngModelCtrl.$valid, + touched: ngModelCtrl.$touched + }); + }; + + var initErrorState = function () { + scope.$watch(function () { + return ngModelCtrl.$error; + }, function (newErrorObj, oldErrorObj) { + setErrors(newErrorObj, oldErrorObj); + }, true); + }; + + var initModelAndInputState = function () { + scope.$watch(function () { + return ngModelCtrl.$error; + }, setModelState, true); + + scope.$watch(function () { + return ngModelCtrl.$touched; + }, setModelState); + + attrs.$observe('required', function(){ + mwInputWrapperCtrl.setInputState({ + required: angular.isDefined(el.attr('required')) + }); + }); + + el.on('focus blur', function(ev){ + mwInputWrapperCtrl.setInputState({ + focused: ev.type === 'focus' + }); + }); + scope.$on('$destroy', el.off.bind(el)); + }; + + if (ngModelErrorsCtrl) { + initErrorState(); + } + + if(mwInputWrapperCtrl){ + initModelAndInputState(); + } + } + }; + + }); +angular.module('mwUI.Form') + + .directive('ngModelErrors', function () { + return { + scope: true, + controller: function () { + var ErrorModel = mwUI.Backbone.Model.extend({ + idAttribute: 'error', + nested: function () { + return { + inputIds: Backbone.Collection + }; + } + }), + Errors = Backbone.Collection.extend({ + model: ErrorModel + }), + allErrors = new Errors(); + + var addErrorForInput = function (error, inputId, attrs) { + var alreadyExistingError = allErrors.get(error); + + if (alreadyExistingError) { + var inputIds = alreadyExistingError.get('inputIds'); + + _.extend(alreadyExistingError.get('attrs'),attrs); + inputIds.add({id: inputId}); + } else { + allErrors.add({error: error, inputIds: [inputId], attrs: attrs}); + } + }; + + var removeErrorForInput = function (error, inputId) { + var existingError = allErrors.get(error); + + if(existingError){ + var inputIdsInError = existingError.get('inputIds'), + inputIdModel = inputIdsInError.get(inputId); + + if (inputIdModel) { + inputIdsInError.remove(inputIdModel); + + if (inputIdsInError.length === 0) { + allErrors.remove(existingError); + } + } + } + }; + + this.addErrorsForInput = function (errors, inputId, attrs) { + errors.forEach(function(error){ + addErrorForInput(error, inputId, attrs); + }); + }; + + this.removeErrorsForInput = function (errors, inputId) { + errors.forEach(function(error){ + removeErrorForInput(error, inputId); + }); + }; + + this.getErrors = function(){ + return allErrors; + }; + + } + }; + }); + +angular.module('mwUI.Form') + + .provider('mwValidationMessages', function () { + var _stringValidators = {}, + _functionValidators = {}, + _executedValidators = {}; + + var _setValidationMessage = function (key, validationMessage) { + if (typeof validationMessage === 'function') { + _functionValidators[key] = validationMessage; + } else if (typeof validationMessage === 'string') { + _stringValidators[key] = validationMessage; + } else if (validationMessage) { + throw new Error('The validation has to be either a string or a function. String can be also a reference to i18n'); + } + }; + + this.registerValidator = function (key, validationMessage) { + if (!_stringValidators[key] && !_functionValidators[key]) { + _setValidationMessage(key, validationMessage); + } else { + throw new Error('The key ' + key + ' has already been registered'); + } + }; + + this.$get = ['$rootScope', 'i18n', function ($rootScope, i18n) { + var getTranslatedValidator = function (key, options) { + var message = _stringValidators[key]; + + if (i18n.translationIsAvailable(message)) { + return i18n.get(message, options); + } else { + return message; + } + }; + + var getExecutedValidator = function (key, options) { + return _functionValidators[key](i18n, options); + }; + + return { + getRegisteredValidators: function () { + return _.extend(_stringValidators, _executedValidators); + }, + getMessageFor: function (key, options) { + if (_functionValidators[key]) { + return getExecutedValidator(key, options); + } else if (_stringValidators[key]) { + return getTranslatedValidator(key, options); + } + }, + updateMessage: function (key, message) { + if (_stringValidators[key] || _functionValidators[key]) { + _setValidationMessage(key, message); + } else { + throw new Error('The key ' + key + ' is not available. You have to register it first via the provider'); + } + } + }; + }]; + }); + +angular.module('mwUI.Form') + + .config(['i18nProvider', 'mwValidationMessagesProvider', function(i18nProvider, mwValidationMessagesProvider){ + i18nProvider.addResource('mw-form/i18n', 'uikit'); + + mwValidationMessagesProvider.registerValidator('required','mwErrorMessages.required'); + mwValidationMessagesProvider.registerValidator('email','mwErrorMessages.hasToBeValidEmail'); + mwValidationMessagesProvider.registerValidator('pattern','mwErrorMessages.hasToMatchPattern'); + mwValidationMessagesProvider.registerValidator('url','mwErrorMessages.hasToBeValidUrl'); + mwValidationMessagesProvider.registerValidator('phone','mwErrorMessages.hasToBeValidPhoneNumber'); + mwValidationMessagesProvider.registerValidator('min','mwErrorMessages.hasToBeMin'); + mwValidationMessagesProvider.registerValidator('minlength','mwErrorMessages.hasToBeMinLength'); + mwValidationMessagesProvider.registerValidator('max','mwErrorMessages.hasToBeSmaller'); + mwValidationMessagesProvider.registerValidator('maxlength','mwErrorMessages.hasToBeSmallerLength'); + }]); +angular.module('mwUI.i18n', [ + 'mwUI.Backbone' +]); + +angular.module('mwUI.i18n') + + .provider('i18n', function () { + + var _resources = [], + _locales = [], + _dictionary = {}, + _isLoadingresources = false, + _oldLocale = null, + _defaultLocale = null; + + var _getActiveLocale = function () { + // This variable was set from 'LanguageService' in method setDefaultLocale() + return _.findWhere(_locales, {active: true}); + }; + + var _setActiveLocale = function (locale) { + var oldLocale = _getActiveLocale(), + newLocale = _.findWhere(_locales, {id: locale}); + + if (newLocale) { + if (oldLocale) { + oldLocale.active = false; + } + + newLocale.active = true; + } else { + throw new Error('You can not set a locale that has not been registered. Please register the locale first by calling addLocale()'); + } + }; + + /** + * Returns a translation for a key when a translation is available otherwise false + * @param key {String} + * @returns {*} + * @private + */ + var _getTranslationForKey = function (key) { + var activeLocale = _oldLocale || _getActiveLocale(); + + if (activeLocale && _dictionary && _dictionary[activeLocale.id]) { + var translation = _dictionary[activeLocale.id]; + angular.forEach(key.split('.'), function (k) { + translation = translation ? translation[k] : null; + }); + return translation; + } else { + return false; + } + }; + + /** + * Checks all locales for an available translation until it finds one + * @param property {String} + * @returns {*} + * @private + */ + var _getContentOfOtherLocale = function (property) { + var result; + _.each(_locales, function (locale) { + if (!result && property[locale.id]) { + result = property[locale.id]; + } + }); + if (!result) { + result = _.values(property)[0]; + } + return result; + }; + + /** + * Return all placeholders that are available in the translation string + * @param property {String} + * @returns {String} + * @private + */ + var _getUsedPlaceholdersInTranslationStr = function (str) { + + var re = /{{\s*([a-zA-Z0-9$_]+)\s*}}/g, + usedPlaceHolders = [], + matches; + + while ((matches = re.exec(str)) !== null) { + if (matches.index === re.lastIndex) { + re.lastIndex++; + } + usedPlaceHolders.push(matches[1]); + } + + return usedPlaceHolders; + }; + + /** + * Replaces placeholders in transaltion string with a value defined in the placeholder param + * @param str + * @param placeholder + * @returns {String} + * @private + */ + var _replacePlaceholders = function (str, placeholders) { + if (placeholders) { + var usedPlaceHolders = _getUsedPlaceholdersInTranslationStr(str); + usedPlaceHolders.forEach(function (usedPlaceholder) { + var escapedPlaceholder = usedPlaceholder.replace(/[$_]/g, '\\$&'), + replaceRegex = new RegExp('{{\\s*' + escapedPlaceholder + '\\s*}}'); + + str = str.replace(replaceRegex, placeholders[usedPlaceholder]); + }); + } + return str; + }; + + /** + * Registers a locale for which translations are available + * @param locale + * @param name + * @param fileExtension + */ + this.addLocale = function (locale, name, fileExtension, basePath) { + fileExtension = fileExtension || locale + '.json'; + + var existingLocale = _.findWhere(_locales, {id: locale}); + if (!existingLocale) { + _locales.push({ + id: locale, + name: name, + active: locale === _defaultLocale, + basePath: basePath || '', + fileExtension: fileExtension + }); + _dictionary[locale] = {}; + } else { + existingLocale.name = name; + existingLocale.fileExtension = fileExtension; + existingLocale.basePath = basePath; + } + }; + + /** + * Registers a resource so it can be accessed later by calling the public method get + * @param resourcePath {String} + * @param fileNameForLocale {String} + */ + this.addResource = function (resourcePath, basePath) { + basePath = basePath || ''; + + var existingResource = _.findWhere(_resources, {path: resourcePath}); + if (!existingResource) { + _resources.push({ + path: resourcePath, + basePath: basePath + }); + } else { + existingResource.basePath = basePath; + } + }; + + this.setDefaultLocale = function (locale) { + _defaultLocale = locale; + if (_.findWhere(_locales, {id: locale})) { + _setActiveLocale(locale); + } + }; + + this.$get = ['$templateRequest', '$q', '$rootScope', function ($templateRequest, $q, $rootScope) { + return { + /** + * Fills the dictionary with the translations by using the angular templateCache + * We need the dictionary because the get method has to be synchronous for the angular filter + * @param resourcePath {String} + */ + _loadResource: function (resourcePath) { + var resource = _.findWhere(_resources, {path: resourcePath}), + activeLocale = this.getActiveLocale(), + filePath = ''; + + if (resource && activeLocale) { + filePath = mwUI.Backbone.Utils.concatUrlParts(activeLocale.basePath, resource.basePath, resource.path, activeLocale.fileExtension); + + return $templateRequest(filePath).then(function (content) { + _.extend(_dictionary[activeLocale.id], JSON.parse(content)); + return content; + }); + } else { + return $q.reject('Resource not available or no locale has been set'); + } + }, + + /** + * Returns all registered locales + * @returns {Array} + */ + getLocales: function () { + return _locales; + }, + + /** + * Return the currently active locale + * @returns {Object} + */ + getActiveLocale: function () { + return _getActiveLocale(); + }, + + /** + * translates key into current locale, given placeholders in {{placeholderName}} are replaced + * @param key {String} + * @param placeholder {Object} + */ + get: function (key, placeholder) { + var translation = _getTranslationForKey(key); + if (translation) { + return _replacePlaceholders(translation, placeholder); + } else if (_isLoadingresources) { + return '...'; + } else { + return 'MISSING TRANSLATION ' + this.getActiveLocale().id + ': ' + key; + } + }, + + /** + * set the locale and loads all resources for that locale + * @param locale {String} + */ + setLocale: function (localeid) { + var loadTasks = []; + $rootScope.$broadcast('i18n:loadResourcesStart'); + _isLoadingresources = true; + _oldLocale = this.getActiveLocale(); + _setActiveLocale(localeid); + _.each(_resources, function (resource) { + loadTasks.push(this._loadResource(resource.path)); + }, this); + return $q.all(loadTasks).then(function () { + _isLoadingresources = false; + $rootScope.$broadcast('i18n:loadResourcesSuccess'); + _oldLocale = null; + $rootScope.$broadcast('i18n:localeChanged', localeid); + return localeid; + }); + }, + + /** + * checks if a translation for the key is available + * @param key {String} + * @returns {boolean} + */ + translationIsAvailable: function (key) { + return !!_getTranslationForKey(key); + }, + /** + * return value of an internationalized object e.g {en_US:'English text', de_DE:'German text'} + * When no translation is availabe for the current set locale it tries the default locale. + * When no translation is available for the defaultLocale it tries all other available locales until + * a translation is found + * @param property {object} + * @returns {String} + */ + localize: function (property) { + var activeLocale = this.getActiveLocale(); + var p = property[activeLocale.id]; + if (angular.isDefined(p) && p !== '') { + return p; + } else { + return property[_defaultLocale] || _getContentOfOtherLocale(property); + } + }, + + extendForLocale: function (locale, translations) { + if (!locale) { + throw new Error('Locale is a required argument!'); + } + if (!_.isObject(translations)) { + throw new Error('The translations argument is of type ' + typeof translations + ' but it has to be an object!'); + } + if (!_.findWhere(_locales, {id: locale})) { + throw new Error('The locale ' + locale + ' does not exist! Make sure you have registered it.'); + } + if (!_isLoadingresources) { + mwUI.Utils.shims.deepExtendObject(_dictionary[locale], translations); + } + $rootScope.$on('i18n:loadResourcesSuccess', function () { + mwUI.Utils.shims.deepExtendObject(_dictionary[locale], translations); + }); + }, + + extend: function (localesWithTranslations) { + if (!_.isObject(localesWithTranslations)) { + throw new Error('The localesWithTranslations argument is from type ' + typeof localesWithTranslations + ' but it has to be an object!'); + } + + for (var locale in localesWithTranslations) { + this.extendForLocale(locale, localesWithTranslations[locale]); + } + } + }; + }]; + + }); +angular.module('mwUI.i18n') + + .filter('i18n', ['i18n', function (i18n) { + + function i18nFilter(translationKey, placeholder) { + if (_.isString(translationKey)) { + return i18n.get(translationKey, placeholder); + } else if(_.isObject(translationKey)){ + return i18n.localize(translationKey); + } + } + + i18nFilter.$stateful = true; + + return i18nFilter; + }]); +angular.module('mwUI.Inputs', ['mwUI.i18n', 'mwUI.Backbone']); + +angular.module('mwUI.Inputs') + + .directive('input', function () { + return { + restrict: 'E', + link: function (scope, el, attrs) { + // render custom checkbox + // to preserve the functionality of the original checkbox we just wrap it with a custom element + // checkbox is set to opacity 0 and has to be positioned absolute inside the custom checkbox element which has to be positioned relative + // additionally a custom status indicator is appended as a sibling of the original checkbox inside the custom checkbox wrapper + var render = function () { + var customCheckbox = angular.element(''), + customCheckboxStateIndicator = angular.element(''), + customCheckboxStateFocusIndicator = angular.element(''); + + el.wrap(customCheckbox); + customCheckboxStateIndicator.insertAfter(el); + customCheckboxStateFocusIndicator.insertAfter(customCheckboxStateIndicator); + }; + + var init = function() { + //after this the remaining element is removed + scope.$on('$destroy', function () { + el.off(); + el.parent('.mw-checkbox').remove(); + }); + + render(); + + }; + + if(attrs.type==='checkbox'){ + init(); + } + } + }; + }); +angular.module('mwUI.Inputs') + + .directive('mwCheckboxGroup', ['i18n', function (i18n) { + return { + restrict: 'A', + scope: { + mwCollection: '=', + mwOptionsCollection: '=', + mwOptionsLabelKey: '@', + mwOptionsLabelI18nPrefix: '@', + mwRequired: '=', + mwDisabled: '=' + }, + templateUrl: 'uikit/mw-inputs/directives/templates/mw_checkbox_group.html', + link: function (scope) { + var getModelIdAttr = function () { + return scope.mwCollection.model.prototype.idAttribute; + }; + + scope.getLabel = function (model) { + var modelAttr = model.get(scope.mwOptionsLabelKey); + + if (modelAttr) { + if (scope.mwOptionsLabelI18nPrefix) { + return i18n.get(scope.mwOptionsLabelI18nPrefix + '.' + modelAttr); + } else { + return modelAttr; + } + } + }; + + scope.isOptionDisabled = function (model) { + return model.selectable.isDisabled(); + }; + + scope.isSelected = function (model) { + return !!scope.mwCollection.get(model.get(getModelIdAttr())); + }; + + scope.toggleModel = function (model) { + var existingModel = scope.mwCollection.findWhere(model.toJSON()); + if (existingModel) { + scope.mwCollection.remove(existingModel); + } else { + scope.mwCollection.add(model.toJSON()); + } + }; + + if (! (scope.mwCollection instanceof Backbone.Collection) ) { + throw new Error('[mwCheckboxGroup] The attribute mwCollection is required and has to be an instanceof Backbone Collection'); + } + + if ( !(scope.mwOptionsCollection instanceof Backbone.Collection) ) { + throw new Error('[mwCheckboxGroup] The attribute mwOptionsCollection is required and has to be an instanceof Backbone Collection'); + } + } + }; + }]); +var extendInput = function () { + return { + restrict: 'E', + require: '?^mwInputWrapper', + link: function (scope, el, attrs, mwInputWrapperCtrl) { + var skipTypes = ['radio','checkbox']; + + if(skipTypes.indexOf(attrs.type)===-1){ + el.addClass('form-control'); + } + + if(mwInputWrapperCtrl){ + if(attrs.type){ + mwInputWrapperCtrl.setType(attrs.type); + } else if(el[0].tagName){ + mwInputWrapperCtrl.setType(el[0].tagName.toLowerCase()); + } + } + } + }; +}; + +angular.module('mwUI.Inputs') + + .directive('select', extendInput) + + .directive('input', extendInput) + + .directive('textarea', extendInput); + +angular.module('mwUI.Inputs') + + .directive('input', function () { + return { + restrict: 'E', + link: function (scope, el, attrs) { + // render custom radio + // to preserve the functionality of the original checkbox we just wrap it with a custom element + // checkbox is set to opacity 0 and has to be positioned absolute inside the custom checkbox element which has to be positioned relative + // additionally a custom status indicator is appended as a sibling of the original checkbox inside the custom checkbox wrapper + var render = function () { + var customRadio = angular.element(''), + customRadioStateIndicator = angular.element(''), + customRadioStateFocusIndicator = angular.element(''); + + el.wrap(customRadio); + customRadioStateIndicator.insertAfter(el); + customRadioStateFocusIndicator.insertAfter(customRadioStateIndicator); + }; + + var init = function() { + //after this the remaining element is removed + scope.$on('$destroy', function () { + el.off(); + el.parent('.mw-radio').remove(); + }); + + render(); + + }; + + if(attrs.type === 'radio'){ + init(); + } + } + }; + }); +angular.module('mwUI.Inputs') + + .directive('mwRadioGroup', ['i18n', function (i18n) { + return { + restrict: 'A', + scope: { + mwModel: '=', + mwModelAttr: '@', + mwOptionsCollection: '=', + mwOptionsKey: '@', + mwOptionsLabelKey: '@', + mwOptionsLabelI18nPrefix: '@', + mwRequired: '=', + mwDisabled: '=' + }, + templateUrl: 'uikit/mw-inputs/directives/templates/mw_radio_group.html', + link: function (scope) { + scope.radioGroupId = _.uniqueId('radio_'); + + var setBackboneModel = function(model){ + if(scope.mwModelAttr){ + scope.mwModel.set(scope.mwModelAttr, model.get(scope.mwOptionsKey)); + } else { + scope.mwModel.set(model.toJSON()); + } + }; + + var unSetBackboneModel = function(){ + if(scope.mwModelAttr){ + scope.mwModel.unset(scope.mwModelAttr); + } else { + scope.mwModel.clear(); + } + }; + + scope.getLabel = function (model) { + var modelAttr = model.get(scope.mwOptionsLabelKey); + + if (modelAttr) { + if (scope.mwOptionsLabelI18nPrefix) { + return i18n.get(scope.mwOptionsLabelI18nPrefix + '.' + modelAttr); + } else { + return modelAttr; + } + } + }; + + scope.isOptionDisabled = function (model) { + return model.selectable.isDisabled(); + }; + + scope.getModelAttribute = function(){ + return scope.mwModelAttr || scope.mwModel.idAttribute; + }; + + scope.isChecked = function (model) { + if(scope.mwModelAttr){ + return model.get(scope.mwOptionsKey) === scope.mwModel.get(scope.mwModelAttr); + } else { + return model.id === scope.mwModel.id; + } + }; + + scope.selectOption = function (model) { + if (!scope.isChecked(model)) { + setBackboneModel(model); + } else { + unSetBackboneModel(); + } + }; + + if(scope.mwModelAttr && !scope.mwOptionsKey){ + throw new Error('[mwRadioGroup] When using mwModelAttr the attribute mwOptionsKey is required!'); + } + } + }; + }]); +angular.module('mwUI.Inputs') + + .directive('select', function () { + return { + link: function (scope, el) { + var customSelectWrapper = angular.element(''); + + var render = function () { + el.wrap(customSelectWrapper); + el.addClass('custom'); + }; + + render(); + } + }; + }); +angular.module('mwUI.Inputs') + + .directive('mwSelectBox', ['i18n', function (i18n) { + return { + restrict: 'A', + scope: { + mwModel: '=', + mwModelAttr: '@', + mwOptionsCollection: '=', + mwOptionsKey: '@', + mwOptionsLabelKey: '@', + mwOptionsLabelI18nPrefix: '@', + mwPlaceholder: '@', + mwRequired: '=', + mwDisabled: '=' + }, + templateUrl: 'uikit/mw-inputs/directives/templates/mw_select_box.html', + link: function (scope) { + + scope.viewModel = {}; + + var setBackboneModel = function (model) { + if (scope.mwModelAttr) { + scope.mwModel.set(scope.mwModelAttr, model.get(scope.mwOptionsKey)); + } else { + scope.mwModel.set(model.toJSON()); + } + }; + + var unSetBackboneModel = function () { + if (scope.mwModelAttr) { + scope.mwModel.unset(scope.mwModelAttr); + } else { + scope.mwModel.clear(); + } + }; + + var setSelectedVal = function () { + + if (scope.mwModel.id) { + scope.viewModel.selected = scope.mwModel.id.toString(); + } + }; + + var checkIfOptionModelHasId = function () { + scope.mwOptionsCollection.each(function (model) { + if (!model.id) { + throw new Error('[mwSelectBox] Each model of the options collection must have an id. Make sure you set the correct model and modelId attribute!'); + } + }); + }; + + var unset = function () { + unSetBackboneModel(); + scope.viewModel.selected = null; + }; + + scope.getLabel = function (model) { + var modelAttr = model.get(scope.mwOptionsLabelKey); + + if (modelAttr) { + if (scope.mwOptionsLabelI18nPrefix) { + return i18n.get(scope.mwOptionsLabelI18nPrefix + '.' + modelAttr); + } else { + return modelAttr; + } + } + }; + + scope.hasPlaceholder = function () { + return scope.mwPlaceholder || scope.mwRequired; + }; + + scope.getPlaceholder = function () { + if (scope.mwPlaceholder) { + return scope.mwPlaceholder; + } else if (scope.mwRequired) { + return i18n.get('mwSelectBox.pleaseSelect'); + } + }; + + scope.isOptionDisabled = function (model) { + return model.selectable.isDisabled(); + }; + + scope.getModelAttribute = function () { + return scope.mwModelAttr || scope.mwModel.idAttribute; + }; + + scope.isChecked = function (model) { + if (scope.mwModelAttr && scope.mwModel instanceof Backbone.Model) { + return model.get(scope.mwOptionsKey) === scope.mwModel.get(scope.mwModelAttr); + } else { + return model.id === scope.mwModel.id; + } + }; + + scope.select = function (id) { + if (id) { + scope.selectOption(scope.mwOptionsCollection.get(id)); + } else { + unset(); + } + }; + + scope.selectOption = function (model) { + if (!scope.isChecked(model)) { + setBackboneModel(model); + } + }; + + if (scope.mwModel) { + if(!(scope.mwModel instanceof Backbone.Model)){ + throw new Error('[mwSelectBox] The attribute mw-model is from type '+typeof scope.mwModel+' but has to be a Backbone Model!'); + } + + scope.mwModel.on('change:' + scope.mwModel.idAttribute, setSelectedVal); + setSelectedVal(); + + scope.mwModel.on('change', function (model) { + if ((scope.mwModelAttr && !model.get(scope.mwModelAttr)) || (!scope.mwModelAttr && !model.id)) { + unset(); + } + }); + } + + if (scope.mwModelAttr && !scope.mwOptionsKey) { + throw new Error('[mwSelectBox] When using mwModelAttr the attribute mwOptionsKey is required!'); + } + + if (!scope.mwOptionsCollection || !(scope.mwOptionsCollection instanceof Backbone.Collection)) { + throw new Error('[mwSelectBox] An options collection is required. Make sure you set the attribute mw-options-collection and that it is a backbone collection!'); + } + + checkIfOptionModelHasId(); + scope.mwOptionsCollection.on('add', checkIfOptionModelHasId); + } + }; + }]); +angular.module('mwUI.Inputs') + + .directive('mwToggle', ['$timeout', function ($timeout) { + return { + scope: { + mwModel: '=', + mwDisabled: '=', + mwIconOn: '@', + mwIconOff: '@', + mwChange: '&' + }, + replace: true, + templateUrl: 'uikit/mw-inputs/directives/templates/mw_toggle.html', + link: function (scope) { + scope.toggle = function (value) { + if (scope.mwModel !== value) { + scope.mwModel = !scope.mwModel; + $timeout(function () { + scope.mwChange({value: scope.mwModel}); + }); + } + }; + } + }; + }]); + +angular.module('mwUI.Inputs') + .config(['i18nProvider', function(i18nProvider) { + i18nProvider.addResource('mw-inputs/i18n', 'uikit'); + }]); +angular.module('mwUI.Layout', ['mwUI.Utils']); + +angular.module('mwUI.Layout') + + .directive('mwUi', ['Modal', function (Modal) { + return { + transclude: true, + templateUrl: 'uikit/mw-layout/directives/templates/mw_ui.html', + link: function (scope) { + scope.displayToasts = function(){ + return Modal.getOpenedModals().length === 0; + }; + } + }; + }]); +angular.module('mwUI.Layout') + + .directive('mwHeader', ['$rootScope', '$route', '$location', 'BrowserTitleHandler', function ($rootScope, $route, $location, BrowserTitleHandler) { + return { + transclude: true, + scope: { + title: '@', + url: '@', + mwTitleIcon: '@', + showBackButton: '=', + mwBreadCrumbs: '=' + }, + templateUrl: 'uikit/mw-layout/directives/templates/mw_header.html', + link: function (scope, el, attrs, ctrl, $transclude) { + $rootScope.siteTitleDetails = scope.title; + BrowserTitleHandler.setTitle(scope.title); + + $transclude(function (clone) { + if ((!clone || clone.length === 0) && !scope.showBackButton) { + el.find('.mw-header').addClass('no-buttons'); + } + }); + + scope.refresh = function () { + $route.reload(); + }; + + if (!scope.url && scope.mwBreadCrumbs && scope.mwBreadCrumbs.length > 0) { + scope.url = scope.mwBreadCrumbs[scope.mwBreadCrumbs.length - 1].url; + scope.url = scope.url.replace('#', ''); + } else if (!scope.url && scope.showBackButton) { + console.error('Url attribute in header is missing!!'); + } + + scope.back = function () { + $location.path(scope.url); + }; + } + }; + }]); +angular.module('mwUI.Layout') + + .directive('mwSidebar', function () { + return { + transclude: true, + templateUrl: 'uikit/mw-layout/directives/templates/mw_sidebar.html', + link: function (scope, el) { + var sidebarEl = el.find('.mw-sidebar'), + footerEl = angular.element('.mw-footer'), + headerEl = angular.element('.mw-header'), + navbarEl = angular.element('.mw-menu-top-bar'); + + var setMaxHeight = function () { + var docHeight = angular.element(window).innerHeight(), + maxHeight = docHeight; + + if (headerEl.length) { + maxHeight -= headerEl.height(); + } + + if (navbarEl.length) { + maxHeight -= navbarEl.height(); + } + + if (footerEl.length) { + maxHeight -= footerEl.height(); + } + + sidebarEl.css('maxHeight', maxHeight); + }; + + var throttledSetMaxHeight = _.throttle(setMaxHeight, 100); + + setMaxHeight(); + angular.element(window).on('resize', throttledSetMaxHeight); + } + }; + }); +angular.module('mwUI.Layout') + + .directive('mwFooter', function () { + return { + transclude: true, + scope: { + type: '@mwFooter' + }, + templateUrl: 'uikit/mw-layout/directives/templates/mw_footer.html' + }; + }); +angular.module('mwUI.Layout') + + .directive('mwSubNav', function () { + return { + restrict: 'A', + scope: { + justified: '=' + }, + transclude: true, + replace: true, + templateUrl: 'uikit/mw-layout/directives/templates/mw_sub_nav.html' + }; + }); +angular.module('mwUI.Layout') + + .directive('mwSubNavPill', ['$location', function ($location) { + return { + restrict: 'A', + scope: { + url: '@mwSubNavPill', + mwDisabled: '=' + }, + transclude: true, + replace: true, + templateUrl: 'uikit/mw-layout/directives/templates/mw_sub_nav_pill.html', + link: function (scope, elm) { + var setActiveClassOnUrlMatch = function (url) { + if (scope.url && url === scope.url.slice(1)) { + elm.addClass('active'); + } else { + elm.removeClass('active'); + } + }; + + scope.$watch('url', function (newUrlAttr) { + if (newUrlAttr) { + setActiveClassOnUrlMatch($location.$$path); + } + }); + + scope.navigate = function(url){ + if(!scope.mwDisabled){ + url = url.replace(/#\//,''); + $location.path(url); + $location.replace(); + } + }; + + setActiveClassOnUrlMatch($location.$$path); + + } + }; + }]); +angular.module('mwUI.List', ['mwUI.i18n', 'mwUI.Backbone', 'mwUI.UiComponents']); + +angular.module('mwUI.List') + + //Todo rename to mwList + .directive('mwListableBb', function(){ + return { + //TODO rename collection to mwCollection + //Move sort and filter persistance into filterable and remove mwListCollection + scope: { + collection: '=', + mwListCollection: '=' + }, + compile: function (elm) { + elm.append(''); + + return function (scope, elm) { + elm.addClass('table table-striped mw-list'); + }; + }, + controller: ['$scope', function ($scope) { + var _columns = $scope.columns = [], + _collection = null, + _mwListCollectionFilter = null; + + this.actionColumns = []; + + this.registerColumn = function (scope) { + _columns.push(scope); + }; + + this.unRegisterColumn = function (scope) { + if (scope && scope.$id) { + var scopeInArray = _.findWhere(_columns, {$id: scope.$id}), + indexOfScope = _.indexOf(_columns, scopeInArray); + + if (indexOfScope > -1) { + _columns.splice(indexOfScope, 1); + } + } + }; + + this.getColumns = function () { + return _columns; + }; + + this.getCollection = function () { + return _collection; + }; + + this.isSingleSelection = function () { + if (_collection && _collection.selectable) { + return _collection.selectable.isSingleSelection(); + } + return false; + }; + + $scope.$on('$destroy', function () { + this.actionColumns = []; + }.bind(this)); + + if ($scope.mwListCollection) { + _collection = $scope.mwListCollection.getCollection(); + _mwListCollectionFilter = $scope.mwListCollection.getMwListCollectionFilter(); + } else if ($scope.collection) { + _collection = $scope.collection; + } + }] + }; + }); +angular.module('mwUI.List') + + //TODO rename to mwListBodyRow + .directive('mwListableBodyRowBb', ['$timeout', function ($timeout) { + return { + require: '^mwListableBb', + compile: function (elm) { + + elm.prepend(''); + + return function (scope, elm, attr, mwListCtrl) { + var selectedClass = 'selected'; + + scope.collection = mwListCtrl.getCollection(); + + if (!scope.item) { + throw new Error('No item available in the list! Please make sure to use ng-repeat="item in collection"'); + } + + if (scope.item && scope.item.selectable && !scope.item.selectable.isDisabled()) { + elm.addClass('selectable clickable'); + } else if (mwListCtrl.actionColumns && mwListCtrl.actionColumns.length > 0) { + elm.addClass('clickable'); + } + + elm.on('click', function () { + if (scope.item && scope.item.selectable && !scope.item.selectable.isDisabled()) { + $timeout(function () { + scope.item.selectable.toggleSelect(); + }); + } + }); + + elm.on('dblclick', function () { + if (mwListCtrl.actionColumns && angular.isNumber(scope.$index)) { + var existingLink = _.findWhere(mwListCtrl.actionColumns,{id:scope.$index}); + if(existingLink){ + document.location.href = existingLink.link; + } + } + }); + + scope.$watch('item.selectable.isSelected()', function (value) { + if (value) { + elm.addClass(selectedClass); + } else { + elm.removeClass(selectedClass); + } + }); + }; + } + }; + }]); +angular.module('mwUI.List') + + .directive('mwListBodyRowCheckbox', function () { + return { + restrict: 'A', + require: '^mwListableBb', + scope: { + item: '=' + }, + templateUrl: 'uikit/mw-list/directives/templates/mw_list_body_row_checkbox.html', + link: function (scope, elm, attr, mwListCtrl) { + scope.isSingleSelection = mwListCtrl.isSingleSelection(); + scope.click = function (item, $event) { + $event.stopPropagation(); + if (item.selectable) { + item.selectable.toggleSelect(); + } + }; + + scope.$watch('item.selectable.isDisabled()', function (isDisabled) { + if (isDisabled) { + scope.item.selectable.unSelect(); + } + }); + } + }; + }); +angular.module('mwUI.List') + //TODO rename + .directive('mwListableFooterBb', function () { + return { + require: '^mwListableBb', + templateUrl: 'uikit/mw-list/directives/templates/mw_list_footer.html', + link: function (scope, elm, attr, mwListCtrl) { + scope.collection = mwListCtrl.getCollection(); + scope.columns = mwListCtrl.getColumns(); + + scope.collection.on('request', function(){ + scope.isSynchronising = true; + }); + + scope.collection.on('sync error', function(){ + scope.isSynchronising = false; + }); + + scope.showSpinner = function(){ + return scope.isSynchronising && scope.collection.filterable.hasNextPage(); + }; + } + }; + }); +angular.module('mwUI.List') + +// TODO: rename to something else +// TODO: extract functionalities into smaller directives + .directive('mwListableHead2', ['$window', '$document', 'i18n', function ($window, $document, i18n) { + return { + scope: { + collection: '=', + mwListCollection: '=', + affix: '=', + affixOffset: '=', + collectionName: '@', + nameFn: '&', + nameAttribute: '@', + localizeName: '@', + nameI18nPrefix: '@', + nameI18nSuffix: '@', + searchAttribute: '@' + }, + transclude: true, + templateUrl: 'uikit/mw-list/directives/templates/mw_list_head.html', + link: function (scope, el, attrs, ctrl, $transclude) { + var scrollEl, + bodyEl = angular.element('body'), + modalEl = el.parents('*[mw-modal-body]'), + mwHeaderEl = angular.element('*[mw-header]'), + canShowSelected = false, + _affix = angular.isDefined(scope.affix) ? scope.affix : true, + windowEl = angular.element($window), + collection; + + scope.selectable = false; + scope.selectedAmount = 0; + scope.collectionName = scope.collectionName || i18n.get('List.mwListHead.items'); + scope.isModal = modalEl.length > 0; + scope.isLoadingModelsNotInCollection = false; + scope.hasFetchedModelsNotInCollection = false; + scope.isLoadingModelsNotInCollection = false; + scope.hasFetchedModelsNotInCollection = false; + + var newOffset; + + var throttledScrollFn = _.throttle(function () { + if (!el.is(':visible')) { + return; + } + + if (!newOffset) { + var headerOffset, + headerHeight, + headerBottomOffset, + listHeaderOffset, + spacer; + + if (scope.isModal) { + var modalHeaderEl = el.parents('.modal-content').find('.modal-header'); + headerOffset = modalHeaderEl.offset().top; + headerHeight = modalHeaderEl.innerHeight(); + spacer = 0; + } else if (mwHeaderEl.length) { + headerOffset = mwHeaderEl.last().offset().top; + headerHeight = mwHeaderEl.last().innerHeight(); + spacer = 5; + } else { + return; + } + + headerBottomOffset = headerOffset + headerHeight; + listHeaderOffset = el.offset().top; + + newOffset = listHeaderOffset - headerBottomOffset - spacer; + } + + var scrollTop = scrollEl.scrollTop(); + + if (scrollTop > newOffset && _affix) { + el.find('.mw-listable-header').css('top', scrollTop - newOffset); + el.addClass('affixed'); + } else if (!_affix) { + scrollEl.off('scroll', throttledScrollFn); + } else { + el.find('.mw-listable-header').css('top', 'initial'); + el.removeClass('affixed'); + } + + }, 10); + + var throttledRecalculate = _.throttle(function () { + el.find('.mw-listable-header').css('top', 'initial'); + newOffset = null; + }); + + var loadItemsNotInCollection = function () { + if (scope.hasFetchedModelsNotInCollection) { + return; + } + var selectedNotInCollection = []; + scope.selectable.getSelected().each(function (model) { + if (!model.selectable.isInCollection && !scope.getModelAttribute(model)) { + selectedNotInCollection.push(model); + } + }); + + if (selectedNotInCollection.length === 0) { + return; + } + + var CollectionWithMissingEntries = collection.constructor.extend({ + filterableOptions: function () { + return { + filterDefinition: function () { + var filter = new window.mCAP.Filter(), + filters = []; + + selectedNotInCollection.forEach(function (model) { + if (model.id) { + filters.push( + filter.string(model.idAttribute, model.id) + ); + } + }); + + return filter.or(filters); + } + }; + } + }); + var collectionExt = new CollectionWithMissingEntries(); + collectionExt.url = collection.url(); + + scope.isLoadingModelsNotInCollection = true; + + collectionExt.fetch().then(function (collection) { + scope.hasFetchedModelsNotInCollection = true; + var selected = scope.selectable.getSelected(); + collection.each(function (model) { + selected.get(model.id).set(model.toJSON()); + }); + + var deletedUuids = _.difference(_.pluck(selectedNotInCollection, 'id'), collection.pluck('uuid')); + + deletedUuids.forEach(function (id) { + selected.get(id).selectable.isDeletedItem = true; + }); + + scope.isLoadingModelsNotInCollection = false; + }); + }; + + scope.getCollection = function () { + return collection; + }; + + scope.showSelected = function () { + canShowSelected = true; + loadItemsNotInCollection(); + setTimeout(function () { + var height; + if (scope.isModal) { + height = modalEl.height() + (modalEl.offset().top - el.find('.selected-items').offset().top) + 25; + modalEl.css('overflow', 'hidden'); + } else { + height = angular.element($window).height() - el.find('.selected-items').offset().top + scrollEl.scrollTop() - 25; + bodyEl.css('overflow', 'hidden'); + } + + el.find('.selected-items').css('height', height); + el.find('.selected-items').css('bottom', height * -1); + }); + }; + + scope.hideSelected = function () { + if (scope.isModal) { + modalEl.css('overflow', 'auto'); + } else { + bodyEl.css('overflow', 'inherit'); + } + canShowSelected = false; + }; + + scope.canShowSelected = function () { + return scope.selectable && canShowSelected && scope.selectedAmount > 0; + }; + + scope.unSelect = function (model) { + model.selectable.unSelect(); + }; + + scope.toggleSelectAll = function () { + scope.selectable.toggleSelectAll(); + }; + + scope.getTotalAmount = function () { + if (collection.filterable && collection.filterable.getTotalAmount()) { + return collection.filterable.getTotalAmount(); + } else { + return collection.length; + } + }; + + scope.toggleShowSelected = function () { + if (canShowSelected) { + scope.hideSelected(); + } else { + scope.showSelected(); + } + }; + + scope.getModelAttribute = function (model) { + if (scope.nameAttribute) { + var modelAttr = model.get(scope.nameAttribute); + + if (scope.nameI18nPrefix || scope.nameI18nSuffix) { + var i18nPrefix = scope.nameI18nPrefix || '', + i18nSuffix = scope.nameI18nSuffix || ''; + + return i18n.get(i18nPrefix + '.' + modelAttr + '.' + i18nSuffix); + } else if (angular.isDefined(scope.localizeName)) { + return i18n.localize(modelAttr); + } else { + return modelAttr; + } + } else { + return scope.nameFn({item: model}); + } + }; + + var init = function () { + scope.selectable = collection.selectable; + if (scope.isModal) { + //element in modal + scrollEl = modalEl; + } + else { + //element in window + scrollEl = windowEl; + } + + // Register scroll callback + scrollEl.on('scroll', throttledScrollFn); + + scrollEl.on('resize', throttledRecalculate); + + // Deregister scroll callback if scope is destroyed + scope.$on('$destroy', function () { + scrollEl.off('scroll', throttledScrollFn); + }); + + scope.$on('$destroy', function () { + scrollEl.off('resize', throttledRecalculate); + }); + + el.on('focus', 'input[type=text]', function () { + el.find('.search-bar').addClass('focused'); + }); + + el.on('blur', 'input[type=text]', function () { + el.find('.search-bar').removeClass('focused'); + }); + }; + + $transclude(function (clone) { + if (clone && clone.length > 0) { + el.addClass('has-extra-content'); + } + }); + + scope.$watch(function () { + if (scope.selectable) { + return scope.selectable.getSelected().length; + } else { + return 0; + } + }, function (val) { + scope.selectedAmount = val; + if (val < 1) { + scope.hideSelected(); + } + }); + + if (scope.mwListCollection) { + collection = scope.mwListCollection.getCollection(); + } else if (scope.collection) { + collection = scope.collection; + } else { + throw new Error('[mwListableHead2] Either a collection or a mwListCollection has to be passed as attribute'); + } + init(); + } + }; + }]); +angular.module('mwUI.List') + + //TODO rename to mwListHeader + .directive('mwListableHeaderBb', function () { + return { + require: '^mwListableBb', + scope: { + property: '@sort' + }, + transclude: true, + replace: true, + templateUrl: 'uikit/mw-list/directives/templates/mw_list_header.html', + link: function (scope, elm, attr, mwListCtrl) { + var ascending = '+', + descending = '-', + collection = mwListCtrl.getCollection(); + + var getSortOrder = function () { + if (collection && collection.filterable) { + return collection.filterable.getSortOrder(); + } else { + return false; + } + }; + + var sort = function (property, order) { + var sortOrder = order + property; + + collection.filterable.setSortOrder(sortOrder); + return collection.fetch(); + }; + + scope.canBeSorted = function(){ + return angular.isString(scope.property) && scope.property.length > 0 && !!collection.filterable; + }; + + scope.toggleSortOrder = function () { + if (scope.canBeSorted()) { + var sortOrder = ascending; //default + if (getSortOrder() === ascending + scope.property) { + sortOrder = descending; + } + sort(scope.property, sortOrder); + } + }; + + scope.isSelected = function (prefix) { + var sortOrder = getSortOrder(); + + if (sortOrder && prefix) { + return sortOrder === prefix + scope.property; + } else if(sortOrder && !prefix){ + return (sortOrder === '+' + scope.property || sortOrder === '-' + scope.property); + } + }; + + mwListCtrl.registerColumn(scope); + + scope.$on('$destroy', function () { + mwListCtrl.unRegisterColumn(scope); + }); + } + }; + }); +angular.module('mwUI.List') + + //TODO rename to mwListHeaderRow + .directive('mwListableHeaderRowBb', function () { + return { + require: '^mwListableBb', + scope: true, + compile: function (elm) { + elm.prepend(''); + elm.append(''); + + return function (scope, elm, attr, mwListCtrl) { + //empty collection is [] so ng-if would not work as expected + //we also have to check if the collection has a selectable + scope.hasCollection = false; + var collection = mwListCtrl.getCollection(); + if (collection) { + scope.hasCollection = angular.isDefined(collection.length) && collection.selectable; + } + scope.actionColumns = mwListCtrl.actionColumns; + }; + } + }; + }); +angular.module('mwUI.List') + //TODO rename to mwListUrlActionButton + .directive('mwListableLinkShowBb', function () { + return { + restrict: 'A', + require: '^mwListableBb', + template: '', + link: function (scope, elm, attr, mwListableCtrl) { + var updateLink = function(link){ + if(_.isNumber(scope.$index) && link){ + var existingItem = _.findWhere(mwListableCtrl.actionColumns, {id: scope.$index}); + + if(existingItem){ + existingItem.link = link; + } else { + mwListableCtrl.actionColumns.push({id: scope.$index, link: link}); + } + } + scope.link = link; + }; + + var removeLink = function(){ + if(scope.$index){ + var existingItem = _.findWhere(mwListableCtrl.actionColumns, {id:scope.$index}); + if(existingItem){ + var indexOfExistingItem = _.indexOf(mwListableCtrl.actionColumns, existingItem); + mwListableCtrl.actionColumns.splice(indexOfExistingItem, 1); + } + } + }; + + scope.linkTarget = attr.linkTarget; + updateLink(attr.mwListableLinkShowBb); + attr.$observe('mwListableLinkShowBb', updateLink); + scope.$on('$destroy', removeLink); + } + }; + }); + +angular.module('mwUI.List') + + .config(['i18nProvider', function(i18nProvider){ + i18nProvider.addResource('mw-list/i18n', 'uikit'); + }]); + +/** + * Created by zarges on 23/02/16. + */ +angular.module('mwUI.Menu', []); + +window.mwUI.Menu = {}; + +var routeToRegex = mwUI.Utils.shims.routeToRegExp; + +var MwMenuEntry = window.mwUI.Backbone.NestedModel.extend({ + idAttribute: 'id', + defaults: function () { + return { + url: null, + label: null, + icon: null, + activeUrls: [], + order: null, + action: null, + isActive: null, + target: null + }; + }, + nested: function () { + return { + subEntries: window.mwUI.Menu.MwMenuSubEntries + }; + }, + _throwMissingIdError: function (entry) { + throw new Error('No id is specified for the entry', entry); + }, + _throwNoTypeCouldBeDeterminedError: function (entry) { + throw new Error('No type could be determinded for the given entry: ', entry); + }, + _throwNotValidEntryError: function (entry) { + throw new Error('Is not a valid entry', entry); + }, + _determineType: function (entry) { + if (!entry.type) { + if (!entry.url && (!entry.subEntries || entry.subEntries.length === 0) && !(entry.label || entry.icon)) { + entry.type = 'DIVIDER'; + } else if (entry.url || entry.subEntries && entry.subEntries.length > 0 && (entry.label || entry.icon)) { + entry.type = 'ENTRY'; + } else { + this._throwNoTypeCouldBeDeterminedError(); + } + } + + return entry; + }, + _missingLabel: function (entry) { + return entry.type === 'ENTRY' && !entry.label && !entry.icon; + }, + _urlsAreMatching: function (url, matchUrl) { + if (matchUrl.match('#')) { + matchUrl = matchUrl.split('#')[1]; + } + return url.match(routeToRegex(matchUrl)); + }, + validate: function (entry) { + if (entry && _.isObject(entry)) { + entry = this._determineType(entry); + if (!entry.id) { + this._throwMissingIdError(); + } + if (!this.isValidEntry(entry)) { + this._throwNotValidEntryError(entry); + } + } + }, + set: function (entry, options) { + options = options || {}; + if (_.isUndefined(options.validate)) { + this.validate(entry); + } + return window.mwUI.Backbone.NestedModel.prototype.set.call(this, entry, options); + }, + isValidEntry: function (entry) { + if (entry.type) { + return !this._missingLabel(entry); + } else { + return false; + } + }, + ownUrlIsActiveForUrl: function (url) { + if (this.get('url')) { + return this._urlsAreMatching(url, this.get('url')); + } else { + return false; + } + }, + activeUrlIsActiveForUrl: function (url) { + var isActive = false; + this.get('activeUrls').forEach(function (activeUrl) { + if (!isActive) { + isActive = this._urlsAreMatching(url, activeUrl); + } + }.bind(this)); + return isActive; + }, + isSubEntry: function () { + if (this.collection && this.collection.parent) { + return true; + } + return false; + }, + hasSubEntries: function () { + return this.get('subEntries').length > 0; + }, + hasManualActiveFunction: function () { + return this.get('isActive') && typeof this.get('isActive') === 'function'; + }, + isActiveForUrl: function (url) { + if (this.hasManualActiveFunction()) { + return this.get('isActive')(); + } else { + return this.ownUrlIsActiveForUrl(url) || this.activeUrlIsActiveForUrl(url); + } + }, + getActiveSubEntryForUrl: function (url) { + return this.get('subEntries').getActiveEntryForUrl(url); + }, + hasActiveSubEntryOrIsActiveForUrl: function (url) { + if (this.get('type') === 'ENTRY') { + if (this.hasManualActiveFunction()) { + return this.get('isActive')(); + } else { + return !!this.getActiveSubEntryForUrl(url) || this.isActiveForUrl(url); + } + } + }, + constructor: function (model, options) { + options = options || {}; + options.validate = model ? true : false; + return window.mwUI.Backbone.NestedModel.prototype.constructor.call(this, model, options); + } +}); + +window.mwUI.Menu.MwMenuEntry = MwMenuEntry; + +/** + * Created by zarges on 15/02/16. + */ +var MwMenuEntries = Backbone.Collection.extend({ + model: window.mwUI.Menu.MwMenuEntry, + comparator: 'order', + _isAlreadyRegistered: function(entry) { + return ( + this.get(entry.id) || + (entry.url && this.findWhere({url: entry.url})) + ); + }, + _throwIsAlreadyRegisteredError: function(entry){ + if(entry.url){ + throw new Error('The entry with the id ' + entry.id + ' and the url ' + entry.url + ' has already been registered'); + } else { + throw new Error('The entry with the id ' + entry.id + ' has already been registered'); + } + }, + add: function(entries){ + if(_.isArray(entries)){ + entries.forEach(function(entry){ + if(this._isAlreadyRegistered(entry)){ + this._throwIsAlreadyRegisteredError(entry); + } + }.bind(this)); + } else { + if(this._isAlreadyRegistered(entries)){ + this._throwIsAlreadyRegisteredError(entries); + } + } + return Backbone.Collection.prototype.add.apply(this, arguments); + }, + addEntry: function(id, url, label, options){ + options = options || {}; + var addObj = { + id: id, + url: url, + label: label, + icon: options.icon, + activeUrls: options.activeUrls || [], + order: options.order, + subEntries: options.subEntries || [], + type: 'ENTRY' + }; + + return this.add(addObj); + }, + addDivider: function(id, options){ + options = options || {}; + var addObj = { + id: id, + label: options.label, + order: options.order, + type: 'DIVIDER' + }; + + return this.add(addObj); + }, + + getActiveEntryForUrl: function(url){ + var activeEntryFound = false, + activeEntry = null; + + this.each(function(model){ + if(!activeEntryFound && model.hasActiveSubEntryOrIsActiveForUrl(url)){ + activeEntryFound = true; + activeEntry = model; + } + }); + + return activeEntry; + } +}); + +window.mwUI.Menu.MwMenuEntries = MwMenuEntries; +/** + * Created by zarges on 15/02/16. + */ +var MwMenuSubEntries = window.mwUI.Menu.MwMenuEntries.extend({}); + +window.mwUI.Menu.MwMenuSubEntries = MwMenuSubEntries; +/** + * Created by zarges on 15/02/16. + */ +var MwMenu = window.mwUI.Menu.MwMenuEntries.extend({}); + +window.mwUI.Menu.MwMenu = MwMenu; + +angular.module('mwUI.Menu') + + .directive('mwMenuTopBar', function () { + return { + transclude: { + 'brand': '?img', + 'entries': '?div' + }, + templateUrl: 'uikit/mw-menu/directives/templates/mw_menu_top_bar.html' + }; + }); +angular.module('mwUI.Menu') + + .directive('mwMenuTopEntries', ['$rootScope', '$timeout', function ($rootScope, $timeout) { + return { + scope: { + menu: '=mwMenuTopEntries', + right: '=' + }, + transclude: true, + templateUrl: 'uikit/mw-menu/directives/templates/mw_menu_top_entries.html', + controller: ['$scope', function ($scope) { + var menu = $scope.menu || new mwUI.Menu.MwMenu(); + + this.getMenu = function () { + return menu; + }; + }], + link: function (scope, el, attrs, ctrl) { + scope.entries = ctrl.getMenu(); + + scope.unCollapse = function () { + var collapseEl = el.find('.navbar-collapse'); + if (collapseEl.hasClass('in')) { + collapseEl.collapse('hide'); + } + }; + + $rootScope.$on('$locationChangeSuccess', function () { + scope.unCollapse(); + }); + + scope.$on('mw-menu:triggerReorder', _.throttle(function () { + $timeout(function () { + scope.$broadcast('mw-menu:reorder'); + }); + })); + + scope.$on('mw-menu:triggerResort', _.throttle(function () { + $timeout(function () { + scope.$broadcast('mw-menu:resort'); + scope.entries.sort(); + }); + })); + + scope.entries.on('add remove reset', _.throttle(function () { + $timeout(function () { + scope.$broadcast('mw-menu:reorder'); + }); + })); + } + }; + }]); +angular.module('mwUI.Menu') + + .directive('mwMenuTopDropDownItem', function () { + return { + scope: { + entry: '=mwMenuTopDropDownItem' + }, + templateUrl: 'uikit/mw-menu/directives/templates/mw_menu_top_drop_down_item.html' + }; + }); +angular.module('mwUI.Menu') + + .directive('mwMenuTopItem', function () { + return { + scope: { + entry: '=mwMenuTopItem' + }, + templateUrl: 'uikit/mw-menu/directives/templates/mw_menu_top_item.html', + link: function(scope){ + scope.executeAction = function(){ + var action = scope.entry.get('action'); + if(action && typeof action === 'function' ){ + action(); + } + }; + } + }; + }); +angular.module('mwUI.Menu') + + .directive('mwMenuEntry', ['$timeout', function ($timeout) { + return { + scope: { + id: '@', + url: '@', + icon: '@', + label: '@', + type: '@', + target: '@', + class: '@styleClass', + order: '=', + activeUrls: '=', + action: '&', + isActive: '&' + }, + templateUrl: 'uikit/mw-menu/directives/templates/mw_menu_entry.html', + require: ['mwMenuEntry', '?^^mwMenuEntry', '?^mwMenuTopEntries'], + transclude: true, + controller: function () { + var _menuEntry; + + this.setMenuEntry = function (menuEntry) { + _menuEntry = menuEntry; + }; + + this.getMenuEntry = function () { + return _menuEntry; + }; + }, + link: function (scope, el, attrs, ctrls) { + var ctrl = ctrls[0], + parentCtrl = ctrls[1], + menuCtrl = ctrls[2], + menuEntry = new mwUI.Menu.MwMenuEntry(), + entryHolder; + + var getDomOrder = function () { + var orderDomEl = el; + + while (true) { + if (orderDomEl.parent('.mw-menu-entry').length !== 0 || orderDomEl.parent('.mw-menu-entries').length !== 0) { + break; + } + orderDomEl = orderDomEl.parent(); + } + + return orderDomEl.index() + 1; + }; + + var tryToRegisterAtParent = function () { + if (parentCtrl) { + if (!parentCtrl.getMenuEntry()) { + // TODO could not produce that error. In case the following exception is thrown write a test case and comment line in + //return $timeout(tryToRegisterAtParent); + throw new Error('Menu entry is not available, so registration failed!'); + } + entryHolder = parentCtrl.getMenuEntry().get('subEntries'); + } else if (menuCtrl) { + entryHolder = menuCtrl.getMenu(); + } + + if (entryHolder && !entryHolder.get(menuEntry)) { + entryHolder.add(menuEntry); + } + }; + + var setMenuEntry = function () { + menuEntry.set({ + id: scope.id || scope.url || scope.label || scope.$id, + label: scope.label, + url: scope.url, + icon: scope.icon, + type: scope.type || 'ENTRY', + target: scope.target, + order: scope.order || getDomOrder(), + activeUrls: scope.activeUrls || [], + class: scope.class, + action: attrs.action ? function () { + scope.action(); + } : null, + isActive: attrs.isActive ? function () { + return scope.isActive(); + } : null + }); + }; + + setMenuEntry(); + + scope.menuEntry = menuEntry; + + $timeout(tryToRegisterAtParent); + + ctrl.setMenuEntry(menuEntry); + + menuEntry.get('subEntries').on('add remove reset change:order', function () { + scope.$emit('mw-menu:triggerReorder'); + }); + + menuEntry.on('change:order', function () { + scope.$emit('mw-menu:triggerResort'); + }); + + scope.$on('mw-menu:reorder', function () { + if (!scope.order) { + menuEntry.set('order', getDomOrder()); + } + }); + + scope.$on('mw-menu:resort', function () { + menuEntry.get('subEntries').sort(); + }); + + scope.$on('$destroy', function () { + if (entryHolder) { + entryHolder.remove(menuEntry); + } + }); + + scope.$watchGroup(['id', 'label', 'url', 'icon', 'class', 'order', 'target'], setMenuEntry); + } + }; + }]); +angular.module('mwUI.Menu') + + .directive('mwMenuDivider', function () { + return { + scope: { + id: '@', + label: '@', + icon: '@', + order: '=' + }, + templateUrl: 'uikit/mw-menu/directives/templates/mw_menu_divider.html' + }; + }); +angular.module('mwUI.Menu') + + .directive('mwMenuToggleActiveClass', ['$rootScope', '$location', '$timeout', function ($rootScope, $location, $timeout) { + return { + scope: { + entry: '=mwMenuToggleActiveClass', + isActive: '&' + }, + link: function (scope, el) { + var setIsActiveState = function () { + $timeout(function () { + var url = $location.url(), + hadClass = el.hasClass('active'); + + if (scope.entry.hasActiveSubEntryOrIsActiveForUrl(url)) { + el.addClass('active'); + } else { + el.removeClass('active'); + } + + if (hadClass !== el.hasClass('active')) { + scope.$emit('menu-toggle-active-class-changed', el.hasClass('active')); + } + }); + }; + + if (scope.entry && scope.entry.get('isActive')) { + scope.$watch(function () { + return scope.entry.get('isActive')(); + }, setIsActiveState); + } + + setIsActiveState(); + $rootScope.$on('menu-toggle-active-class-changed', setIsActiveState); + $rootScope.$on('$locationChangeSuccess', setIsActiveState); + $rootScope.$on('$routeChangeError', setIsActiveState); + } + }; + }]); +angular.module('mwUI.Modal', ['mwUI.i18n', 'mwUI.Toast']); + +angular.module('mwUI.Modal') + + .directive('mwModal', ['mwModalTmpl', function (mwModalTmpl) { + return { + restrict: 'A', + scope: { + title: '@' + }, + transclude: true, + templateUrl: 'uikit/mw-modal/directives/templates/mw_modal.html', + link: function (scope) { + scope.$emit('COMPILE:FINISHED'); + scope.mwModalTmpl = mwModalTmpl; + } + }; + }]); +angular.module('mwUI.Modal') + + .directive('mwModalBody', function () { + return { + transclude: true, + templateUrl: 'uikit/mw-modal/directives/templates/mw_modal_body.html' + }; + }); +angular.module('mwUI.Modal') + + .directive('mwModalConfirm', function () { + return { + restrict: 'A', + transclude: true, + scope: true, + templateUrl: 'uikit/mw-modal/directives/templates/mw_modal_confirm.html', + link: function (scope, elm, attr) { + angular.forEach(['ok', 'cancel'], function (action) { + scope[action] = function () { + scope.$eval(attr[action]); + }; + }); + } + }; + }); +angular.module('mwUI.Modal') + + .directive('mwModalFooter', function () { + return { + transclude: true, + templateUrl: 'uikit/mw-modal/directives/templates/mw_modal_footer.html' + }; + }); + +angular.module('mwUI.Modal') + + .service('Modal', ['$rootScope', '$templateCache', '$document', '$compile', '$controller', '$injector', '$q', '$templateRequest', '$timeout', 'mwModalOptions', 'Toast', function ($rootScope, $templateCache, $document, $compile, $controller, $injector, $q, $templateRequest, $timeout, mwModalOptions, Toast) { + + var _openedModals = []; + + var Modal = function (modalOptions, bootStrapModalOptions) { + var _id = modalOptions.templateUrl, + _scope = modalOptions.scope || $rootScope, + _scopeAttributes = modalOptions.scopeAttributes || {}, + _resolve = modalOptions.resolve || {}, + _controller = modalOptions.controller, + _modalOptions = _.extend(mwModalOptions.getOptions(), modalOptions), + _bootStrapModalOptions = _.extend(_modalOptions.bootStrapModalOptions, bootStrapModalOptions), + _watchers = [], + _modalOpened = false, + _self = this, + _modal, + _usedScope = _scope.$new(), + _usedController, + _bootstrapModal, + _previousFocusedEl; + + var _setAttributes = function (target, attributes) { + if (_.isObject(attributes) && _.isObject(target)) { + for (var key in attributes) { + target[key] = attributes[key]; + } + } + }; + + var _prepareController = function(locals){ + _setAttributes(_usedScope, _scopeAttributes); + + if (_controller) { + locals.$scope = _usedScope; + locals.modalId = _id; + var ctrl = $controller(_controller, locals, true, _modalOptions.controllerAs); + _setAttributes(ctrl.instance, _scopeAttributes); + _usedController = ctrl(); + } + }; + + var _getTemplate = function () { + if (!_id) { + throw new Error('Modal service: templateUrl options is required.'); + } + return $templateRequest(_id); + }; + + var _bindModalCloseEvent = function () { + _bootstrapModal.on('hidden.bs.modal', function () { + _self.destroy(); + }); + }; + + var _destroyOnRouteChange = function () { + var changeLocationOff = $rootScope.$on('$locationChangeStart', function (ev, newUrl) { + if (_bootstrapModal && _modalOpened) { + ev.preventDefault(); + _self.hide().then(function () { + document.location.href = newUrl; + changeLocationOff(); + }); + } else { + changeLocationOff(); + } + }); + }; + + var _setScopeWatcher = function(){ + _watchers.forEach(function(watcher){ + _usedScope.$watch(watcher.expression,watcher.callback); + }); + }; + + var _resolveLocals = function () { + var locals = angular.extend({}, _resolve); + angular.forEach(locals, function (value, key) { + locals[key] = angular.isString(value) ? + $injector.get(value) : + $injector.invoke(value, null, null, key); + }); + locals.$template = _getTemplate(); + return $q.all(locals); + }; + + var _compileTemplate = function (locals) { + _prepareController(locals); + return $compile(locals.$template)(_usedScope); + }; + + var _buildModal = function () { + + var dfd = $q.defer(); + + _resolveLocals().then(function (locals) { + _setScopeWatcher(); + _scopeAttributes.hideModal = function(){ + return _self.hide(); + }; + + _modal = _compileTemplate(locals); + + _usedScope.$on('COMPILE:FINISHED', function () { + _modal.addClass('mw-modal'); + _modal.addClass(_modalOptions.styleClass); + _bootstrapModal = _modal.find('.modal'); + _bootStrapModalOptions.show = false; + + if(!_modalOptions.dismissible){ + _bootStrapModalOptions.backdrop = 'static'; + _bootStrapModalOptions.keyboard = false; + } + + _bootstrapModal.modal(_bootStrapModalOptions); + + // We need to overwrite the the original backdrop method with our own one + // to make it possible to define the element where the backdrop should be placed + // This enables us a backdrop per modal because we are appending the backdrop to the modal + // When opening multiple modals the previous will be covered by the backdrop of the latest opened modal + /* jshint ignore:start */ + if (_bootstrapModal.data()) { + var bootstrapModal = _bootstrapModal.data()['bs.modal'], + $bootstrapBackdrop = bootstrapModal.backdrop; + + bootstrapModal.backdrop = function (callback) { + $bootstrapBackdrop.call(bootstrapModal, callback, $(_modalOptions.holderEl).find('.modal')); + }; + } + /* jshint ignore:end */ + + _bindModalCloseEvent(); + _destroyOnRouteChange(); + dfd.resolve(); + }); + + }.bind(this), function (err) { + dfd.reject(err); + }); + + return dfd.promise; + }; + + this.id = _id; + + this.getScope = function () { + return _usedScope; + }; + + this.watchScope = function(expression, callback){ + _watchers.push({ + expression: expression, + callback: callback + }); + }; + + /** + * + * @ngdoc function + * @name mwModal.Modal#show + * @methodOf mwModal.Modal + * @function + * @description Shows the modal + */ + this.show = function () { + var dfd = $q.defer(); + var $holderEl = angular.element(_modalOptions.holderEl); + if(!$holderEl || $holderEl.length === 0){ + throw new Error('[Modal] no element could be found for the selector string '+_modalOptions.holderEl+'. Make sure that the element exists'); + } + Toast.clear(); + _previousFocusedEl = angular.element(document.activeElement); + $rootScope.$broadcast('$modalOpenStart'); + $rootScope.$broadcast('$modalResolveDependenciesStart'); + _buildModal.call(this).then(function () { + $rootScope.$broadcast('$modalResolveDependenciesSuccess'); + angular.element(_modalOptions.holderEl).append(_modal); + _bootstrapModal.modal('show'); + _modalOpened = true; + _openedModals.push(this); + _bootstrapModal.on('shown.bs.modal', function () { + angular.element(this).find('input:text:visible:first').focus(); + $rootScope.$broadcast('$modalOpenSuccess'); + dfd.resolve(); + }); + if (_previousFocusedEl) { + _bootstrapModal.on('hidden.bs.modal', function () { + _previousFocusedEl.focus(); + }); + } + + }.bind(this), function (err) { + $rootScope.$broadcast('$modalOpenError', err); + dfd.reject(err); + }); + + return dfd.promise; + }; + + this.setScopeAttributes = function (obj) { + _setAttributes(_scopeAttributes, obj); + + $timeout(function () { + if (_usedScope) { + _setAttributes(_usedScope, obj); + } + + if (_usedController) { + _setAttributes(_usedController, obj); + } + }); + }; + + /** + * + * @ngdoc function + * @name mwModal.Modal#hide + * @methodOf mwModal.Modal + * @function + * @description Hides the modal + * @returns {Object} Promise which will be resolved when modal is successfully closed + */ + this.hide = function () { + var dfd = $q.defer(); + + $rootScope.$broadcast('$modalCloseStart'); + if (_bootstrapModal && _modalOpened) { + _bootstrapModal.one('hidden.bs.modal', function () { + _bootstrapModal.off(); + _self.destroy(); + _modalOpened = false; + $rootScope.$broadcast('$modalCloseSuccess'); + dfd.resolve(); + }); + _bootstrapModal.modal('hide'); + } else { + dfd.resolve(); + } + + return dfd.promise; + }; + + /** + * + * @ngdoc function + * @name mwModal.Modal#toggle + * @methodOf mwModal.Modal + * @function + * @description Toggles the modal + * @param {String} modalId Modal identifier + */ + this.toggle = function () { + _bootstrapModal.modal('toggle'); + }; + + /** + * + * @ngdoc function + * @name mwModal.Modal#destroy + * @methodOf mwModal.Modal + * @function + * @description Removes the modal from the dom + */ + this.destroy = function () { + _openedModals = _.without(_openedModals, this); + var toasts = Toast.getToasts(); + toasts.forEach(function (toast) { + if (+new Date() - toast.initDate > 500) { + Toast.removeToast(toast.id); + } + }); + + $timeout(function () { + if (_modal) { + _modal.remove(); + _modalOpened = false; + } + + if (_usedScope) { + _usedScope.$destroy(); + _usedScope = _scope.$new(); + } + + _scopeAttributes = modalOptions.scopeAttributes || {}; + }.bind(this)); + }; + + (function main() { + + _getTemplate(); + + _scope.$on('$destroy', function () { + _self.hide(); + }); + + })(); + + }; + + /** + * + * @ngdoc function + * @name mwModal.Modal#create + * @methodOf mwModal.Modal + * @function + * @description Create and initialize the modal element in the DOM. Available options + * + * - **templateUrl**: URL to a template (_required_) + * - **scope**: scope that should be available in the controller + * - **controller**: controller instance for the modal + * + * @param {Object} modalOptions The options of the modal which are used to instantiate it + * @returns {Object} Modal + */ + this.create = function (modalOptions, bootstrapModalOptions) { + if(modalOptions && modalOptions.el){ + modalOptions.holderEl = modalOptions.el; + window.mwUI.Utils.shims.deprecationWarning('[Modal] The modal options property el was renamed to holderEl'); + } + + if(modalOptions && modalOptions.class){ + modalOptions.styleClass = modalOptions.class; + window.mwUI.Utils.shims.deprecationWarning('[Modal] The modal options property class was renamed to styleClass'); + } + + return new Modal(modalOptions, bootstrapModalOptions); + }; + + this.prepare = function (modalOptions, bootstrapModalOptions) { + return this.create.bind(this, modalOptions, bootstrapModalOptions); + }; + + this.getOpenedModals = function () { + return _openedModals; + }; + }]); + +angular.module('mwUI.Modal') + + .provider('mwModalOptions', function () { + + var _options = { + controllerAs: '$ctrl', + styleClass: '', + holderEl: 'body', + dismissible: true, + bootStrapModalOptions: {} + }; + + this.config = function (options) { + if (_.isObject(options)) { + _.extend(_options, options); + } + }; + + this.$get = function () { + return { + getOptions: function () { + return _.clone(_options); + } + }; + }; + }); +angular.module('mwUI.Modal') + + .provider('mwModalTmpl', function () { + + var _logoPath; + + this.setLogoPath = function (path) { + _logoPath = path; + }; + + this.$get = function () { + return { + getLogoPath: function () { + return _logoPath; + } + }; + }; + }); + +/* jshint ignore:start */ +// This is the orginal bootstrap backdrop implementation with the only +// modification that the element can be defined as parameter where the backdrop should be placed +$.fn.modal.Constructor.prototype.backdrop = function (callback, holderEl) { + var animate = this.$element.hasClass('fade') ? 'fade' : ''; + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate; + + this.$backdrop = $('