Files
standardnotes-app-web/app/assets/javascripts/app/directives/views/componentView.js
2019-06-03 17:15:35 -05:00

268 lines
9.5 KiB
JavaScript

class ComponentView {
constructor($rootScope, componentManager, desktopManager, $timeout, themeManager) {
this.restrict = "E";
this.templateUrl = "directives/component-view.html";
this.scope = {
component: "=",
onLoad: "=",
manualDealloc: "="
};
this.desktopManager = desktopManager;
}
link($scope, el, attrs, ctrl) {
$scope.el = el;
$scope.componentValid = true;
$scope.updateObserver = this.desktopManager.registerUpdateObserver((component) => {
if(component == $scope.component && component.active) {
$scope.reloadComponent();
}
})
$scope.$watch('component', function(component, prevComponent){
ctrl.componentValueChanging(component, prevComponent);
});
}
controller($scope, $rootScope, $timeout, componentManager, desktopManager, themeManager) {
'ngInject';
$scope.onVisibilityChange = function() {
if(document.visibilityState == "hidden") {
return;
}
if($scope.issueLoading) {
$scope.reloadComponent();
}
}
$scope.themeHandlerIdentifier = "component-view-" + Math.random();
componentManager.registerHandler({identifier: $scope.themeHandlerIdentifier, areas: ["themes"], activationHandler: (component) => {
$scope.reloadThemeStatus();
}});
$scope.identifier = "component-view-" + Math.random();
componentManager.registerHandler({
identifier: $scope.identifier,
areas: [$scope.component.area],
activationHandler: (component) => {
if(component !== $scope.component) {
return;
}
$timeout(() => {
$scope.handleActivation();
})
},
actionHandler: (component, action, data) => {
if(action == "set-size") {
componentManager.handleSetSizeEvent(component, data);
}
}
});
$scope.handleActivation = function() {
// activationHandlers may be called multiple times, design below to be idempotent
let component = $scope.component;
if(!component.active) {
return;
}
let iframe = componentManager.iframeForComponent(component);
if(iframe) {
$scope.loading = true;
// begin loading error handler. If onload isn't called in x seconds, display an error
if($scope.loadTimeout) { $timeout.cancel($scope.loadTimeout);}
$scope.loadTimeout = $timeout(() => {
if($scope.loading) {
$scope.loading = false;
$scope.issueLoading = true;
if(!$scope.didAttemptReload) {
$scope.didAttemptReload = true;
$scope.reloadComponent();
} else {
// We'll attempt to reload when the tab gains focus
document.addEventListener("visibilitychange", $scope.onVisibilityChange);
}
}
}, 3500);
iframe.onload = (event) => {
let desktopError = false;
try {
// Accessing iframe.contentWindow.origin will throw an exception if we are in the web app, or if the iframe content
// is remote content. The only reason it works in this case is because we're accessing a local extension.
// In the future when the desktop app falls back to the web location if local fail loads, we won't be able to access this property anymore.
if(isDesktopApplication() && (iframe.contentWindow.origin == null || iframe.contentWindow.origin == 'null')) {
/*
Don't attempt reload in this case, as it results in infinite loop, since a reload will deactivate the extension and then reactivate.
This can cause this componentView to be dealloced and a new one to be instantiated. This happens in editor.js, which we'll need to look into.
Don't return from this clause either, since we don't want to cancel loadTimeout (that will trigger reload). Instead, handle custom fail logic here.
*/
desktopError = true;
}
} catch (e) {
}
$timeout.cancel($scope.loadTimeout);
componentManager.registerComponentWindow(component, iframe.contentWindow).then(() => {
// Add small timeout to, as $scope.loading controls loading overlay,
// which is used to avoid flicker when enabling extensions while having an enabled theme
// we don't use ng-show because it causes problems with rendering iframes after timeout, for some reason.
$timeout(() => {
$scope.loading = false;
$scope.issueLoading = desktopError; /* Typically we'd just set this to false at this point, but we now account for desktopError */
$scope.onLoad && $scope.onLoad($scope.component);
}, 7)
})
};
}
}
/*
General note regarding activation/deactivation of components:
We pass `true` to componentManager.ac/detivateComponent for the `dontSync` parameter.
The activation we do in here is not global, but just local, so we don't need to sync the state.
For example, if we activate an editor, we just need to do that for display purposes, but dont
need to perform a sync to propagate that .active flag.
*/
this.componentValueChanging = (component, prevComponent) => {
//
// See comment above about passing true to componentManager.ac/detivateComponent
//
if(prevComponent && component !== prevComponent) {
// Deactive old component
componentManager.deactivateComponent(prevComponent, true);
}
if(component) {
componentManager.activateComponent(component, true);
// console.log("Loading", $scope.component.name, $scope.getUrl(), component.valid_until);
$scope.reloadStatus();
}
}
$scope.$on("ext-reload-complete", () => {
$scope.reloadStatus(false);
})
$scope.reloadComponent = function() {
// console.log("Reloading component", $scope.component);
// force iFrame to deinit, allows new one to be created
$scope.componentValid = false;
componentManager.reloadComponent($scope.component).then(() => {
$scope.reloadStatus();
})
}
$scope.reloadStatus = function(doManualReload = true) {
let component = $scope.component;
$scope.reloading = true;
let previouslyValid = $scope.componentValid;
let offlineRestricted = component.offlineOnly && !isDesktopApplication();
let urlError =
(!isDesktopApplication() && !component.hasValidHostedUrl())
||
(isDesktopApplication() && (!component.local_url && !component.hasValidHostedUrl()))
$scope.expired = component.valid_until && component.valid_until <= new Date();
// Here we choose our own readonly state based on custom logic. However, if a parent
// wants to implement their own readonly logic, they can lock it.
if(!component.lockReadonly) {
component.readonly = $scope.expired;
}
$scope.componentValid = !offlineRestricted && !urlError;
if(!$scope.componentValid) {
// required to disable overlay
$scope.loading = false;
}
if(offlineRestricted) $scope.error = 'offline-restricted';
else if(urlError) $scope.error = 'url-missing';
else $scope.error = null;
if($scope.componentValid !== previouslyValid) {
if($scope.componentValid) {
// We want to reload here, rather than `activateComponent`, because the component will already have attempted to been activated.
componentManager.reloadComponent(component, true);
}
}
if($scope.expired && doManualReload) {
// Try reloading, handled by footer, which will open Extensions window momentarily to pull in latest data
// Upon completion, this method, reloadStatus, will be called, upon where doManualReload will be false to prevent recursion.
$rootScope.$broadcast("reload-ext-data");
}
$scope.reloadThemeStatus();
$timeout(() => {
$scope.reloading = false;
}, 500)
}
$scope.reloadThemeStatus = function() {
if(!$scope.component.acceptsThemes()) {
if(themeManager.hasActiveTheme()) {
if(!$scope.dismissedNoThemesMessage) {
$scope.showNoThemesMessage = true;
}
} else {
// Can be the case if we've just deactivated a theme
$scope.showNoThemesMessage = false;
}
}
}
$scope.noThemesMessageDismiss = function() {
$scope.showNoThemesMessage = false;
$scope.dismissedNoThemesMessage = true;
}
$scope.disableActiveTheme = function() {
themeManager.deactivateAllThemes();
$scope.noThemesMessageDismiss();
}
$scope.getUrl = function() {
var url = componentManager.urlForComponent($scope.component);
$scope.component.runningLocally = (url == $scope.component.local_url);
return url;
}
$scope.destroy = function() {
componentManager.deregisterHandler($scope.themeHandlerIdentifier);
componentManager.deregisterHandler($scope.identifier);
if($scope.component && !$scope.manualDealloc) {
componentManager.deactivateComponent($scope.component, true);
}
desktopManager.deregisterUpdateObserver($scope.updateObserver);
document.removeEventListener("visibilitychange", $scope.onVisibilityChange);
}
$scope.$on("$destroy", function() {
$scope.destroy();
});
}
}
angular.module('app').directive('componentView', ($rootScope, componentManager, desktopManager, $timeout) => new ComponentView($rootScope, componentManager, desktopManager, $timeout));