Bladeren bron

HUE-9203 [editor] Improve drag and drop from the left assist into the editor

Johan Ahlen 5 jaren geleden
bovenliggende
commit
65a348e648

+ 12 - 164
desktop/core/src/desktop/js/ko/bindings/ace/ko.aceEditor.js

@@ -20,12 +20,15 @@ import * as ko from 'knockout';
 import apiHelper from 'api/apiHelper';
 import AceLocationHandler from 'ko/bindings/ace/aceLocationHandler';
 import huePubSub from 'utils/huePubSub';
-import sqlUtils from 'sql/sqlUtils';
 import AceGutterHandler from 'ko/bindings/ace/aceGutterHandler';
+import { registerBinding } from '../bindingUtils';
 
 // TODO: Depends on Ace
 
-ko.bindingHandlers.aceEditor = {
+export const NAME = 'aceEditor';
+export const INSERT_AT_CURSOR_EVENT = 'editor.insert.at.cursor';
+
+registerBinding(NAME, {
   init: function(element, valueAccessor) {
     const $el = $(element);
     const options = ko.unwrap(valueAccessor());
@@ -623,9 +626,12 @@ ko.bindingHandlers.aceEditor = {
       }
     );
 
-    const insertAtCursorSub = huePubSub.subscribe('editor.insert.at.cursor', text => {
-      if ($el.data('last-active-editor')) {
-        insertSqlAtCursor(text + ' ', 0);
+    const insertAtCursorSub = huePubSub.subscribe(INSERT_AT_CURSOR_EVENT, details => {
+      if (
+        (details.targetEditor && details.targetEditor === editor) ||
+        $el.data('last-active-editor')
+      ) {
+        insertSqlAtCursor(details.text + ' ', details.cursorEndAdjust || 0);
       }
     });
 
@@ -695,164 +701,6 @@ ko.bindingHandlers.aceEditor = {
       sampleErrorInsertSub.remove();
     });
 
-    const $tableDropMenu = $el.next('.table-drop-menu');
-    const $identifierDropMenu = $tableDropMenu.find('.editor-drop-identifier');
-
-    const hideDropMenu = function() {
-      $tableDropMenu.css('opacity', 0);
-      window.setTimeout(() => {
-        $tableDropMenu.hide();
-      }, 300);
-    };
-
-    const documentClickListener = function(event) {
-      if ($tableDropMenu.find($(event.target)).length === 0) {
-        hideDropMenu();
-      }
-    };
-
-    $(document).on('click', documentClickListener);
-
-    disposeFunctions.push(() => {
-      $(document).off('click', documentClickListener);
-    });
-
-    let lastMeta = {};
-    const draggableTextSub = huePubSub.subscribe('draggable.text.meta', meta => {
-      lastMeta = meta;
-      if (meta.isView) {
-        $tableDropMenu.find('.editor-drop-update').hide();
-        $tableDropMenu.find('.editor-drop-insert').hide();
-        $tableDropMenu.find('.editor-drop-drop').hide();
-        $tableDropMenu.find('.editor-drop-view').show();
-      } else {
-        $tableDropMenu.find('.editor-drop-update').show();
-        $tableDropMenu.find('.editor-drop-insert').show();
-        $tableDropMenu.find('.editor-drop-drop').show();
-        $tableDropMenu.find('.editor-drop-view').hide();
-      }
-      if (
-        typeof meta !== 'undefined' &&
-        typeof meta.database !== 'undefined' &&
-        typeof meta.table !== 'undefined'
-      ) {
-        $identifierDropMenu.text(meta.database + '.' + meta.table);
-      }
-    });
-
-    disposeFunctions.push(() => {
-      draggableTextSub.remove();
-    });
-
-    const menu = ko.bindingHandlers.contextMenu.initContextMenu(
-      $tableDropMenu,
-      $('.content-panel')
-    );
-
-    $tableDropMenu.find('.editor-drop-value').click(() => {
-      insertSqlAtCursor(
-        sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
-          '.' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
-          ' ',
-        0,
-        menu
-      );
-    });
-
-    $tableDropMenu.find('.editor-drop-select').click(() => {
-      insertSqlAtCursor(
-        'SELECT * FROM ' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
-          '.' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
-          ' LIMIT 100;',
-        -1,
-        menu
-      );
-      $tableDropMenu.hide();
-    });
-
-    $tableDropMenu.find('.editor-drop-insert').click(() => {
-      insertSqlAtCursor(
-        'INSERT INTO ' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
-          '.' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
-          ' VALUES ();',
-        -2,
-        menu
-      );
-    });
-
-    $tableDropMenu.find('.editor-drop-update').click(() => {
-      insertSqlAtCursor(
-        'UPDATE ' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
-          '.' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
-          ' SET ',
-        0,
-        menu
-      );
-    });
-
-    $tableDropMenu.find('.editor-drop-view').click(() => {
-      insertSqlAtCursor(
-        'DROP VIEW ' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
-          '.' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
-          ';',
-        -1,
-        menu
-      );
-    });
-
-    $tableDropMenu.find('.editor-drop-drop').click(() => {
-      insertSqlAtCursor(
-        'DROP TABLE ' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
-          '.' +
-          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
-          ';',
-        -1,
-        menu
-      );
-    });
-
-    $el.droppable({
-      accept: '.draggableText',
-      drop: function(e, ui) {
-        const position = editor.renderer.screenToTextCoordinates(e.clientX, e.clientY);
-        let text = ui.helper.text();
-        if (
-          lastMeta.type === 's3' ||
-          lastMeta.type === 'hdfs' ||
-          lastMeta.type === 'adls' ||
-          lastMeta.type === 'abfs'
-        ) {
-          text = "'" + lastMeta.definition.path + "'";
-        }
-        editor.moveCursorToPosition(position);
-        const before = editor.getTextBeforeCursor();
-        if (lastMeta.database && lastMeta.table && !lastMeta.column && /.*;|^\s*$/.test(before)) {
-          menu.show(e);
-        } else {
-          if (/\S+$/.test(before) && before.charAt(before.length - 1) !== '.') {
-            text = ' ' + text;
-          }
-          const after = editor.getTextAfterCursor();
-          if (after.length > 0 && after.charAt(0) !== ' ' && text.charAt(text.length - 1) !== ' ') {
-            text += ' ';
-          }
-          editor.session.insert(position, text);
-          position.column += text.length;
-          editor.clearSelection();
-        }
-      }
-    });
-
     let autocompleteTemporarilyDisabled = false;
     let autocompleteThrottle = -1;
     const afterExecListener = editor.commands.on('afterExec', e => {
@@ -1017,4 +865,4 @@ ko.bindingHandlers.aceEditor = {
       } catch (e) {}
     }
   }
-};
+});

+ 24 - 0
desktop/core/src/desktop/js/ko/bindings/bindingUtils.js

@@ -0,0 +1,24 @@
+// 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 * as ko from 'knockout';
+
+export const registerBinding = (name, model) => {
+  if (ko.bindingHandlers[name]) {
+    return;
+  }
+  ko.bindingHandlers[name] = model;
+};

+ 7 - 1
desktop/core/src/desktop/js/ko/bindings/ko.contextMenu.js

@@ -35,7 +35,7 @@ import huePubSub from 'utils/huePubSub';
  *
  */
 ko.bindingHandlers.contextMenu = {
-  initContextMenu: function($menu, $scrollContainer) {
+  initContextMenu: function($menu, $scrollContainer, onShow, onHide) {
     let active = false;
 
     let currentLeft = 0;
@@ -74,6 +74,9 @@ ko.bindingHandlers.contextMenu = {
         $menu.css('opacity', 1);
         active = true;
         $scrollContainer.on('scroll', adjustForScroll);
+        if (onShow) {
+          onShow();
+        }
       },
       hide: function() {
         if (active) {
@@ -84,6 +87,9 @@ ko.bindingHandlers.contextMenu = {
           }, 300);
           active = false;
         }
+        if (onHide) {
+          onHide();
+        }
       }
     };
   },

+ 7 - 3
desktop/core/src/desktop/js/ko/bindings/ko.draggableText.js

@@ -18,8 +18,12 @@ import $ from 'jquery';
 import * as ko from 'knockout';
 
 import huePubSub from 'utils/huePubSub';
+import { registerBinding } from './bindingUtils';
 
-ko.bindingHandlers.draggableText = {
+export const DRAGGABLE_TEXT_META_EVENT = 'draggable.text.meta';
+export const NAME = 'draggableText';
+
+registerBinding(NAME, {
   init: function(element, valueAccessor) {
     const $element = $(element);
     const options = valueAccessor();
@@ -42,7 +46,7 @@ ko.bindingHandlers.draggableText = {
       start: function(event) {
         dragStartX = event.clientX;
         dragStartY = event.clientY;
-        huePubSub.publish('draggable.text.meta', options.meta);
+        huePubSub.publish(DRAGGABLE_TEXT_META_EVENT, options.meta);
         notifiedOnDragStarted = false;
       },
       drag: function(event) {
@@ -80,4 +84,4 @@ ko.bindingHandlers.draggableText = {
       }
     });
   }
-};
+});

+ 3 - 3
desktop/core/src/desktop/js/ko/components/assist/ko.assistFunctionsPanel.js

@@ -48,7 +48,7 @@ const TEMPLATE = `
             <a class="black-link" href="javascript: void(0);" data-bind="toggle: open"><i class="fa fa-fw" data-bind="css: { 'fa-chevron-right': !open(), 'fa-chevron-down': open }"></i> <span data-bind="text: name"></span></a>
             <ul class="assist-functions" data-bind="slideVisible: open, foreach: functions">
               <li data-bind="tooltip: { title: description, placement: 'left', delay: 1000 }">
-                <a class="assist-field-link" href="javascript: void(0);" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, css: { 'blue': $parents[1].selectedFunction() === $data }, multiClick: { click: function () { $parents[1].selectedFunction($data); }, dblClick: function () { huePubSub.publish('editor.insert.at.cursor', draggable); } }, text: signature"></a>
+                <a class="assist-field-link" href="javascript: void(0);" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, css: { 'blue': $parents[1].selectedFunction() === $data }, multiClick: { click: function () { $parents[1].selectedFunction($data); }, dblClick: function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }, text: signature"></a>
               </li>
             </ul>
           </li>
@@ -58,7 +58,7 @@ const TEMPLATE = `
         <!-- ko if: filteredFunctions().length > 0 -->
         <ul class="assist-functions" data-bind="foreach: filteredFunctions">
           <li data-bind="tooltip: { title: description, placement: 'left', delay: 1000 }">
-            <a class="assist-field-link" href="javascript: void(0);" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, css: { 'blue': $parent.selectedFunction() === $data }, multiClick: { click: function () { $parent.selectedFunction($data); }, dblClick: function () { huePubSub.publish('editor.insert.at.cursor', draggable); } }, html: signatureMatch"></a>
+            <a class="assist-field-link" href="javascript: void(0);" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, css: { 'blue': $parent.selectedFunction() === $data }, multiClick: { click: function () { $parent.selectedFunction($data); }, dblClick: function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }, html: signatureMatch"></a>
           </li>
         </ul>
         <!-- /ko -->
@@ -72,7 +72,7 @@ const TEMPLATE = `
       <!-- ko if: selectedFunction -->
       <div class="assist-flex-half assist-function-details" data-bind="with: selectedFunction">
         <div class="assist-panel-close"><button class="close" data-bind="click: function() { $parent.selectedFunction(null); }">&times;</button></div>
-        <div class="assist-function-signature blue" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, text: signature, event: { 'dblclick': function () { huePubSub.publish('editor.insert.at.cursor', draggable); } }"></div>
+        <div class="assist-function-signature blue" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, text: signature, event: { 'dblclick': function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }"></div>
         <!-- ko if: description -->
         <div data-bind="html: descriptionMatch"></div>
         <!-- /ko -->

+ 181 - 0
desktop/core/src/desktop/js/ko/components/ko.editorDroppableMenu.js

@@ -0,0 +1,181 @@
+// 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 componentUtils from './componentUtils';
+import huePubSub from 'utils/huePubSub';
+import sqlUtils from 'sql/sqlUtils';
+import DisposableComponent from './DisposableComponent';
+import { DRAGGABLE_TEXT_META_EVENT } from '../bindings/ko.draggableText';
+import { INSERT_AT_CURSOR_EVENT } from '../bindings/ace/ko.aceEditor';
+import { defer, sleep } from 'utils/hueUtils';
+
+export const NAME = 'hue-editor-droppable-menu';
+
+const TYPES = {
+  VALUE: 'value',
+  SELECT: 'select',
+  INSERT: 'insert',
+  UPDATE: 'update',
+  DROP: 'drop'
+};
+
+// prettier-ignore
+const TEMPLATE = `
+  <ul class="table-drop-menu hue-context-menu">
+    <!-- ko if: meta -->
+    <li><a href="javascript: void(0);" data-bind="click: insertValue.bind($data, '${ TYPES.VALUE }')">"<span data-bind="text: identifier"></span>"</a></li>
+    <li class="divider"></li>
+    <li><a href="javascript: void(0);" data-bind="click: insertValue.bind($data, '${ TYPES.SELECT }')">SELECT FROM <span data-bind="text: identifier"></span>...</a></li>
+    <!-- ko ifnot: meta().isView -->
+      <li><a href="javascript: void(0);" data-bind="click: insertValue.bind($data, '${ TYPES.INSERT }')">INSERT INTO <span data-bind="text: identifier"></span>...</a></li>
+      <li><a href="javascript: void(0);" data-bind="click: insertValue.bind($data, '${ TYPES.UPDATE }')">UPDATE <span data-bind="text: identifier"></span>...</a></li>
+      <li><a href="javascript: void(0);" data-bind="click: insertValue.bind($data, '${ TYPES.DROP }')">DROP TABLE <span data-bind="text: identifier"></span>...</a></li>
+    <!-- /ko -->
+    <!-- ko if: meta().isView -->
+      <li><a href="javascript: void(0);"data-bind="click: insertValue.bind($data, '${ TYPES.DROP }')">DROP VIEW <span data-bind="text: identifier"></span>...</a></li>
+    <!-- /ko -->
+    <!-- /ko -->
+  </ul>
+`;
+
+class EditorDroppableMenu extends DisposableComponent {
+  constructor(params, element) {
+    super();
+    this.editor = params.editor;
+    const $parentDropTarget = $(params.parentDropTarget);
+    const $tableDropMenu = $(element)
+      .parent()
+      .find('.table-drop-menu');
+
+    this.meta = ko.observable();
+
+    this.identifier = ko.pureComputed(() => {
+      const meta = this.meta();
+      if (meta && meta.database && meta.table) {
+        return (
+          sqlUtils.backTickIfNeeded(meta.type, meta.database) +
+          '.' +
+          sqlUtils.backTickIfNeeded(meta.type, meta.table)
+        );
+      }
+      return '';
+    });
+
+    super.subscribe(DRAGGABLE_TEXT_META_EVENT, this.meta);
+
+    this.menu = ko.bindingHandlers.contextMenu.initContextMenu(
+      $tableDropMenu,
+      $('.content-panel'),
+      async () => {
+        await defer();
+        $(document).on('click.' + NAME, async () => {
+          if (this.menu) {
+            $tableDropMenu.css('opacity', 0);
+            await sleep(300);
+            this.menu.hide();
+          }
+        });
+      },
+      () => {
+        $(document).off('click.' + NAME);
+      }
+    );
+
+    $parentDropTarget.droppable({
+      accept: '.draggableText',
+      drop: (e, ui) => {
+        const editor = this.editor();
+        const meta = this.meta();
+        if (!meta || !editor) {
+          return;
+        }
+        const position = editor.renderer.screenToTextCoordinates(e.clientX, e.clientY);
+        let text = ui.helper.text();
+        if (
+          meta.type === 's3' ||
+          meta.type === 'hdfs' ||
+          meta.type === 'adls' ||
+          meta.type === 'abfs'
+        ) {
+          text = "'" + meta.definition.path + "'";
+        }
+        editor.moveCursorToPosition(position);
+        const before = editor.getTextBeforeCursor();
+        if (meta.database && meta.table && !meta.column && /.*;|^\s*$/.test(before)) {
+          this.menu.show(e);
+        } else {
+          if (/\S+$/.test(before) && before.charAt(before.length - 1) !== '.') {
+            text = ' ' + text;
+          }
+          const after = editor.getTextAfterCursor();
+          if (after.length > 0 && after.charAt(0) !== ' ' && text.charAt(text.length - 1) !== ' ') {
+            text += ' ';
+          }
+          editor.session.insert(position, text);
+          position.column += text.length;
+          editor.clearSelection();
+        }
+      }
+    });
+  }
+
+  insertValue(type) {
+    let textToInsert = '';
+    let cursorEndAdjust = 0;
+    switch (type) {
+      case TYPES.UPDATE:
+        textToInsert = 'UPDATE ' + this.identifier() + ' SET ';
+        cursorEndAdjust = -1;
+        break;
+      case TYPES.DROP:
+        textToInsert =
+          (this.meta().isView ? 'DROP VIEW ' : 'DROP TABLE ') + this.identifier() + ';';
+        cursorEndAdjust = -2;
+        break;
+      case TYPES.INSERT:
+        textToInsert = 'INSERT INTO ' + this.identifier() + ' VALUES ();';
+        cursorEndAdjust = -3;
+        break;
+      case TYPES.SELECT:
+        textToInsert = 'SELECT * FROM ' + this.identifier() + ' LIMIT 100;';
+        cursorEndAdjust = -2;
+        break;
+      default:
+        textToInsert = this.identifier();
+        break;
+    }
+
+    huePubSub.publish(INSERT_AT_CURSOR_EVENT, {
+      text: textToInsert,
+      targetEditor: this.editor(),
+      cursorEndAdjust: cursorEndAdjust
+    });
+
+    this.menu.hide();
+  }
+}
+
+componentUtils.registerComponent(
+  NAME,
+  {
+    createViewModel: (params, componentInfo) =>
+      new EditorDroppableMenu(params, componentInfo.element)
+  },
+  TEMPLATE
+);

+ 1 - 0
desktop/core/src/desktop/js/ko/ko.all.js

@@ -151,6 +151,7 @@ import 'ko/components/ko.contextSelector';
 import 'ko/components/ko.createDirectoryModal';
 import 'ko/components/ko.deleteDocModal';
 import 'ko/components/ko.dropDown';
+import 'ko/components/ko.editorDroppableMenu';
 import 'ko/components/ko.executionAnalysis';
 import 'ko/components/ko.favoriteApp';
 import 'ko/components/ko.fieldSamples';

+ 3 - 1
desktop/core/src/desktop/js/utils/hueUtils.js

@@ -584,7 +584,9 @@ export const sleep = async timeout => new Promise(resolve => setTimeout(resolve,
 
 export const defer = async callback => {
   await sleep(0);
-  callback();
+  if (callback) {
+    callback();
+  }
 };
 
 export default {

+ 1 - 10
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -1132,16 +1132,7 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
         }
       }, style: {opacity: statementType() !== 'text' || $root.isPresentationMode() ? '0.75' : '1', 'min-height': $root.editorMode() ? '0' : '48px', 'top': $root.editorMode() && statementType() !== 'text' ? '60px' : '0'}"></div>
       <!-- ko component: { name: 'hueAceAutocompleter', params: { editor: ace.bind($data), snippet: $data } } --><!-- /ko -->
-
-      <ul class="table-drop-menu hue-context-menu">
-        <li class="editor-drop-value"><a href="javascript:void(0);">"<span class="editor-drop-identifier"></span>"</a></li>
-        <li class="divider"></li>
-        <li class="editor-drop-select"><a href="javascript:void(0);">SELECT FROM <span class="editor-drop-identifier"></span>...</a></li>
-        <li class="editor-drop-insert"><a href="javascript:void(0);">INSERT INTO <span class="editor-drop-identifier"></span>...</a></li>
-        <li class="editor-drop-update"><a href="javascript:void(0);">UPDATE <span class="editor-drop-identifier"></span>...</a></li>
-        <li class="editor-drop-drop"><a href="javascript:void(0);">DROP TABLE <span class="editor-drop-identifier"></span>...</a></li>
-        <li class="editor-drop-view"><a href="javascript:void(0);">DROP VIEW <span class="editor-drop-identifier"></span>...</a></li>
-      </ul>
+      <!-- ko component: { name: 'hue-editor-droppable-menu', params: { editor: ace.bind($data), parentDropTarget: '.editor' } } --><!-- /ko -->
     </div>
 
     <div class="clearfix"></div>

+ 1 - 10
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -928,16 +928,7 @@
             }
           "></div>
         <!-- ko component: { name: 'hueAceAutocompleter', params: { editor: ace.bind($data), snippet: $data } } --><!-- /ko -->
-
-        <ul class="table-drop-menu hue-context-menu">
-          <li class="editor-drop-value"><a href="javascript:void(0);">"<span class="editor-drop-identifier"></span>"</a></li>
-          <li class="divider"></li>
-          <li class="editor-drop-select"><a href="javascript:void(0);">SELECT FROM <span class="editor-drop-identifier"></span>...</a></li>
-          <li class="editor-drop-insert"><a href="javascript:void(0);">INSERT INTO <span class="editor-drop-identifier"></span>...</a></li>
-          <li class="editor-drop-update"><a href="javascript:void(0);">UPDATE <span class="editor-drop-identifier"></span>...</a></li>
-          <li class="editor-drop-drop"><a href="javascript:void(0);">DROP TABLE <span class="editor-drop-identifier"></span>...</a></li>
-          <li class="editor-drop-view"><a href="javascript:void(0);">DROP VIEW <span class="editor-drop-identifier"></span>...</a></li>
-        </ul>
+        <!-- ko component: { name: 'hue-editor-droppable-menu', params: { editor: ace.bind($data), parentDropTarget: '.editor' } } --><!-- /ko -->
       </div>
 
       <div class="clearfix"></div>