File size: 5,091 Bytes
8e04495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
from dataclasses import dataclass, field

import mesop as me

_INTRO_TEXT = """
# Mesop Markdown Editor Example

This example shows how to make a simple markdown editor.
""".strip()


@dataclass(kw_only=True)
class Note:
  """Content of note."""

  content: str = ""


@me.stateclass
class State:
  notes: list[Note] = field(default_factory=lambda: [Note(content=_INTRO_TEXT)])
  selected_note_index: int = 0
  selected_note_content: str = _INTRO_TEXT
  show_preview: bool = True


def load(e: me.LoadEvent):
  me.set_theme_mode("system")


@me.page(
  on_load=load,
  security_policy=me.SecurityPolicy(
    allowed_iframe_parents=["https://google.github.io", "https://huggingface.co."]
  ),
  path="/markdown_editor",
  title="Markdown Editor",
)
def page():
  state = me.state(State)

  with me.box(style=_style_container(state.show_preview)):
    # Note list column
    with me.box(style=_STYLE_NOTES_NAV):
      # Toolbar
      with me.box(style=_STYLE_TOOLBAR):
        with me.content_button(on_click=on_click_new):
          with me.tooltip(message="New note"):
            me.icon(icon="add_notes")
        with me.content_button(on_click=on_click_hide):
          with me.tooltip(
            message="Hide preview" if state.show_preview else "Show preview"
          ):
            me.icon(icon="hide_image")

      # Note list
      for index, note in enumerate(state.notes):
        with me.box(
          key=f"note-{index}",
          on_click=on_click_note,
          style=_style_note_row(index == state.selected_note_index),
        ):
          me.text(_render_note_excerpt(note.content))

    # Markdown Editor Column
    with me.box(style=_STYLE_EDITOR):
      me.native_textarea(
        value=state.selected_note_content,
        style=_STYLE_TEXTAREA,
        on_input=on_text_input,
      )

    # Markdown Preview Column
    if state.show_preview:
      with me.box(style=_STYLE_PREVIEW):
        if state.selected_note_index < len(state.notes):
          me.markdown(state.notes[state.selected_note_index].content)


# HELPERS

_EXCERPT_CHAR_LIMIT = 90


def _render_note_excerpt(content: str) -> str:
  if len(content) <= _EXCERPT_CHAR_LIMIT:
    return content
  return content[:_EXCERPT_CHAR_LIMIT] + "..."


# EVENT HANDLERS


def on_click_new(e: me.ClickEvent):
  state = me.state(State)
  # Need to update the initial value of the editor text area so we can
  # trigger a diff to reset the editor to empty. Need to yield this change.
  # for this to work.
  state.selected_note_content = state.notes[state.selected_note_index].content
  yield
  # Reset the initial value of the editor text area to empty since the new note
  # has no content.
  state.selected_note_content = ""
  state.notes.append(Note())
  state.selected_note_index = len(state.notes) - 1
  yield


def on_click_hide(e: me.ClickEvent):
  """Hides/Shows preview Markdown pane."""
  state = me.state(State)
  state.show_preview = bool(not state.show_preview)


def on_click_note(e: me.ClickEvent):
  """Selects a note from the note list."""
  state = me.state(State)
  note_id = int(e.key.replace("note-", ""))
  note = state.notes[note_id]
  state.selected_note_index = note_id
  state.selected_note_content = note.content


def on_text_input(e: me.InputEvent):
  """Captures text in editor."""
  state = me.state(State)
  state.notes[state.selected_note_index].content = e.value


# STYLES

_BACKGROUND_COLOR = me.theme_var("surface-container-lowest")
_FONT_COLOR = me.theme_var("on-surface-variant")
_NOTE_ROW_FONT_COLOR = me.theme_var("on-surface")
_NOTE_ROW_FONT_SIZE = "14px"
_SELECTED_ROW_BACKGROUND_COLOR = me.theme_var("surface-variant")
_DEFAULT_BORDER_STYLE = me.BorderSide(
  width=1, style="solid", color=me.theme_var("outline-variant")
)


def _style_container(show_preview: bool = True) -> me.Style:
  return me.Style(
    background=_BACKGROUND_COLOR,
    color=_FONT_COLOR,
    display="grid",
    grid_template_columns="2fr 4fr 4fr" if show_preview else "2fr 8fr",
    height="100vh",
  )


def _style_note_row(selected: bool = False) -> me.Style:
  return me.Style(
    color=_NOTE_ROW_FONT_COLOR,
    font_size=_NOTE_ROW_FONT_SIZE,
    background=_SELECTED_ROW_BACKGROUND_COLOR if selected else "none",
    padding=me.Padding.all(10),
    border=me.Border(bottom=_DEFAULT_BORDER_STYLE),
    height="100px",
    overflow_x="hidden",
    overflow_y="hidden",
  )


_STYLE_NOTES_NAV = me.Style(overflow_y="scroll", padding=me.Padding.all(15))


_STYLE_TOOLBAR = me.Style(
  padding=me.Padding.all(5),
  border=me.Border(bottom=_DEFAULT_BORDER_STYLE),
)


_STYLE_EDITOR = me.Style(
  overflow_y="hidden",
  padding=me.Padding(left=20, right=15, top=20, bottom=0),
  border=me.Border(
    left=_DEFAULT_BORDER_STYLE,
    right=_DEFAULT_BORDER_STYLE,
  ),
)


_STYLE_PREVIEW = me.Style(
  overflow_y="scroll", padding=me.Padding.symmetric(vertical=0, horizontal=20)
)


_STYLE_TEXTAREA = me.Style(
  color=_FONT_COLOR,
  background=_BACKGROUND_COLOR,
  outline="none",  # Hides focus border
  border=me.Border.all(me.BorderSide(style="none")),
  width="100%",
  height="100%",
)