Singleton manager + Revert back to old outside click handling. New system unable cope with internal clicks with ng-if

This commit is contained in:
Mo Bitar
2017-12-26 15:09:02 -06:00
parent fc4abbbaf6
commit b124332619
9 changed files with 156 additions and 105 deletions

View File

@@ -8,6 +8,11 @@ angular.module('app.frontend')
$rootScope.$broadcast('new-update-available', version);
}
/* Used to avoid circular dependencies where syncManager cannot be imported but rootScope can */
$rootScope.sync = function() {
syncManager.sync();
}
$rootScope.lockApplication = function() {
// Reloading wipes current objects from memory
window.location.reload();

View File

@@ -7,11 +7,11 @@ angular.module('app.frontend')
return domain;
}
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager);
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager) {
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager);
}
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager) {
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager) {
this.loadInitialData = function() {
var userData = storageManager.getItem("user");
@@ -290,5 +290,23 @@ angular.module('app.frontend')
this._authParams = null;
}
}
/* User Preferences */
let prefsContentType = "SN|UserPreferences";
singletonManager.registerSingleton({content_type: prefsContentType}, (resolvedSingleton) => {
console.log("AuthManager received resolved", resolvedSingleton);
this.userPreferences = resolvedSingleton;
}, () => {
// Safe to create. Create and return object.
var prefs = new Item({content_type: prefsContentType});
modelManager.addItem(prefs);
prefs.setDirty(true);
console.log("Created new prefs", prefs);
$rootScope.sync();
return prefs;
});
}
});

View File

