2014-09-24 22:06:12 +02:00
|
|
|
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
2014-08-20 21:32:16 +02:00
|
|
|
/* JavaScript for common UI services */
|
|
|
|
|
|
|
|
(function() {
|
2014-09-24 22:06:12 +02:00
|
|
|
'use strict';
|
2014-08-20 21:32:16 +02:00
|
|
|
|
2015-05-06 04:06:13 +02:00
|
|
|
angular.module('SOGo.Common')
|
2015-01-08 17:52:10 +01:00
|
|
|
|
2014-11-23 07:09:04 +01:00
|
|
|
/*
|
|
|
|
* sgFolderTree - Provides hierarchical folders tree
|
2015-05-06 04:06:13 +02:00
|
|
|
* @memberof SOGo.Common
|
2014-11-23 07:09:04 +01:00
|
|
|
* @restrict element
|
2015-01-08 17:52:10 +01:00
|
|
|
* @param {object} sgRoot
|
|
|
|
* @param {object} sgFolder
|
2015-05-06 04:06:13 +02:00
|
|
|
* @param {function} sgSelectFolder
|
2014-11-23 07:09:04 +01:00
|
|
|
* @see https://github.com/marklagendijk/angular-recursion
|
|
|
|
* @example:
|
|
|
|
|
2015-01-08 17:52:10 +01:00
|
|
|
<sg-folder-tree ng-repeat="folder in folders track by folder.id"
|
|
|
|
sg-root="account"
|
|
|
|
sg-folder="folder"
|
|
|
|
sg-select-folder="setCurrentFolder"><!-- tree --></sg-folder-tree>
|
2014-11-23 07:09:04 +01:00
|
|
|
*/
|
|
|
|
.directive('sgFolderTree', function(RecursionHelper) {
|
|
|
|
return {
|
|
|
|
restrict: 'E',
|
|
|
|
scope: {
|
|
|
|
root: '=sgRoot',
|
|
|
|
folder: '=sgFolder',
|
2015-01-08 17:52:10 +01:00
|
|
|
selectFolder: '=sgSelectFolder'
|
2014-11-23 07:09:04 +01:00
|
|
|
},
|
2015-05-06 04:06:13 +02:00
|
|
|
template: [
|
|
|
|
'<md-list-item>',
|
|
|
|
' <md-item-content layout="row" layout-align="start center" flex>',
|
|
|
|
' <i class="md-icon-folder"></i>',
|
|
|
|
' <button class="md-button md-flex sg-item-name">{{folder.name}}</button>',
|
2015-01-29 17:09:31 +01:00
|
|
|
' <md-input-container class="md-flex md-tile-content ng-hide">'+
|
2015-05-06 04:06:13 +02:00
|
|
|
' <input type="text"',
|
|
|
|
' ng-model="folder.name"',
|
|
|
|
' ng-blur="save()"',
|
|
|
|
' sg-enter="save()"',
|
|
|
|
' sg-escape="revert()"/>',
|
|
|
|
' </md-input-container>',
|
|
|
|
' <span class="icon ng-hide" ng-cloak="ng-cloak">',
|
|
|
|
' <a class="icon" href="#"',
|
|
|
|
' dropdown-toggle="#folderProperties"',
|
|
|
|
' options="align:right"><i class="md-icon-more-vert"></i></a>',
|
|
|
|
' </span>',
|
|
|
|
' </md-item-content>',
|
|
|
|
'</md-list-item>',
|
|
|
|
'<sg-folder-tree ng-repeat="child in folder.children track by child.path"',
|
|
|
|
' sg-root="root"',
|
|
|
|
' sg-folder="child"',
|
|
|
|
' sg-select-folder="selectFolder"></sg-folder-tree>'
|
|
|
|
].join(''),
|
2014-11-23 07:09:04 +01:00
|
|
|
compile: function(element) {
|
|
|
|
return RecursionHelper.compile(element, function(scope, iElement, iAttrs, controller, transcludeFn) {
|
2015-01-29 17:09:31 +01:00
|
|
|
var level, link, inputContainer, input, edit;
|
2015-01-08 17:52:10 +01:00
|
|
|
|
2014-11-23 07:09:04 +01:00
|
|
|
// Set CSS class for folder hierarchical level
|
2015-01-08 17:52:10 +01:00
|
|
|
level = scope.folder.path.split('/').length - 1;
|
2015-01-30 20:54:55 +01:00
|
|
|
angular.element(iElement.find('i')[0]).addClass('sg-child-level-' + level);
|
2014-11-23 07:09:04 +01:00
|
|
|
|
2015-01-08 17:52:10 +01:00
|
|
|
// Select dynamic elements
|
2015-01-30 20:54:55 +01:00
|
|
|
link = angular.element(iElement.find('button')[0]);
|
2015-01-29 17:09:31 +01:00
|
|
|
inputContainer = angular.element(iElement.find('md-input-container'));
|
2015-01-08 17:52:10 +01:00
|
|
|
input = iElement.find('input')[0];
|
|
|
|
|
|
|
|
var edit = function() {
|
|
|
|
link.addClass('ng-hide');
|
2015-01-29 17:09:31 +01:00
|
|
|
inputContainer.removeClass('ng-hide');
|
2015-01-08 17:52:10 +01:00
|
|
|
input.focus();
|
|
|
|
input.select();
|
|
|
|
};
|
|
|
|
|
|
|
|
// jQLite listeners
|
|
|
|
|
|
|
|
// click - call the directive's external function sgSelectFolder
|
2014-11-23 07:09:04 +01:00
|
|
|
link.on('click', function() {
|
2015-01-08 17:52:10 +01:00
|
|
|
var list, items;
|
|
|
|
if (!scope.mode.selected) {
|
|
|
|
list = iElement.parent();
|
2015-01-09 21:58:11 +01:00
|
|
|
while (list[0].tagName != 'MD-LIST') {
|
2015-01-08 17:52:10 +01:00
|
|
|
list = list.parent();
|
|
|
|
}
|
2015-04-21 19:40:39 +02:00
|
|
|
items = list.find('md-list-item');
|
2015-01-08 17:52:10 +01:00
|
|
|
|
|
|
|
// Highlight element as "loading"
|
2015-01-29 17:09:31 +01:00
|
|
|
items.removeClass('sg-active');
|
|
|
|
items.removeClass('sg-loading');
|
2015-04-21 19:40:39 +02:00
|
|
|
angular.element(iElement.find('md-list-item')[0]).addClass('sg-loading');
|
2014-11-23 07:09:04 +01:00
|
|
|
|
2015-01-08 17:52:10 +01:00
|
|
|
// Call external function
|
|
|
|
scope.selectFolder(scope.root, scope.folder);
|
|
|
|
}
|
|
|
|
});
|
2014-11-23 07:09:04 +01:00
|
|
|
|
2015-01-08 17:52:10 +01:00
|
|
|
// dblclick - enter edit mode
|
|
|
|
link.on('dblclick', function() {
|
|
|
|
edit();
|
2014-11-23 07:09:04 +01:00
|
|
|
});
|
2015-01-08 17:52:10 +01:00
|
|
|
|
|
|
|
// Broadcast listeners
|
|
|
|
|
|
|
|
// sgSelectFolder - broadcasted when the folder has been successfully loaded
|
2014-11-23 07:09:04 +01:00
|
|
|
scope.$on('sgSelectFolder', function(event, folderId) {
|
|
|
|
if (folderId == scope.folder.id) {
|
2015-01-08 17:52:10 +01:00
|
|
|
var list = iElement.parent(),
|
|
|
|
items;
|
|
|
|
|
|
|
|
scope.mode.selected = true;
|
2015-01-09 21:58:11 +01:00
|
|
|
while (list[0].tagName != 'MD-LIST') {
|
2015-01-08 17:52:10 +01:00
|
|
|
list = list.parent();
|
|
|
|
}
|
2015-04-21 19:40:39 +02:00
|
|
|
items = list.find('md-list-item');
|
2014-11-23 07:09:04 +01:00
|
|
|
|
|
|
|
// Hightlight element as "selected"
|
2015-04-21 19:40:39 +02:00
|
|
|
angular.element(iElement.find('md-list-item')[0]).addClass('sg-active');
|
|
|
|
angular.element(iElement.find('md-list-item')[0]).removeClass('sg-loading');
|
2014-11-23 07:09:04 +01:00
|
|
|
|
|
|
|
// Show options button
|
|
|
|
angular.forEach(items, function(element) {
|
|
|
|
var li = angular.element(element);
|
|
|
|
var spans = li.find('span');
|
2015-01-29 17:09:31 +01:00
|
|
|
angular.element(spans[0]).addClass('ng-hide');
|
2014-11-23 07:09:04 +01:00
|
|
|
});
|
2015-01-29 17:09:31 +01:00
|
|
|
angular.element(iElement.find('span')[0]).removeClass('ng-hide');
|
2014-11-23 07:09:04 +01:00
|
|
|
}
|
2015-01-08 17:52:10 +01:00
|
|
|
else {
|
|
|
|
scope.mode.selected = false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// sgEditFolder - broadcasted when the user wants to rename the folder
|
|
|
|
scope.$on('sgEditFolder', function(event, folderId) {
|
|
|
|
if (scope.mode.selected && folderId == scope.folder.id) {
|
|
|
|
edit();
|
|
|
|
}
|
2014-11-23 07:09:04 +01:00
|
|
|
});
|
2015-01-08 17:52:10 +01:00
|
|
|
|
|
|
|
// Local scope variables and functions
|
|
|
|
|
|
|
|
scope.mode = { selected: false };
|
|
|
|
|
|
|
|
scope.save = function() {
|
|
|
|
if (link.hasClass('ng-hide')) {
|
2015-01-29 17:09:31 +01:00
|
|
|
inputContainer.addClass('ng-hide');
|
2015-01-08 17:52:10 +01:00
|
|
|
link.removeClass('ng-hide');
|
|
|
|
scope.$emit('sgSaveFolder', scope.folder.id);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
scope.revert = function() {
|
|
|
|
scope.$emit('sgRevertFolder', scope.folder.id);
|
|
|
|
};
|
2014-11-23 07:09:04 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
})
|
|
|
|
|
|
|
|
/*
|
|
|
|
* sgDropdownContentToggle - Provides dropdown content functionality
|
2015-05-06 04:06:13 +02:00
|
|
|
* @memberof SOGo.Common
|
2014-11-23 07:09:04 +01:00
|
|
|
* @restrict class or attribute
|
|
|
|
* @see https://github.com/pineconellc/angular-foundation/blob/master/src/dropdownToggle/dropdownToggle.js
|
|
|
|
* @example:
|
|
|
|
|
|
|
|
<a dropdown-toggle="#dropdown-content">My Dropdown Content</a>
|
|
|
|
<div id="dropdown-content" class="sg-dropdown-content">
|
|
|
|
<div>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<p>World!</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
*/
|
2015-03-09 21:21:31 +01:00
|
|
|
/*
|
2014-11-11 16:18:45 +01:00
|
|
|
.directive('sgDropdownContentToggle', ['$document', '$window', '$location', '$position', function ($document, $window, $location, $position) {
|
|
|
|
var openElement = null,
|
|
|
|
closeMenu = angular.noop;
|
|
|
|
return {
|
|
|
|
restrict: 'CA', // class and attribute
|
|
|
|
scope: {
|
|
|
|
dropdownToggle: '@sgDropdownContentToggle'
|
|
|
|
},
|
|
|
|
link: function(scope, element, attrs, controller) {
|
|
|
|
var dropdown = angular.element($document[0].querySelector(scope.dropdownToggle));
|
|
|
|
|
|
|
|
scope.$watch('$location.path', function() {
|
|
|
|
closeMenu();
|
|
|
|
});
|
|
|
|
element.bind('click', function(event) {
|
|
|
|
var elementWasOpen = (element === openElement);
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
if (!!openElement) {
|
|
|
|
closeMenu();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
|
|
|
|
dropdown.css('display', 'block');
|
|
|
|
|
|
|
|
var offset = $position.offset(element),
|
|
|
|
dropdownParentOffset = $position.offset(angular.element(dropdown[0].offsetParent)),
|
|
|
|
dropdownWidth = dropdown.prop('offsetWidth'),
|
|
|
|
dropdownHeight = dropdown.prop('offsetHeight'),
|
|
|
|
dropdownCss = {},
|
|
|
|
left = Math.round(offset.left - dropdownParentOffset.left),
|
|
|
|
rightThreshold = $window.innerWidth - dropdownWidth - 8,
|
|
|
|
nub = angular.element(dropdown.children()[0]),
|
|
|
|
nubCss = {};
|
|
|
|
|
|
|
|
if (left > rightThreshold) {
|
|
|
|
// There's more place on the left side of the element
|
|
|
|
left = rightThreshold;
|
|
|
|
dropdown.removeClass('left').addClass('right');
|
|
|
|
nub.removeClass('left').addClass('right');
|
|
|
|
}
|
|
|
|
|
|
|
|
dropdownCss.position = null;
|
|
|
|
dropdownCss['max-width'] = null;
|
|
|
|
// Place a third of the dropdown above the element
|
|
|
|
dropdownCss.top = Math.round(offset.top + offset.height / 2 - dropdownHeight / 3),
|
|
|
|
dropdownCss.left = Math.round(offset.left + offset.width + 10);
|
|
|
|
|
|
|
|
if (dropdownCss.top + dropdownHeight > $window.innerHeight) {
|
|
|
|
// Position dropdown at the very top of the window
|
|
|
|
dropdownCss.top = $window.innerHeight - dropdownHeight - 5;
|
|
|
|
if (dropdownHeight > $window.innerHeight) {
|
|
|
|
// Resize height of dropdown to fit window
|
2014-11-12 16:44:43 +01:00
|
|
|
dropdownCss.top = 5;
|
2014-11-11 16:18:45 +01:00
|
|
|
dropdownCss.height = ($window.innerHeight - 10) + 'px';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Place nub beside the element
|
|
|
|
nubCss.top = Math.round(offset.top - dropdownCss.top + offset.height / 2 - nub.prop('offsetHeight') / 2) + 'px';
|
|
|
|
|
|
|
|
// Apply CSS
|
|
|
|
dropdownCss.top += 'px';
|
|
|
|
dropdownCss.left += 'px';
|
|
|
|
dropdown.css(dropdownCss);
|
|
|
|
nub.css(nubCss);
|
|
|
|
|
|
|
|
openElement = element;
|
|
|
|
closeMenu = function (event) {
|
|
|
|
if (event) {
|
|
|
|
// We ignore clicks that occur inside the dropdown content element, unless it's a button
|
|
|
|
var target = angular.element(event.target),
|
|
|
|
ignoreClick = false;
|
|
|
|
while (target[0]) {
|
|
|
|
if (target[0].tagName == 'BUTTON') break;
|
|
|
|
if (target[0] == dropdown[0]) {
|
|
|
|
ignoreClick = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
target = target.parent();
|
|
|
|
}
|
|
|
|
if (ignoreClick) return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$document.unbind('click', closeMenu);
|
|
|
|
dropdown.css('display', 'none');
|
|
|
|
closeMenu = angular.noop;
|
|
|
|
openElement = null;
|
|
|
|
};
|
|
|
|
$document.bind('click', closeMenu);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (dropdown) {
|
|
|
|
dropdown.css('display', 'none');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}])
|
2015-03-09 21:21:31 +01:00
|
|
|
*/
|
2014-11-11 16:18:45 +01:00
|
|
|
|
2014-11-23 07:09:04 +01:00
|
|
|
/*
|
2015-05-06 04:06:13 +02:00
|
|
|
* UserTypeahead - Typeahead of users, used internally by sgSubscribe
|
2014-11-23 07:09:04 +01:00
|
|
|
* @restrict attribute
|
|
|
|
* @param {String} sgModel - the folder type
|
|
|
|
* @param {Function} sgSubscribeOnSelect - the function to call when subscribing to a folder
|
|
|
|
* @see https://github.com/pineconellc/angular-foundation/blob/master/src/typeahead/typeahead.js
|
|
|
|
* @example:
|
2014-11-11 16:18:45 +01:00
|
|
|
|
2014-11-23 07:09:04 +01:00
|
|
|
<div sg-subscribe="contact" sg-subscribe-on-select="subscribeToFolder"></div>
|
|
|
|
*/
|
2015-05-06 04:06:13 +02:00
|
|
|
.directive('sgUserTypeahead', ['$parse', '$q', '$timeout', 'User', function($parse, $q, $timeout, User) {
|
2014-11-11 16:18:45 +01:00
|
|
|
return {
|
|
|
|
restrict: 'A',
|
|
|
|
require: 'ngModel',
|
|
|
|
link: function(originalScope, element, attrs, controller) {
|
|
|
|
|
|
|
|
var hasFocus,
|
|
|
|
scope,
|
|
|
|
resetMatches,
|
|
|
|
getMatchesAsync,
|
|
|
|
// Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
|
|
|
|
timeoutPromise,
|
|
|
|
// Minimal number of characters that needs to be entered before typeahead kicks-in
|
|
|
|
minSearch = originalScope.$eval(attrs.sgSubscribeMinLength) || 3,
|
|
|
|
// Minimal wait time after last character typed before typehead kicks-in
|
|
|
|
waitTime = originalScope.$eval(attrs.sgSubscribeWaitMs) || 500,
|
|
|
|
// Binding to a variable that indicates if matches are being retrieved asynchronously
|
|
|
|
isLoadingSetter = $parse(attrs.sgSubscribeLoading).assign || angular.noop;
|
|
|
|
|
|
|
|
// Create a child scope for the typeahead directive so we are not polluting original scope
|
|
|
|
// with typeahead-specific data (users, query, etc.)
|
|
|
|
scope = originalScope.$new();
|
|
|
|
originalScope.$on('$destroy', function(){
|
|
|
|
scope.$destroy();
|
|
|
|
});
|
|
|
|
|
|
|
|
resetMatches = function() {
|
|
|
|
originalScope.users = [];
|
|
|
|
originalScope.selectedUser = undefined;
|
|
|
|
scope.activeIdx = -1;
|
|
|
|
};
|
|
|
|
|
|
|
|
getMatchesAsync = function(inputValue) {
|
|
|
|
isLoadingSetter(originalScope, true);
|
|
|
|
$q.when(User.$filter(inputValue)).then(function(matches) {
|
|
|
|
// It might happen that several async queries were in progress if a user were typing fast
|
|
|
|
// but we are interested only in responses that correspond to the current view value
|
|
|
|
if (inputValue === controller.$viewValue && hasFocus) {
|
|
|
|
if (matches.length > 0) {
|
|
|
|
scope.activeIdx = 0;
|
|
|
|
originalScope.users = matches;
|
|
|
|
originalScope.query = inputValue; // for the hightlighter
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
resetMatches();
|
|
|
|
}
|
|
|
|
isLoadingSetter(originalScope, false);
|
|
|
|
}
|
|
|
|
}, function(){
|
|
|
|
resetMatches();
|
|
|
|
isLoadingSetter(originalScope, false);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
resetMatches();
|
|
|
|
|
|
|
|
// We need to propagate user's query so we can higlight matches
|
|
|
|
originalScope.query = undefined;
|
|
|
|
|
|
|
|
// Plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
|
|
|
|
// $parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
|
|
|
|
controller.$parsers.unshift(function (inputValue) {
|
|
|
|
if (inputValue && inputValue.length >= minSearch) {
|
|
|
|
if (waitTime > 0) {
|
|
|
|
if (timeoutPromise) {
|
|
|
|
$timeout.cancel(timeoutPromise); // cancel previous timeout
|
|
|
|
}
|
|
|
|
timeoutPromise = $timeout(function() {
|
|
|
|
getMatchesAsync(inputValue);
|
|
|
|
}, waitTime);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
getMatchesAsync(inputValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
isLoadingSetter(originalScope, false);
|
|
|
|
resetMatches();
|
|
|
|
}
|
|
|
|
return inputValue;
|
|
|
|
});
|
|
|
|
|
|
|
|
element.bind('blur', function (evt) {
|
|
|
|
hasFocus = false;
|
|
|
|
});
|
|
|
|
|
|
|
|
element.bind('focus', function (evt) {
|
|
|
|
hasFocus = true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
2015-03-18 18:55:36 +01:00
|
|
|
}])
|
|
|
|
|
2014-08-20 21:32:16 +02:00
|
|
|
})();
|