Browse Source

HUE-9182 [ui] Add a binding that triggers the context popover on hover

Johan Ahlen 5 years ago
parent
commit
ea37d07c17

+ 3 - 0
desktop/core/src/desktop/js/ko/components/contextPopover/__snapshots__/ko.popoverOnHover.test.js.snap

@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.popoverOnHover.js should render binding 1`] = `"<div id=\\"foo\\" data-bind=\\"popoverOnHover: () => {}\\"></div>"`;

+ 32 - 22
desktop/core/src/desktop/js/ko/components/contextPopover/ko.contextPopover.js

@@ -34,6 +34,12 @@ import StorageContext from './storageContext';
 import componentUtils from '../componentUtils';
 import { GET_KNOWN_CONFIG_EVENT } from 'utils/hueConfig';
 
+export const CONTEXT_POPOVER_CLASS = 'hue-popover';
+export const HIDE_EVENT = 'context.popover.hide';
+export const SHOW_EVENT = 'context.popover.show';
+export const NAME = 'context-popover';
+
+// prettier-ignore
 const SUPPORT_TEMPLATES = `
   <script type="text/html" id="context-popover-footer">
     <div class="context-popover-flex-bottom-links">
@@ -500,8 +506,9 @@ const SUPPORT_TEMPLATES = `
   </script>
 `;
 
+// prettier-ignore
 const CONTEXT_POPOVER_TEMPLATE = `
-  <div class="hue-popover" data-bind="css: orientationClass, style: { 'left': left() + 'px', 'top': top() + 'px', 'width': width() + 'px', height: height() + 'px' }, resizable: { containment: 'document', handles: resizeHelper.resizableHandles, start: resizeHelper.resizeStart, stop: resizeHelper.resizeStop, resize: resizeHelper.resize }">
+  <div class="${ CONTEXT_POPOVER_CLASS }" data-bind="css: orientationClass, style: { 'left': left() + 'px', 'top': top() + 'px', 'width': width() + 'px', height: height() + 'px' }, resizable: { containment: 'document', handles: resizeHelper.resizableHandles, start: resizeHelper.resizeStart, stop: resizeHelper.resizeStop, resize: resizeHelper.resize }">
     <div class="hue-popover-arrow" data-bind="style: { 'margin-left': leftAdjust() + 'px',  'margin-top': topAdjust() + 'px' }"></div>
     <!-- ko if: typeof titleTemplate !== 'undefined' -->
     <!-- ko template: { name: titleTemplate, data: contents } --><!-- /ko -->
@@ -530,6 +537,7 @@ const CONTEXT_POPOVER_TEMPLATE = `
   </div>
 `;
 
+// prettier-ignore
 const GLOBAL_SEARCH_TEMPLATE = `
   <!-- ko if: isCatalogEntry -->
   <!-- ko with: contents -->
@@ -845,27 +853,29 @@ class ContextPopoverViewModel {
   }
 }
 
