Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add on_keyup and value_input for code editor #6919

Merged
merged 16 commits into from
Aug 20, 2024
4 changes: 2 additions & 2 deletions examples/reference/widgets/CodeEditor.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
"* **``print_margin``** (boolean): Whether to show a print margin in the editor\n",
"* **``theme``** (str): theme of the editor (default: 'chrome')\n",
"* **``readonly``** (boolean): Whether the editor should be opened in read-only mode\n",
"* **``value``** (str): A string with (initial) code to set in the editor\n",
"\n",
"* **``value``** (str): State of the current code in the editor upon loss of focus, i.e. clicking outside the editor\n",
"* **``value_input``** (str): State of the current code updated on every key press\n",
"___"
]
},
Expand Down
2 changes: 2 additions & 0 deletions panel/models/ace.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def __js_skip__(cls):

code = String(default='')

code_input = String(default='')

theme = Enum(ace_themes, default='chrome')

filename = Nullable(String())
Expand Down
11 changes: 10 additions & 1 deletion panel/models/ace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export class AcePlotView extends HTMLBoxView {
this._update_language()
this._editor.setReadOnly(this.model.readonly)
this._editor.setShowPrintMargin(this.model.print_margin)
this._editor.on("change", () => this._update_code_from_editor())
this._editor.on("blur", () => this._update_code_from_editor())
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
this._editor.on("change", () => this._update_code_input_from_editor())
}

_update_code_from_model(): void {
Expand All @@ -87,6 +88,12 @@ export class AcePlotView extends HTMLBoxView {
}
}

_update_code_input_from_editor(): void {
if (this._editor.getValue() != this.model.code_input) {
this.model.code_input = this._editor.getValue()
}
}

_update_theme(): void {
this._editor.setTheme(`ace/theme/${this.model.theme}`)
}
Expand Down Expand Up @@ -120,6 +127,7 @@ export namespace AcePlot {
export type Attrs = p.AttrsOf<Props>
export type Props = HTMLBox.Props & {
code: p.Property<string>
code_input: p.Property<string>
language: p.Property<string>
filename: p.Property<string | null>
theme: p.Property<string>
Expand All @@ -145,6 +153,7 @@ export class AcePlot extends HTMLBox {

this.define<AcePlot.Props>(({Any, List, Bool, Str, Nullable}) => ({
code: [ Str, "" ],
code_input: [ Str, "" ],
filename: [ Nullable(Str), null],
language: [ Str, "" ],
theme: [ Str, "chrome" ],
Expand Down
33 changes: 33 additions & 0 deletions panel/tests/ui/widgets/test_codeeditor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import pytest

pytest.importorskip("playwright")

from playwright.sync_api import expect

from panel.tests.util import serve_component, wait_until
from panel.widgets import CodeEditor

pytestmark = pytest.mark.ui


def test_code_editor(page):

editor = CodeEditor(value="print('Hello World!')")
serve_component(page, editor)
ace_input = page.locator(".ace_content")
expect(ace_input).to_have_count(1)
ace_input.click()

page.keyboard.press("Enter")
page.keyboard.type('print("Hello Panel!")')
wait_until(
lambda: page.locator(".ace_content").inner_text()
== "print('Hello World!')\nprint(\"Hello Panel!\")"
)
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
wait_until(lambda: editor.value_input == "print('Hello World!')\nprint(\"Hello Panel!\")")
assert editor.value == "print('Hello World!')"

# page click outside the editor
page.locator("body").click()
assert editor.value_input == "print('Hello World!')\nprint(\"Hello Panel!\")"
wait_until(lambda: editor.value == "print('Hello World!')\nprint(\"Hello Panel!\")")
11 changes: 11 additions & 0 deletions panel/tests/widgets/test_codeeditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,14 @@ def test_ace(document, comm):
# Try changes
editor._process_events({"value": "Hi there!"})
assert editor.value == "Hi there!"


def test_ace_input(document, comm):
editor = CodeEditor(value="", language="python")
editor.value = "Hello World!"
assert editor.value == "Hello World!"
assert editor.value_input == "Hello World!"

editor.value = ""
assert editor.value == ""
assert editor.value_input == ""
11 changes: 9 additions & 2 deletions panel/widgets/codeeditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ class CodeEditor(Widget):
theme = param.ObjectSelector(default="chrome", objects=list(ace_themes),
doc="Theme of the editor")

value = param.String(doc="State of the current code in the editor")
value = param.String(doc="""
State of the current code in the editor upon loss of focus, i.e. clicking outside the editor.""")

_rename: ClassVar[Mapping[str, str | None]] = {"value": "code", "name": None}
value_input = param.String(doc="State of the current code updated on every key press.")

_rename: ClassVar[Mapping[str, str | None]] = {"value": "code", "value_input": "code_input", "name": None}

def __init__(self, **params):
if 'readonly' in params:
Expand All @@ -64,6 +67,10 @@ def __init__(self, **params):
)
self.jslink(self, readonly='disabled', bidirectional=True)

@param.depends("value", watch=True)
def _update_value_input(self):
self.value_input = self.value

def _get_model(
self, doc: Document, root: Optional[Model] = None,
parent: Optional[Model] = None, comm: Optional[Comm] = None
Expand Down