-
+
+
{
+ document.addEventListener("mousedown", this.onMouseDown, { passive: true });
+ document.addEventListener("mouseup", this.onMouseUp, { passive: true });
+ document.addEventListener("selectionchange", this.onSelectionChanged);
+
+ this.dEditorInput = document.querySelector(".d-editor-input");
+
+ if (this.dEditorInput) {
+ this.dEditorInput.addEventListener("scroll", this.updatePosition);
+ }
+
+ return () => {
+ document.removeEventListener("mousedown", this.onMouseDown);
+ document.removeEventListener("mouseup", this.onMouseUp);
+ document.removeEventListener("selectionchange", this.onSelectionChanged);
+
+ if (this.dEditorInput) {
+ this.dEditorInput.removeEventListener("scroll", this.updatePosition);
+ }
+ };
+ });
+
+ willDestroy() {
+ super.willDestroy(...arguments);
+ this.menuInstance?.close();
+ }
+
+ @bind
+ onSelectionChanged() {
+ if (
+ this.aiComposerHelper.menuState !==
+ this.aiComposerHelper.MENU_STATES.triggers
+ ) {
+ return;
+ }
+
+ if (this.isSelecting) {
+ return;
+ }
+
+ if (document.activeElement !== this.dEditorInput) {
+ return;
+ }
+
+ const canSelect = Boolean(
+ window.getSelection() &&
+ document.activeElement &&
+ document.activeElement.value
+ );
+
+ this.selectedText = canSelect
+ ? document.activeElement.value.substring(
+ document.activeElement.selectionStart,
+ document.activeElement.selectionEnd
+ )
+ : "";
+
+ this.selectionRange = canSelect
+ ? {
+ x: document.activeElement.selectionStart,
+ y: document.activeElement.selectionEnd,
+ }
+ : { x: 0, y: 0 };
+
+ if (this.selectedText?.length === 0) {
+ this.menuInstance?.close();
+ return;
+ }
+ if (this.selectedText?.length < this.minSelectionChars) {
+ return;
+ }
+
+ this.selectionChanged();
+ }
+
+ @debounce(INPUT_DELAY)
+ selectionChanged() {
+ this.positionMenu();
+ }
+
+ @bind
+ onMouseDown() {
+ this.isSelecting = true;
+ }
+
+ @bind
+ onMouseUp() {
+ this.isSelecting = false;
+ this.onSelectionChanged();
+ }
+
+ @bind
+ updatePosition() {
+ if (!this.menuInstance) {
+ return;
+ }
+
+ this.positionMenu();
+ }
+
+ @afterRender
+ async positionMenu() {
+ this.caretCoords = getCaretPosition(this.dEditorInput, {
+ pos: caretPosition(this.dEditorInput),
+ });
+ const virtualElement = virtualElementFromCaretCoords(
+ this.caretCoords,
+ this.menuOffset
+ );
+
+ if (this.handleBoundaries(virtualElement)) {
+ return;
+ }
+
+ // Position context menu at based on if interfering with button bar
+ this.menuInstance = await this.menu.show(virtualElement, {
+ identifier: "ai-composer-helper-menu",
+ placement: this.menuPlacement,
+ component: AiComposerHelperMenu,
+ inline: true,
+ modalForMobile: false,
+ data: {
+ selectedText: this.selectedText,
+ dEditorInput: this.dEditorInput,
+ selectionRange: this.selectionRange,
+ },
+ interactive: true,
+ onClose: () => {
+ this.aiComposerHelper.menuState =
+ this.aiComposerHelper.MENU_STATES.triggers;
+ },
+ });
+ this.menuElement = this.menuInstance.content;
+ }
+
+ handleBoundaries(virtualElement) {
+ const textAreaWrapper = document
+ .querySelector(".d-editor-textarea-wrapper")
+ .getBoundingClientRect();
+ const buttonBar = document
+ .querySelector(".d-editor-button-bar")
+ .getBoundingClientRect();
+ const boundaryElement = {
+ top: buttonBar.bottom,
+ bottom: textAreaWrapper.bottom,
+ };
+
+ const menuHeightBuffer = 35; // rough estimate of menu height since we can't get actual in this context.
+ if (this.caretCoords.y - menuHeightBuffer < boundaryElement.top) {
+ this.menuPlacement = "bottom-start";
+ } else {
+ this.menuPlacement = "top-start";
+ }
+
+ if (this.isScrolledOutOfBounds(boundaryElement, virtualElement)) {
+ this.menuInstance?.close();
+ return true;
+ }
+ }
+
+ isScrolledOutOfBounds(boundaryElement, virtualElement) {
+ // Hide context menu if it's scrolled out of bounds:
+ if (virtualElement.rect.top < boundaryElement.top) {
+ return true;
+ } else if (virtualElement.rect.bottom > boundaryElement.bottom) {
+ return true;
+ }
+
+ return false;
+ }
+
+
+
+
+}
diff --git a/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.hbs b/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.hbs
deleted file mode 100644
index bb7426e1..00000000
--- a/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.hbs
+++ /dev/null
@@ -1,93 +0,0 @@
-
- {{#if this.showContextMenu}}
-
- {{/if}}
-
-
-{{#if this.showDiffModal}}
-
-{{/if}}
-
-{{#if this.showThumbnailModal}}
-
-{{/if}}
\ No newline at end of file
diff --git a/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js b/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js
deleted file mode 100644
index 911721d5..00000000
--- a/assets/javascripts/discourse/connectors/after-d-editor/ai-helper-context-menu.js
+++ /dev/null
@@ -1,444 +0,0 @@
-import Component from "@glimmer/component";
-import { tracked } from "@glimmer/tracking";
-import { action } from "@ember/object";
-import { inject as service } from "@ember/service";
-import { createPopper } from "@popperjs/core";
-import { ajax } from "discourse/lib/ajax";
-import { popupAjaxError } from "discourse/lib/ajax-error";
-import { caretPosition, getCaretPosition } from "discourse/lib/utilities";
-import { INPUT_DELAY } from "discourse-common/config/environment";
-import { afterRender, bind, debounce } from "discourse-common/utils/decorators";
-import I18n from "discourse-i18n";
-import { showComposerAIHelper } from "../../lib/show-ai-helper";
-
-export default class AiHelperContextMenu extends Component {
- static shouldRender(outletArgs, helper) {
- return showComposerAIHelper(outletArgs, helper, "context_menu");
- }
-
- @service currentUser;
- @service siteSettings;
- @service modal;
- @service capabilities;
- @tracked showContextMenu = false;
- @tracked caretCoords;
- @tracked virtualElement;
- @tracked selectedText = "";
- @tracked newSelectedText;
- @tracked loading = false;
- @tracked lastUsedOption = null;
- @tracked showDiffModal = false;
- @tracked showThumbnailModal = false;
- @tracked diff;
- @tracked popperPlacement = "top-start";
- @tracked previousMenuState = null;
- @tracked customPromptValue = "";
- @tracked initialValue = "";
- @tracked thumbnailSuggestions = null;
- @tracked selectionRange = { x: 0, y: 0 };
- @tracked lastSelectionRange = null;
-
- CONTEXT_MENU_STATES = {
- triggers: "TRIGGERS",
- options: "OPTIONS",
- resets: "RESETS",
- loading: "LOADING",
- review: "REVIEW",
- };
- prompts = [];
- promptTypes = {};
- minSelectionChars = 3;
-
- @tracked _menuState = this.CONTEXT_MENU_STATES.triggers;
- @tracked _popper;
- @tracked _dEditorInput;
- @tracked _customPromptInput;
- @tracked _contextMenu;
- @tracked _activeAIRequest = null;
-
- willDestroy() {
- super.willDestroy(...arguments);
- document.removeEventListener("selectionchange", this.selectionChanged);
- document.removeEventListener("keydown", this.onKeyDown);
- this._popper?.destroy();
- }
-
- get menuState() {
- return this._menuState;
- }
-
- set menuState(newState) {
- this.previousMenuState = this._menuState;
- this._menuState = newState;
- }
-
- get helperOptions() {
- let prompts = this.currentUser?.ai_helper_prompts;
-
- prompts = prompts
- .filter((p) => p.location.includes("composer"))
- .filter((p) => p.name !== "generate_titles")
- .map((p) => {
- // AI helper by default returns interface locale on translations
- // Since we want site default translations (and we are using: force_default_locale)
- // we need to replace the translated_name with the site default locale name
- const siteLocale = this.siteSettings.default_locale;
- const availableLocales = JSON.parse(
- this.siteSettings.available_locales
- );
- const locale = availableLocales.find((l) => l.value === siteLocale);
- const translatedName = I18n.t(
- "discourse_ai.ai_helper.context_menu.translate_prompt",
- {
- language: locale.name,
- }
- );
-
- if (p.name === "translate") {
- return { ...p, translated_name: translatedName };
- }
- return p;
- });
-
- // Find the custom_prompt object and move it to the beginning of the array
- const customPromptIndex = prompts.findIndex(
- (p) => p.name === "custom_prompt"
- );
- if (customPromptIndex !== -1) {
- const customPrompt = prompts.splice(customPromptIndex, 1)[0];
- prompts.unshift(customPrompt);
- }
-
- if (!this._showUserCustomPrompts()) {
- prompts = prompts.filter((p) => p.name !== "custom_prompt");
- }
-
- prompts.forEach((p) => {
- this.prompts[p.id] = p;
- });
-
- this.promptTypes = prompts.reduce((memo, p) => {
- memo[p.name] = p.prompt_type;
- return memo;
- }, {});
- return prompts;
- }
-
- @bind
- selectionChanged() {
- if (document.activeElement !== this._dEditorInput) {
- return;
- }
-
- const canSelect = Boolean(
- window.getSelection() &&
- document.activeElement &&
- document.activeElement.value
- );
-
- this.selectedText = canSelect
- ? document.activeElement.value.substring(
- document.activeElement.selectionStart,
- document.activeElement.selectionEnd
- )
- : "";
-
- this.selectionRange = canSelect
- ? {
- x: document.activeElement.selectionStart,
- y: document.activeElement.selectionEnd,
- }
- : { x: 0, y: 0 };
-
- if (this.selectedText?.length === 0) {
- this.closeContextMenu();
- return;
- }
-
- if (this.selectedText?.length < this.minSelectionChars) {
- return;
- }
-
- this._onSelectionChanged();
- }
-
- @bind
- updatePosition() {
- if (!this.showContextMenu) {
- return;
- }
-
- this.positionContextMenu();
- }
-
- @bind
- onKeyDown(event) {
- if (event.key === "Escape") {
- return this.closeContextMenu();
- }
- if (
- event.key === "Backspace" &&
- this.selectedText &&
- this.menuState === this.CONTEXT_MENU_STATES.triggers
- ) {
- return this.closeContextMenu();
- }
- }
-
- @debounce(INPUT_DELAY)
- _onSelectionChanged() {
- this.positionContextMenu();
- this.showContextMenu = true;
- }
-
- generateGetBoundingClientRect(width = 0, height = 0, x = 0, y = 0) {
- return () => ({
- width,
- height,
- top: y,
- right: x,
- bottom: y,
- left: x,
- });
- }
-
- get canCloseContextMenu() {
- if (document.activeElement === this._customPromptInput) {
- return false;
- }
-
- if (this.loading && this._activeAIRequest !== null) {
- return false;
- }
-
- if (this.menuState === this.CONTEXT_MENU_STATES.review) {
- return false;
- }
-
- return true;
- }
-
- closeContextMenu() {
- if (!this.canCloseContextMenu) {
- return;
- }
- this.showContextMenu = false;
- this.menuState = this.CONTEXT_MENU_STATES.triggers;
- this.customPromptValue = "";
- }
-
- _updateSuggestedByAI(data) {
- this.newSelectedText = data.suggestions[0];
-
- if (data.diff) {
- this.diff = data.diff;
- }
-
- this._insertAt(
- this.selectionRange.x,
- this.selectionRange.y,
- this.newSelectedText
- );
- this.menuState = this.CONTEXT_MENU_STATES.review;
- }
-
- _insertAt(start, end, text) {
- this._dEditorInput.setSelectionRange(start, end);
- this._dEditorInput.focus();
- document.execCommand("insertText", false, text);
- }
-
- _toggleLoadingState(loading) {
- if (loading) {
- this._dEditorInput.classList.add("loading");
- return (this.loading = true);
- }
-
- this._dEditorInput.classList.remove("loading");
- return (this.loading = false);
- }
-
- _showUserCustomPrompts() {
- return this.currentUser?.can_use_custom_prompts;
- }
-
- handleBoundaries() {
- const textAreaWrapper = document
- .querySelector(".d-editor-textarea-wrapper")
- .getBoundingClientRect();
- const buttonBar = document
- .querySelector(".d-editor-button-bar")
- .getBoundingClientRect();
-
- const boundaryElement = {
- top: buttonBar.bottom,
- bottom: textAreaWrapper.bottom,
- };
-
- const contextMenuRect = this._contextMenu.getBoundingClientRect();
-
- // Hide context menu if it's scrolled out of bounds:
- if (contextMenuRect.top < boundaryElement.top) {
- this._contextMenu.classList.add("out-of-bounds");
- } else if (contextMenuRect.bottom > boundaryElement.bottom) {
- this._contextMenu.classList.add("out-of-bounds");
- } else {
- this._contextMenu.classList.remove("out-of-bounds");
- }
-
- // Position context menu at based on if interfering with button bar
- if (this.caretCoords.y - contextMenuRect.height < boundaryElement.top) {
- this.popperPlacement = "bottom-start";
- } else {
- this.popperPlacement = "top-start";
- }
- }
-
- @afterRender
- positionContextMenu() {
- this._contextMenu = document.querySelector(".ai-helper-context-menu");
-
- if (!this._dEditorInput || !this._contextMenu) {
- return;
- }
-
- this.caretCoords = getCaretPosition(this._dEditorInput, {
- pos: caretPosition(this._dEditorInput),
- });
-
- // prevent overflow of context menu outside of editor
- this.handleBoundaries();
-
- this.virtualElement = {
- getBoundingClientRect: this.generateGetBoundingClientRect(
- this._contextMenu.clientWidth,
- this._contextMenu.clientHeight,
- this.caretCoords.x,
- this.caretCoords.y
- ),
- };
-
- this._popper = createPopper(this.virtualElement, this._contextMenu, {
- placement: this.popperPlacement,
- modifiers: [
- {
- name: "offset",
- options: {
- offset: [10, 0],
- },
- },
- ],
- });
- }
-
- @action
- setupContextMenu() {
- document.addEventListener("selectionchange", this.selectionChanged);
- document.addEventListener("keydown", this.onKeyDown);
-
- this._dEditorInput = document.querySelector(".d-editor-input");
-
- if (this._dEditorInput) {
- this._dEditorInput.addEventListener("scroll", this.updatePosition);
- }
- }
-
- @action
- setupCustomPrompt() {
- this._customPromptInput = document.querySelector(
- ".ai-custom-prompt__input"
- );
- this._customPromptInput.focus();
- }
-
- @action
- toggleAiHelperOptions() {
- this.menuState = this.CONTEXT_MENU_STATES.options;
- }
-
- @action
- undoAIAction() {
- if (this.capabilities.isFirefox) {
- // execCommand("undo") is no not supported in Firefox so we insert old text at range
- // we also need to calculate the length diffrence between the old and new text
- const lengthDifference =
- this.selectedText.length - this.initialValue.length;
- const end = this.lastSelectionRange.y - lengthDifference;
- this._insertAt(this.lastSelectionRange.x, end, this.initialValue);
- } else {
- document.execCommand("undo", false, null);
- }
-
- // context menu is prevented from closing when in review state
- // so we change to reset state quickly before closing
- this.menuState = this.CONTEXT_MENU_STATES.resets;
- this.closeContextMenu();
- }
-
- @action
- async updateSelected(option) {
- this._toggleLoadingState(true);
- this.lastUsedOption = option;
- this.menuState = this.CONTEXT_MENU_STATES.loading;
- this.initialValue = this.selectedText;
- this.lastSelectionRange = this.selectionRange;
-
- this._activeAIRequest = ajax("/discourse-ai/ai-helper/suggest", {
- method: "POST",
- data: {
- mode: option.id,
- text: this.selectedText,
- custom_prompt: this.customPromptValue,
- force_default_locale: true,
- },
- });
-
- this._activeAIRequest
- .then((data) => {
- // resets the values if new suggestion is started:
- this.diff = null;
- this.newSelectedText = null;
- this.thumbnailSuggestions = null;
-
- if (option.name === "illustrate_post") {
- this._toggleLoadingState(false);
- this.closeContextMenu();
- this.showThumbnailModal = true;
- this.thumbnailSuggestions = data.thumbnails;
- } else {
- this._updateSuggestedByAI(data);
- }
- })
- .catch(popupAjaxError)
- .finally(() => {
- this._toggleLoadingState(false);
- });
-
- return this._activeAIRequest;
- }
-
- @action
- viewChanges() {
- this.showDiffModal = true;
- }
-
- @action
- confirmChanges() {
- this.menuState = this.CONTEXT_MENU_STATES.resets;
- }
-
- @action
- cancelAIAction() {
- if (this._activeAIRequest) {
- this._activeAIRequest.abort();
- this._activeAIRequest = null;
- this._toggleLoadingState(false);
- this.closeContextMenu();
- }
- }
-
- @action
- togglePreviousMenu() {
- this.menuState = this.previousMenuState;
- }
-}
diff --git a/assets/javascripts/discourse/lib/virtual-element-from-caret-coords.js b/assets/javascripts/discourse/lib/virtual-element-from-caret-coords.js
new file mode 100644
index 00000000..eb0b76c9
--- /dev/null
+++ b/assets/javascripts/discourse/lib/virtual-element-from-caret-coords.js
@@ -0,0 +1,45 @@
+class VirtualElementFromCaretCoords {
+ constructor(caretCoords, offset = [0, 0]) {
+ this.caretCoords = caretCoords;
+ this.offset = offset;
+ this.updateRect();
+ }
+
+ updateRect() {
+ const [xOffset, yOffset] = this.offset;
+ this.rect = {
+ top: this.caretCoords.y + yOffset,
+ right: this.caretCoords.x,
+ bottom: this.caretCoords.y + yOffset,
+ left: this.caretCoords.x + xOffset,
+ width: 0,
+ height: 0,
+ x: this.caretCoords.x,
+ y: this.caretCoords.y,
+ toJSON() {
+ return this;
+ },
+ };
+ return this.rect;
+ }
+
+ getBoundingClientRect() {
+ return this.rect;
+ }
+
+ getClientRects() {
+ return [this.rect];
+ }
+
+ get clientWidth() {
+ return this.rect.width;
+ }
+
+ get clientHeight() {
+ return this.rect.height;
+ }
+}
+
+export default function virtualElementFromCaretCoords(caretCoords, offset) {
+ return new VirtualElementFromCaretCoords(caretCoords, offset);
+}
diff --git a/assets/javascripts/discourse/services/ai-composer-helper.js b/assets/javascripts/discourse/services/ai-composer-helper.js
new file mode 100644
index 00000000..3217f655
--- /dev/null
+++ b/assets/javascripts/discourse/services/ai-composer-helper.js
@@ -0,0 +1,14 @@
+import { tracked } from "@glimmer/tracking";
+import Service from "@ember/service";
+
+export default class AiComposerHelper extends Service {
+ @tracked menuState = this.MENU_STATES.triggers;
+
+ MENU_STATES = {
+ triggers: "TRIGGERS",
+ options: "OPTIONS",
+ resets: "RESETS",
+ loading: "LOADING",
+ review: "REVIEW",
+ };
+}
diff --git a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
index 74fc05da..45f4bda1 100644
--- a/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
+++ b/assets/stylesheets/modules/ai-helper/common/ai-helper.scss
@@ -31,7 +31,7 @@
margin: 4.5em 0 1em;
}
-.ai-helper-context-menu {
+.ai-composer-helper-menu {
background: var(--secondary);
box-shadow: var(--shadow-card);
padding: 0.25rem;
@@ -40,16 +40,16 @@
list-style: none;
z-index: 999;
- &.out-of-bounds {
- visibility: hidden;
- pointer-events: none;
- }
-
ul {
margin: 0;
list-style: none;
}
+}
+.ai-helper-button-group {
+ display: flex;
+ align-items: center;
+ flex-flow: row wrap;
&__resets {
display: flex;
align-items: center;
@@ -606,3 +606,7 @@
}
}
}
+
+.fk-d-menu[data-identifier="ai-composer-helper-menu"] {
+ z-index: z("composer", "dropdown");
+}
diff --git a/spec/system/ai_helper/ai_composer_helper_spec.rb b/spec/system/ai_helper/ai_composer_helper_spec.rb
index c8c151a5..2c5d79ac 100644
--- a/spec/system/ai_helper/ai_composer_helper_spec.rb
+++ b/spec/system/ai_helper/ai_composer_helper_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
let(:input) { "The rain in spain stays mainly in the Plane." }
let(:composer) { PageObjects::Components::Composer.new }
- let(:ai_helper_context_menu) { PageObjects::Components::AIHelperContextMenu.new }
+ let(:ai_helper_context_menu) { PageObjects::Components::AiComposerHelperMenu.new }
let(:diff_modal) { PageObjects::Modals::DiffModal.new }
let(:ai_suggestion_dropdown) { PageObjects::Components::AISuggestionDropdown.new }
fab!(:category)
@@ -239,21 +239,7 @@ RSpec.describe "AI Composer helper", type: :system, js: true do
input.gsub(/[[:space:]]+/, " ").gsub(/[‘’]/, "'").gsub(/[“”]/, '"').strip,
)
diff_modal.confirm_changes
- expect(ai_helper_context_menu).to be_showing_resets
- end
- end
-
- it "should not close the context menu when in review state" do
- trigger_context_menu(spanish_input)
- ai_helper_context_menu.click_ai_button
-
- DiscourseAi::Completions::Llm.with_prepared_responses([input]) do
- ai_helper_context_menu.select_helper_model(mode)
-
- wait_for { composer.composer_input.value == input }
-
- find(".d-editor-preview").click
- expect(ai_helper_context_menu).to have_context_menu
+ expect(ai_helper_context_menu).to have_no_context_menu
end
end
end
diff --git a/spec/system/page_objects/components/ai_helper_context_menu.rb b/spec/system/page_objects/components/ai_composer_helper_menu.rb
similarity index 91%
rename from spec/system/page_objects/components/ai_helper_context_menu.rb
rename to spec/system/page_objects/components/ai_composer_helper_menu.rb
index 51f05b8a..cf9d7fd2 100644
--- a/spec/system/page_objects/components/ai_helper_context_menu.rb
+++ b/spec/system/page_objects/components/ai_composer_helper_menu.rb
@@ -2,12 +2,12 @@
module PageObjects
module Components
- class AIHelperContextMenu < PageObjects::Components::Base
+ class AiComposerHelperMenu < PageObjects::Components::Base
COMPOSER_EDITOR_SELECTOR = ".d-editor-input"
- CONTEXT_MENU_SELECTOR = ".ai-helper-context-menu"
- TRIGGER_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__trigger"
+ CONTEXT_MENU_SELECTOR = ".ai-composer-helper-menu"
+ TRIGGER_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__triggers"
OPTIONS_STATE_SELECTOR = ".ai-helper-options"
- LOADING_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__loading"
+ LOADING_STATE_SELECTOR = ".ai-helper-loading"
RESETS_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__resets"
REVIEW_STATE_SELECTOR = "#{CONTEXT_MENU_SELECTOR}__review"
CUSTOM_PROMPT_SELECTOR = "#{CONTEXT_MENU_SELECTOR} .ai-custom-prompt"
@@ -41,7 +41,7 @@ module PageObjects
end
def press_undo_keys
- find(COMPOSER_EDITOR_SELECTOR).send_keys([:control, "z"])
+ find(COMPOSER_EDITOR_SELECTOR).send_keys([PLATFORM_KEY_MODIFIER, "z"])
end
def press_escape_key