-componentUtils.registerComponent(
-  'context-popover',
-  ContextPopoverViewModel,
-  SUPPORT_TEMPLATES +
-    DOCUMENT_CONTEXT_TEMPLATE +
-    FUNCTION_CONTEXT_TEMPLATE +
-    PARTITION_CONTEXT_TEMPLATE +
-    CONTEXT_POPOVER_TEMPLATE
-);
-
-huePubSub.subscribe('context.popover.hide', hidePopover);
-
-huePubSub.subscribe('context.popover.show', details => {
-  hidePopover();
-  const $contextPopover = $(
-    '<div id="contextPopover" data-bind="component: { name: \'context-popover\', params: $data }" />'
-  );
-  $('body').append($contextPopover);
-  ko.applyBindings(details, $contextPopover[0]);
-  huePubSub.publish('context.popover.shown');
-});
+componentUtils
+  .registerComponent(
+    NAME,
+    ContextPopoverViewModel,
+    SUPPORT_TEMPLATES +
+      DOCUMENT_CONTEXT_TEMPLATE +
+      FUNCTION_CONTEXT_TEMPLATE +
+      PARTITION_CONTEXT_TEMPLATE +
+      CONTEXT_POPOVER_TEMPLATE
+  )
+  .then(() => {
+    huePubSub.subscribe(HIDE_EVENT, hidePopover);
+
+    huePubSub.subscribe(SHOW_EVENT, details => {
+      hidePopover();
+      const $contextPopover = $(
+        '<div id="contextPopover" data-bind="component: { name: \'context-popover\', params: $data }" />'
+      );
+      $('body').append($contextPopover);
+      ko.applyBindings(details, $contextPopover[0]);
+      huePubSub.publish('context.popover.shown');
+    });
+  });
 
 class SqlContextContentsGlobalSearch {
   constructor(params) {

+ 102 - 0
desktop/core/src/desktop/js/ko/components/contextPopover/ko.popoverOnHover.js

@@ -0,0 +1,102 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import * as ko from 'knockout';
+import { CONTEXT_POPOVER_CLASS } from './ko.contextPopover';
+import huePubSub from 'utils/huePubSub';
+
+import { HIDE_EVENT } from './ko.contextPopover';
+
+const DEFAULT_DELAY = 700;
+
+export const NAME = 'popoverOnHover';
+
+ko.bindingHandlers[NAME] = {
+  init: (element, valueAccessor, allBindings, viewModel, bindingContext) => {
+    let options = valueAccessor();
+    const $element = $(element);
+    const popoverSelector = '.' + CONTEXT_POPOVER_CLASS;
+
+    if (typeof options === 'function') {
+      options = {
+        onHover: options,
+        delay: DEFAULT_DELAY
+      };
+    }
+
+    let visible = false;
+    let showTimeout = -1;
+    let hideTimeout = -1;
+
+    const clearTimeouts = () => {
+      window.clearTimeout(showTimeout);
+      window.clearTimeout(hideTimeout);
+    };
+
+    const hide = () => {
+      clearTimeouts();
+      hideTimeout = window.setTimeout(() => {
+        visible = false;
+        $(popoverSelector).off('.onHover');
+        huePubSub.publish(HIDE_EVENT);
+      }, options.delay);
+    };
+
+    const show = event => {
+      clearTimeouts();
+      if (visible) {
+        return;
+      }
+
+      showTimeout = window.setTimeout(() => {
+        visible = true;
+        huePubSub.publish(HIDE_EVENT);
+        options.onHover.bind(bindingContext.$data)(
+          bindingContext.$data,
+          event,
+          options.positionAdjustments
+        );
+      }, options.delay);
+    };
+
+    $element.on('mouseenter.onHover', show);
+
+    $element.on('mouseleave.onHover', () => {
+      clearTimeouts();
+      // Keep open if mouse moves to the context popover
+      if (visible) {
+        const $popoverSelector = $(popoverSelector);
+        $popoverSelector.on('mouseenter.onHover', () => {
+          clearTimeouts();
+        });
+        $popoverSelector.on('mouseleave.onHover', () => {
+          hide();
+        });
+        hide();
+      }
+    });
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      $element.off('.onHover');
+      $(popoverSelector).off('.onHover');
+      clearTimeouts();
+      if (visible) {
+        huePubSub.publish(HIDE_EVENT);
+      }
+    });
+  }
+};

+ 62 - 0
desktop/core/src/desktop/js/ko/components/contextPopover/ko.popoverOnHover.test.js

@@ -0,0 +1,62 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+
+import { koSetup } from 'jest/koTestUtils';
+
+import { NAME } from './ko.popoverOnHover';
+import huePubSub from 'utils/huePubSub';
+import { HIDE_EVENT } from './ko.contextPopover';
+
+describe('ko.popoverOnHover.js', () => {
+  const setup = koSetup();
+
+  jest.useFakeTimers();
+
+  it('should render binding', async () => {
+    const wrapper = await setup.renderKo(`<div id="foo" data-bind="${NAME}: () => {}"></div>`, {});
+
+    expect(wrapper.innerHTML).toMatchSnapshot();
+  });
+
+  it('should call callback on hover and publish on leave', async () => {
+    let callbackCalled = false;
+    const viewModel = {
+      callback: () => {
+        callbackCalled = true;
+      }
+    };
+    const wrapper = await setup.renderKo(
+      `<div id="foo" data-bind="${NAME}: callback"></div>`,
+      viewModel
+    );
+
+    // Mouse enters
+    $(wrapper.firstChild).trigger('mouseenter');
+    jest.runAllTimers();
+    expect(callbackCalled).toBeTruthy();
+
+    // Mouse leaves
+    let publishCalled = false;
+    huePubSub.subscribeOnce(HIDE_EVENT, () => {
+      publishCalled = true;
+    });
+    $(wrapper.firstChild).trigger('mouseleave');
+    jest.runAllTimers();
+    expect(publishCalled).toBeTruthy();
+  });
+});