Browse Source

[editor] Add an EditorResizer Vue component in editor V2

Johan Ahlen 5 năm trước cách đây
mục cha
commit
e132540da1

+ 114 - 0
desktop/core/src/desktop/js/apps/notebook2/components/EditorResizer.vue

@@ -0,0 +1,114 @@
+<!--
+  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.
+-->
+
+<template>
+  <div class="snippet-code-resizer" @mousedown="start"><i class="fa fa-ellipsis-h" /></div>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+
+  import { HIDE_FIXED_HEADERS_EVENT, REDRAW_FIXED_HEADERS_EVENT } from 'apps/notebook2/events';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+  import { Ace } from 'ext/ace';
+  import huePubSub from 'utils/huePubSub';
+
+  const CUSTOM_HEIGHT_STORAGE_KEY = 'ace.editor.custom.height';
+  const TARGET_ELEMENT_SELECTOR = '.ace-editor-component';
+  const CONTENT_PANEL_SELECTOR = '.content-panel';
+
+  @Component
+  export default class EditorResizer extends Vue {
+    @Prop({ required: false })
+    editor?: Ace.Editor | null = null;
+
+    @Prop({ required: false })
+    panelSelector?: string;
+
+    subTracker = new SubscriptionTracker();
+
+    newHeight = 0;
+    startHeight = 0;
+    startY = 0;
+
+    targetElement: HTMLElement | null = null;
+
+    onMouseMove = this.drag.bind(this);
+    onMouseUp = this.stop.bind(this);
+
+    destroyed(): void {
+      document.removeEventListener('mousemove', this.onMouseMove);
+      document.removeEventListener('mouseup', this.onMouseUp);
+      this.subTracker.dispose();
+    }
+
+    drag(event: MouseEvent): void {
+      if (!this.targetElement || !this.editor) {
+        return;
+      }
+      const diff = event.clientY - this.startY;
+
+      this.newHeight = Math.max(this.startHeight + diff, 128);
+      this.targetElement.style.height = this.newHeight + 'px';
+      this.editor.resize();
+    }
+
+    findTarget(): void {
+      const contentPanel = this.$el.closest<HTMLElement>(CONTENT_PANEL_SELECTOR);
+      if (contentPanel) {
+        this.targetElement = contentPanel.querySelector<HTMLElement>(TARGET_ELEMENT_SELECTOR);
+      }
+      if (!this.targetElement) {
+        console.warn(
+          `EditorResizer could not find the target element '${TARGET_ELEMENT_SELECTOR}'.`
+        );
+        return;
+      }
+    }
+
+    start(event: MouseEvent): void {
+      if (!this.targetElement) {
+        this.findTarget();
+        if (!this.targetElement) {
+          return;
+        }
+      }
+      this.startY = event.clientY;
+      this.startHeight = this.targetElement.offsetHeight;
+      document.addEventListener('mousemove', this.onMouseMove);
+      document.addEventListener('mouseup', this.onMouseUp);
+      huePubSub.publish(HIDE_FIXED_HEADERS_EVENT);
+    }
+
+    stop(): void {
+      huePubSub.publish(REDRAW_FIXED_HEADERS_EVENT);
+      document.dispatchEvent(new Event('editorSizeChanged')); // TODO: Who listens?
+      document.removeEventListener('mousemove', this.onMouseMove);
+      document.removeEventListener('mouseup', this.onMouseUp);
+      localStorage.setItem(CUSTOM_HEIGHT_STORAGE_KEY, String(this.newHeight));
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .snippet-code-resizer {
+    user-select: none;
+  }
+</style>

+ 61 - 0
desktop/core/src/desktop/js/apps/notebook2/components/EditorResizerKoBridge.vue

@@ -0,0 +1,61 @@
+<!--
+  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.
+-->
+
+<template>
+  <editor-resizer :editor="editor" />
+</template>
+
+<script lang="ts">
+  import { Ace } from 'ext/ace';
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+  import { wrap } from 'vue/webComponentWrapper';
+
+  import EditorResizer from 'apps/notebook2/components/EditorResizer.vue';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+
+  @Component({
+    components: { EditorResizer }
+  })
+  export default class EditorResizerKoBridge extends Vue {
+    @Prop()
+    editorObservable?: KnockoutObservable<Ace.Editor | undefined>;
+
+    editor: Ace.Editor | null = null;
+    subTracker = new SubscriptionTracker();
+    initialized = false;
+
+    updated(): void {
+      if (!this.initialized && this.editorObservable) {
+        this.editor = this.editorObservable() || null;
+        this.subTracker.subscribe(this.editorObservable, editor => {
+          this.editor = editor;
+        });
+        this.initialized = true;
+      }
+    }
+
+    destroyed(): void {
+      this.subTracker.dispose();
+    }
+  }
+
+  export const COMPONENT_NAME = 'editor-resizer-ko-bridge';
+  wrap(COMPONENT_NAME, EditorResizerKoBridge);
+</script>

+ 5 - 7
desktop/core/src/desktop/js/apps/notebook2/components/ExecutableActionsKoBridge.vue

@@ -17,13 +17,11 @@
 -->
 
 <template>
-  <div>
-    <executable-actions
-      :executable="executable"
-      :before-execute="beforeExecute"
-      @limit-changed="limitChanged"
-    />
-  </div>
+  <executable-actions
+    :executable="executable"
+    :before-execute="beforeExecute"
+    @limit-changed="limitChanged"
+  />
 </template>
 
 <script lang="ts">

+ 12 - 7
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/AceEditor.vue

@@ -17,7 +17,7 @@
 -->
 
 <template>
-  <div>
+  <div class="ace-editor-component">
     <div :id="id" class="ace-editor" />
     <ace-autocomplete v-if="editor" :editor="editor" :editor-id="id" :executor="executor" />
   </div>
@@ -76,6 +76,10 @@
       if (!editorElement) {
         return;
       }
+
+      const height = localStorage.getItem('ace.editor.custom.height') || '128';
+      (<HTMLElement>this.$el).style.height = height + 'px';
+
       editorElement.textContent = this.initialValue;
       const editor = <Ace.Editor>ace.edit(editorElement);
 
@@ -325,13 +329,10 @@
       editor.on('change', triggerChange);
       editor.on('blur', triggerChange);
 
-      window.setTimeout(() => {
-        this.$emit('ace-created', editor);
-      }, 3000);
-
       editor.$blockScrolling = Infinity;
 
       this.editor = editor;
+      this.$emit('ace-created', editor);
     }
 
     destroyed(): void {
@@ -344,7 +345,11 @@
 </script>
 
 <style lang="scss" scoped>
-  .ace-editor {
-    height: 200px;
+  .ace-editor-component {
+    height: 100%;
+
+    .ace-editor {
+      height: 100%;
+    }
   }
 </style>

+ 9 - 10
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/AceEditorKoBridge.vue

@@ -17,16 +17,15 @@
 -->
 
 <template>
-  <div v-if="initialized && editorId">
-    <ace-editor
-      :id="editorId"
-      :executor="executor"
-      :ace-options="aceOptions"
-      :initial-value="value"
-      @value-changed="valueChanged"
-      @ace-created="aceCreated"
-    />
-  </div>
+  <ace-editor
+    v-if="initialized && editorId"
+    :id="editorId"
+    :executor="executor"
+    :ace-options="aceOptions"
+    :initial-value="value"
+    @value-changed="valueChanged"
+    @ace-created="aceCreated"
+  />
 </template>
 
 <script lang="ts">

+ 2 - 0
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/__snapshots__/AceEditor.test.ts.snap

@@ -2,6 +2,8 @@
 
 exports[`AceEditor.vue should render 1`] = `
 <div
+  class="ace-editor-component"
+  style="height: 128px;"
   value="some query"
 >
   <div

+ 25 - 19
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -26,6 +26,7 @@ import 'apps/notebook2/components/ko.snippetResults';
 import 'apps/notebook2/components/ko.queryHistory';
 
 import './components/ExecutableActionsKoBridge.vue';
+import './components/EditorResizerKoBridge.vue';
 import './components/aceEditor/AceEditorKoBridge.vue';
 
 import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
@@ -236,7 +237,15 @@ export default class Snippet {
       this.parentNotebook.isHistory() ? snippetRaw.aceCursorPosition : null
     );
 
-    this.aceEditor = null;
+    this.ace = ko.observable();
+
+    this.ace.subscribe(newVal => {
+      if (newVal) {
+        if (!this.parentNotebook.isPresentationMode()) {
+          newVal.focus();
+        }
+      }
+    });
 
     this.errors = ko.observableArray([]);
 
@@ -340,7 +349,7 @@ export default class Snippet {
     this.associatedDocumentUuid.subscribe(val => {
       if (val !== '') {
         this.getExternalStatement();
-      } else {
+      } else if (this.ace()) {
         this.statement_raw('');
         this.ace().setValue('', 1);
       }
@@ -444,7 +453,7 @@ export default class Snippet {
         this.properties(komapping.fromJS(getDefaultSnippetProperties(newValue)));
       }
       window.setTimeout(() => {
-        if (this.ace() !== null) {
+        if (this.ace()) {
           this.ace().focus();
         }
       }, 100);
@@ -697,10 +706,12 @@ export default class Snippet {
           this.hasSuggestion('error');
           this.complexity({ hints: [] });
         }
-        huePubSub.publish('editor.active.risks', {
-          editor: this.ace(),
-          risks: this.complexity() || {}
-        });
+        if (this.ace()) {
+          huePubSub.publish('editor.active.risks', {
+            editor: this.ace(),
+            risks: this.complexity() || {}
+          });
+        }
         lastCheckedComplexityStatement = this.statement();
       };
 
@@ -713,7 +724,7 @@ export default class Snippet {
           this.suggestion('');
         }
 
-        if (this.complexity() !== {}) {
+        if (this.complexity() !== {} && this.ace()) {
           this.complexity(undefined);
           huePubSub.publish('editor.active.risks', {
             editor: this.ace(),
@@ -831,16 +842,6 @@ export default class Snippet {
     }
   }
 
-  ace(newVal) {
-    if (newVal) {
-      this.aceEditor = newVal;
-      if (!this.parentNotebook.isPresentationMode()) {
-        this.aceEditor.focus();
-      }
-    }
-    return this.aceEditor;
-  }
-
   checkCompatibility() {
     this.hasSuggestion(null);
     this.compatibilitySourcePlatform(COMPATIBILITY_SOURCE_PLATFORMS[this.dialect()]);
@@ -891,7 +892,9 @@ export default class Snippet {
         if (data.status === 0) {
           this.externalStatementLoaded(true);
           this.statement_raw(data.statement);
-          this.ace().setValue(this.statement_raw(), 1);
+          if (this.ace()) {
+            this.ace().setValue(this.statement_raw(), 1);
+          }
         } else {
           this.handleAjaxError(data);
         }
@@ -1040,6 +1043,9 @@ export default class Snippet {
   }
 
   onKeydownInVariable(context, e) {
+    if (!this.ace()) {
+      return;
+    }
     if ((e.ctrlKey || e.metaKey) && e.which === 13) {
       // Ctrl-enter
       this.ace().commands.commands['execute'].exec();

+ 1 - 1
desktop/core/src/desktop/js/ext/ace.d.ts

@@ -67,7 +67,7 @@ declare namespace Ace {
       scrollCursorIntoView(position?: Position, u?: number): void;
       textToScreenCoordinates(row: number, column: number): { pageX: number; pageY: number }
     };
-    resize(force: boolean): void;
+    resize(force?: boolean): void;
     scrollToLine(line: number, u: boolean, v: boolean, callback: () => void): void;
     selection: {
       getRange(): Range;

+ 3 - 9
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -1232,15 +1232,9 @@
   </script>
 
   <script type="text/html" id="snippet-code-resizer${ suffix }">
-##     <div class="snippet-code-resizer" data-bind="
-##         aceResizer : {
-##           snippet: $data,
-##           target: '.ace-container-resizable',
-##           onStart: function () { huePubSub.publish('result.grid.hide.fixed.headers') },
-##           onStop: function () { huePubSub.publish('result.grid.redraw.fixed.headers') }
-##         }">
-##       <i class="fa fa-ellipsis-h"></i>
-##     </div>
+    <editor-resizer-ko-bridge data-bind="vueKoProps: {
+      editorObservable: ace
+    }"></editor-resizer-ko-bridge>
   </script>
 
   <script type="text/html" id="notebook-snippet-type-controls${ suffix }">