@@ -1,102 +1,27 @@
// via https://github.com/IamAdamJowett/angular-click-outside
angular
.module('app.frontend')
.directive('clickOutside', ['$document', '$parse', '$timeout', function($document, $parse, $timeout) {
.directive('clickOutside', ['$document', function($document) {
return {
restrict: 'A',
replace: false,
link: function($scope, elem, attr) {
$timeout(() => {
// postpone linking to next digest to allow for unique id generation
var classList = (attr.outsideIfNot !== undefined) ? attr.outsideIfNot.split(/[ ,]+/) : [],
fn;
link : function($scope, $element, attrs) {
function eventHandler(e) {
var i,
element,
r,
id,
classNames,
l;
var didApplyClickOutside = false;
// check if our element already hidden and abort if so
if (angular.element(elem).hasClass("ng-hide")) {
return;
}
// if there is no click target, no point going on
if (!e || !e.target) {
return;
}
// loop through the available elements, looking for classes in the class list that might match and so will eat
for (element = e.target; element; element = element.parentNode) {
// check if the element is the same element the directive is attached to and exit if so (props @CosticaPuntaru)
if (element === elem[0]) {
return;
}
// now we have done the initial checks, start gathering id's and classes
id = element.id,
classNames = element.className,
l = classList.length;
// Unwrap SVGAnimatedString classes
if (classNames && classNames.baseVal !== undefined) {
classNames = classNames.baseVal;
}
// if there are no class names on the element clicked, skip the check
if (classNames || id) {
// loop through the elements id's and classnames looking for exceptions
for (i = 0; i < l; i++) {
//prepare regex for class word matching
r = new RegExp('\\b' + classList[i] + '\\b');
// check for exact matches on id's or classes, but only if they exist in the first place
if ((id !== undefined && id === classList[i]) || (classNames && r.test(classNames))) {
// now let's exit out as it is an element that has been defined as being ignored for clicking outside
return;
}
}
}
}
// if we have got this far, then we are good to go with processing the command passed in via the click-outside attribute
$timeout(function() {
fn = $parse(attr['clickOutside']);
fn($scope, { event: e });
});
$element.bind('click', function(e) {
didApplyClickOutside = false;
if (attrs.isOpen) {
e.stopPropagation();
}
});
// if the devices has a touchscreen, listen for this event
if (_hasTouch()) {
$document.on('touchstart', eventHandler);
$document.bind('click', function() {
if(!didApplyClickOutside) {
$scope.$apply(attrs.clickOutside);
didApplyClickOutside = true;
}
})
// still listen for the click event even if there is touch to cater for touchscreen laptops
$document.on('click', eventHandler);
// when the scope is destroyed, clean up the documents event handlers as we don't want it hanging around
$scope.$on('$destroy', function() {
if (_hasTouch()) {
$document.off('touchstart', eventHandler);
}
$document.off('click', eventHandler);
});
/**
* @description Private function to attempt to figure out if we are on a touch device
* @private
**/
function _hasTouch() {
// works on most browsers, IE10/11 and Surface
return 'ontouchstart' in window || navigator.maxTouchPoints;
};
});
}
}
}
}]);
}]);

View File

@@ -0,0 +1,101 @@
/*
The SingletonManager allows controllers to register an item as a singleton, which means only one instance of that model
should exist, both on the server and on the client. When the SingletonManager detects multiple items matching the singleton predicate,
the oldest ones will be deleted, leaving the newest ones
*/
class SingletonManager {
constructor($rootScope, modelManager) {
this.$rootScope = $rootScope;
this.modelManager = modelManager;
this.singletonHandlers = [];
$rootScope.$on("initial-data-loaded", (event, data) => {
this.resolveSingletons(modelManager.allItems, true);
})
$rootScope.$on("sync:completed", (event, data) => {
console.log("Sync completed", data);
this.resolveSingletons(data.retrievedItems || []);
})
setTimeout(function () {
var userPrefsTotal = modelManager.itemsForContentType("SN|UserPreferences");
console.log("All extant prefs", userPrefsTotal);
}, 1000);
}
registerSingleton(predicate, resolveCallback, createBlock) {
/*
predicate: a key/value pair that specifies properties that should match in order for an item to be considered a predicate
resolveCallback: called when one or more items are deleted and a new item becomes the reigning singleton
createBlock: called when a sync is complete and no items are found. The createBlock should create the item and return it.
*/
this.singletonHandlers.push({
predicate: predicate,
resolutionCallback: resolveCallback,
createBlock: createBlock
});
}
resolveSingletons(retrievedItems, initialLoad) {
for(var singletonHandler of this.singletonHandlers) {
var predicate = singletonHandler.predicate;
var singletonItems = this.filterItemsWithPredicate(retrievedItems, predicate);
if(singletonItems.length > 0) {
// Check local inventory and make sure only 1 similar item exists. If more than 1, delete oldest
var allExtantItemsMatchingPredicate = this.filterItemsWithPredicate(this.modelManager.allItems, predicate);
if(allExtantItemsMatchingPredicate.length >= 2) {
// Purge old ones
var sorted = allExtantItemsMatchingPredicate.sort((a, b) => {
return a.updated_at < b.updated_at;
})
var toDelete = sorted.slice(1, sorted.length);
for(var d of toDelete) {
this.modelManager.setItemToBeDeleted(d);
}
this.$rootScope.sync();
// Send remaining item to callback
var singleton = sorted[0];
singletonHandler.singleton = singleton;
singletonHandler.resolutionCallback(singleton);
} else if(allExtantItemsMatchingPredicate.length == 1) {
if(!singletonHandler.singleton) {
// Not yet notified interested parties of object
var singleton = allExtantItemsMatchingPredicate[0];
singletonHandler.singleton = singleton;
singletonHandler.resolutionCallback(singleton);
}
}
} else {
// Retrieved items does not include any items of interest. If we don't have a singleton registered to this handler,
// we need to create one. Only do this on actual sync completetions and not on initial data load. Because we want
// to get the latest from the server before making the decision to create a new item
if(!singletonHandler.singleton && !initialLoad) {
var item = singletonHandler.createBlock();
singletonHandler.singleton = item;
singletonHandler.resolutionCallback(item);
}
}
}
}
filterItemsWithPredicate(items, predicate) {
return items.filter((candidate) => {
for(var key in predicate) {
if(candidate[key] != predicate[key]) {
return false;
}
}
return true;
})
}
}
angular.module('app.frontend').service('singletonManager', SingletonManager);

View File

@@ -62,6 +62,8 @@ class SyncManager {
}
}
this.$rootScope.$broadcast("sync:completed", {});
if(callback) {
callback({success: true});
}
@@ -319,10 +321,10 @@ class SyncManager {
this.$rootScope.$broadcast("major-data-change");
}
this.allRetreivedItems = [];
this.callQueuedCallbacksAndCurrent(callback, response);
this.$rootScope.$broadcast("sync:completed");
this.$rootScope.$broadcast("sync:completed", {retrievedItems: this.allRetreivedItems});
this.allRetreivedItems = [];
}
}.bind(this);

View File

