Singleton manager + Revert back to old outside click handling. New system unable cope with internal clicks with ng-if
This commit is contained in:
@@ -8,6 +8,11 @@ angular.module('app.frontend')
|
|||||||
$rootScope.$broadcast('new-update-available', version);
|
$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() {
|
$rootScope.lockApplication = function() {
|
||||||
// Reloading wipes current objects from memory
|
// Reloading wipes current objects from memory
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ angular.module('app.frontend')
|
|||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$get = function($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);
|
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() {
|
this.loadInitialData = function() {
|
||||||
var userData = storageManager.getItem("user");
|
var userData = storageManager.getItem("user");
|
||||||
@@ -290,5 +290,23 @@ angular.module('app.frontend')
|
|||||||
this._authParams = null;
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,102 +1,27 @@
|
|||||||
// via https://github.com/IamAdamJowett/angular-click-outside
|
|
||||||
|
|
||||||
angular
|
angular
|
||||||
.module('app.frontend')
|
.module('app.frontend')
|
||||||
.directive('clickOutside', ['$document', '$parse', '$timeout', function($document, $parse, $timeout) {
|
.directive('clickOutside', ['$document', function($document) {
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
replace: false,
|
replace: false,
|
||||||
link: function($scope, elem, attr) {
|
link : function($scope, $element, attrs) {
|
||||||
$timeout(() => {
|
|
||||||
// postpone linking to next digest to allow for unique id generation
|
|
||||||
var classList = (attr.outsideIfNot !== undefined) ? attr.outsideIfNot.split(/[ ,]+/) : [],
|
|
||||||
fn;
|
|
||||||
|
|
||||||
function eventHandler(e) {
|
var didApplyClickOutside = false;
|
||||||
var i,
|
|
||||||
element,
|
|
||||||
r,
|
|
||||||
id,
|
|
||||||
classNames,
|
|
||||||
l;
|
|
||||||
|
|
||||||
// check if our element already hidden and abort if so
|
$element.bind('click', function(e) {
|
||||||
if (angular.element(elem).hasClass("ng-hide")) {
|
didApplyClickOutside = false;
|
||||||
return;
|
if (attrs.isOpen) {
|
||||||
}
|
e.stopPropagation();
|
||||||
|
|
||||||
// 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 });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// if the devices has a touchscreen, listen for this event
|
$document.bind('click', function() {
|
||||||
if (_hasTouch()) {
|
if(!didApplyClickOutside) {
|
||||||
$document.on('touchstart', eventHandler);
|
$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;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}]);
|
||||||
}]);
|
|
||||||
|
|||||||
101
app/assets/javascripts/app/services/singletonManager.js
Normal file
101
app/assets/javascripts/app/services/singletonManager.js
Normal 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);
|
||||||
@@ -62,6 +62,8 @@ class SyncManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$rootScope.$broadcast("sync:completed", {});
|
||||||
|
|
||||||
if(callback) {
|
if(callback) {
|
||||||
callback({success: true});
|
callback({success: true});
|
||||||
}
|
}
|
||||||
@@ -319,10 +321,10 @@ class SyncManager {
|
|||||||
this.$rootScope.$broadcast("major-data-change");
|
this.$rootScope.$broadcast("major-data-change");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.allRetreivedItems = [];
|
|
||||||
|
|
||||||
this.callQueuedCallbacksAndCurrent(callback, response);
|
this.callQueuedCallbacksAndCurrent(callback, response);
|
||||||
this.$rootScope.$broadcast("sync:completed");
|
this.$rootScope.$broadcast("sync:completed", {retrievedItems: this.allRetreivedItems});
|
||||||
|
|
||||||
|
this.allRetreivedItems = [];
|
||||||
}
|
}
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
|
|
||||||
|
|||||||
@@ -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}}"}
|
%img.icon{"ng-src" => "{{room.package_info.icon_bar}}"}
|
||||||
.label {{room.name}}
|
.label {{room.name}}
|
||||||
.room-container.panel-right{"ng-if" => "room.show && room.active", "ng-attr-id" => "component-{{room.uuid}}"}
|
.room-container.panel-right{"ng-if" => "room.show && room.active", "ng-attr-id" => "component-{{room.uuid}}"}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
%input.tags-input{"ng-if" => "!(ctrl.tagsComponent && ctrl.tagsComponent.active)", "type" => "text", "ng-keyup" => "$event.keyCode == 13 && $event.target.blur();",
|
%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)"}
|
"ng-model" => "ctrl.tagsString", "placeholder" => "#tags", "ng-blur" => "ctrl.updateTagsFromTagsString($event, ctrl.tagsString)"}
|
||||||
%ul.section-menu-bar{"ng-if" => "ctrl.note"}
|
%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
|
%label{"ng-click" => "ctrl.showMenu = !ctrl.showMenu; ctrl.showExtensions = false; ctrl.showEditorMenu = false;"} Menu
|
||||||
|
|
||||||
%ul.dropdown-menu.sectioned-menu{"ng-if" => "ctrl.showMenu"}
|
%ul.dropdown-menu.sectioned-menu{"ng-if" => "ctrl.showMenu"}
|
||||||
@@ -37,11 +37,11 @@
|
|||||||
%li{"ng-if" => "ctrl.hasDisabledStackComponents()"}
|
%li{"ng-if" => "ctrl.hasDisabledStackComponents()"}
|
||||||
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledStackComponents()"} Restore Disabled Components
|
%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
|
%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"}
|
%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
|
%label{"ng-click" => "ctrl.showExtensions = !ctrl.showExtensions; ctrl.showMenu = false; ctrl.showEditorMenu = false;"} Actions
|
||||||
%contextual-extensions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
|
%contextual-extensions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#footer-bar
|
#footer-bar
|
||||||
.pull-left
|
.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
|
%a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account
|
||||||
%account-menu{"ng-if" => "ctrl.showAccountMenu", "on-successful-auth" => "ctrl.onAuthSuccess"}
|
%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
|
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
|
||||||
%global-extensions-menu{"ng-if" => "ctrl.showExtensionsMenu"}
|
%global-extensions-menu{"ng-if" => "ctrl.showExtensionsMenu"}
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
#search-clear-button{"ng-if" => "ctrl.noteFilter.text", "ng-click" => "ctrl.noteFilter.text = ''; ctrl.filterTextChanged()"} ✕
|
#search-clear-button{"ng-if" => "ctrl.noteFilter.text", "ng-click" => "ctrl.noteFilter.text = ''; ctrl.filterTextChanged()"} ✕
|
||||||
%ul.section-menu-bar#notes-menu-bar
|
%ul.section-menu-bar#notes-menu-bar
|
||||||
%li.item-with-subtitle{"ng-class" => "{'selected' : ctrl.showMenu}"}
|
%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
|
%label Options
|
||||||
.subtitle {{ctrl.optionsSubtitle()}}
|
.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
|
%ul
|
||||||
.header
|
.header
|
||||||
.title Sort by
|
.title Sort by
|
||||||
|
|||||||
Reference in New Issue
Block a user