@@ -1,4 +1,4 @@
.room-item{"ng-repeat" => "room in rooms", "ng-click" => "selectRoom(room)", "click-outside" => "hideRoom(room)"}
.room-item{"ng-repeat" => "room in rooms", "ng-click" => "selectRoom(room)", "click-outside" => "hideRoom(room)", "is-open" => "room.show && room.active"}
%img.icon{"ng-src" => "{{room.package_info.icon_bar}}"}
.label {{room.name}}
.room-container.panel-right{"ng-if" => "room.show && room.active", "ng-attr-id" => "component-{{room.uuid}}"}

View File

@@ -13,7 +13,7 @@
%input.tags-input{"ng-if" => "!(ctrl.tagsComponent && ctrl.tagsComponent.active)", "type" => "text", "ng-keyup" => "$event.keyCode == 13 && $event.target.blur();",
"ng-model" => "ctrl.tagsString", "placeholder" => "#tags", "ng-blur" => "ctrl.updateTagsFromTagsString($event, ctrl.tagsString)"}
%ul.section-menu-bar{"ng-if" => "ctrl.note"}
%li{"ng-class" => "{'selected' : ctrl.showMenu}", "click-outside" => "ctrl.showMenu = false;"}
%li{"ng-class" => "{'selected' : ctrl.showMenu}", "click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"}
%label{"ng-click" => "ctrl.showMenu = !ctrl.showMenu; ctrl.showExtensions = false; ctrl.showEditorMenu = false;"} Menu
%ul.dropdown-menu.sectioned-menu{"ng-if" => "ctrl.showMenu"}
@@ -37,11 +37,11 @@
%li{"ng-if" => "ctrl.hasDisabledStackComponents()"}
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledStackComponents()"} Restore Disabled Components
%li{"ng-class" => "{'selected' : ctrl.showEditorMenu}", "click-outside" => "ctrl.showEditorMenu = false;"}
%li{"ng-class" => "{'selected' : ctrl.showEditorMenu}", "click-outside" => "ctrl.showEditorMenu = false;", "is-open" => "ctrl.showEditorMenu"}
%label{"ng-click" => "ctrl.showEditorMenu = !ctrl.showEditorMenu; ctrl.showMenu = false; ctrl.showExtensions = false;"} Editor
%editor-menu{"ng-if" => "ctrl.showEditorMenu", "callback" => "ctrl.selectedEditor", "selected-editor" => "ctrl.editorComponent"}
%li{"ng-class" => "{'selected' : ctrl.showExtensions}", "ng-if" => "ctrl.hasAvailableExtensions()", "click-outside" => "ctrl.showExtensions = false;"}
%li{"ng-class" => "{'selected' : ctrl.showExtensions}", "ng-if" => "ctrl.hasAvailableExtensions()", "click-outside" => "ctrl.showExtensions = false;", "is-open" => "ctrl.showExtensions"}
%label{"ng-click" => "ctrl.showExtensions = !ctrl.showExtensions; ctrl.showMenu = false; ctrl.showEditorMenu = false;"} Actions
%contextual-extensions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}

View File

@@ -1,10 +1,10 @@
#footer-bar
.pull-left
.footer-bar-link{"click-outside" => "ctrl.showAccountMenu = false;"}
.footer-bar-link{"click-outside" => "ctrl.showAccountMenu = false;", "is-open" => "ctrl.showAccountMenu"}
%a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account
%account-menu{"ng-if" => "ctrl.showAccountMenu", "on-successful-auth" => "ctrl.onAuthSuccess"}
.footer-bar-link{"click-outside" => "ctrl.showExtensionsMenu = false;"}
.footer-bar-link{"click-outside" => "ctrl.showExtensionsMenu = false;", "is-open" => "ctrl.showExtensionsMenu"}
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
%global-extensions-menu{"ng-if" => "ctrl.showExtensionsMenu"}

View File

@@ -9,11 +9,11 @@
#search-clear-button{"ng-if" => "ctrl.noteFilter.text", "ng-click" => "ctrl.noteFilter.text = ''; ctrl.filterTextChanged()"} ✕
%ul.section-menu-bar#notes-menu-bar
%li.item-with-subtitle{"ng-class" => "{'selected' : ctrl.showMenu}"}
.wrapper{"ng-click" => "ctrl.showMenu = !ctrl.showMenu"}
.wrapper{"ng-click" => "ctrl.showMenu = !ctrl.showMenu", "click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"}
%label Options
.subtitle {{ctrl.optionsSubtitle()}}
.sectioned-menu.dropdown-menu{"ng-if" => "ctrl.showMenu", "click-outside" => "ctrl.showMenu = false;"}
.sectioned-menu.dropdown-menu{"ng-if" => "ctrl.showMenu"}
%ul
.header
.title Sort by