try:
  import atlastk
except:
  !pip install atlastk
  import atlastk

atlastk.setJupyterHeight("500px") # Adjusting the height of the iframe in which the application will be displayed…

"""
Explanation: Step by step writing of an application using the Atlas toolkit
If you haven't already done so, please take a look at this FAQ, especially if you run this notebook on Google Colab.
About this notebook
This notebook will show the programmation, using the Atlas toolkit, of a basic contacts managing application, which will look like this:
.
This notebook will focus on the use of the Atlas toolkit; the reader is supposed to have basic knowledge of Python and HTML/CSS.
Preparation
If the atlastk module is not installed, it will be installed from Pypi.
Don't worry, it's a very lightweight package (~20 Ko), and it has no dependencies…
End of explanation
"""


import IPython, base64

def show(html,height):
    return IPython.display.IFrame(f"data:text/html;base64,{base64.b64encode(html.encode()).decode('utf-8') }".replace("+","%2B"), "100%", f"{height}px")

"""
Explanation: Let's define a little function which will be useful to display HTML code.
End of explanation
"""


BODY_CONTACT = """
<fieldset id="Contact">
  <table style="margin: auto;">
    <tr>
      <td>
        <label for="Name">Full name:</label>
      </td>
      <td>
        <input id="Name" size="50" />
      </td>
    </tr>
    <tr>
      <td>
        <label for="Address">Address:</label>
      </td>
      <td>
        <input id="Address" size="50" />
      </td>
    </tr>
    <tr>
      <td>
        <label for="Phone">Phone:</label>
      </td>
      <td>
        <input id="Phone" type="tel" size="50" />
      </td>
    </tr>
    <tr>
      <td>
        <label for="Note">Note:</label>
      </td>
      <td>
        <textarea id="Note" style="width: 100%;"></textarea>
      </td>
    </tr>
  </table>
</fieldset>
"""

"""
Explanation: The main HTML code
The HTML code of the main page of the application, beginning with the part displaing a contact.
End of explanation
"""


show(BODY_CONTACT,200)

"""
Explanation: Let's see what it looks like.
End of explanation
"""


BODY_BUTTONS = """
<div style="display: table; margin: 10px auto auto auto;">
  <div>
    <button class="Display" xdh:onevent="New">New</button>
    <span class="DisplayAndSelect">
      <button xdh:onevent="Edit">Edit</button>
      <button xdh:onevent="Delete">Delete</button>
    </span>
  </div>
  <div class="Edition">
    <button xdh:onevent="Cancel">Cancel</button>
    <button xdh:onevent="Submit">Submit</button>
  </div>
</div>
"""

"""
Explanation: HTML code for the buttons.
The label of the class attributes refers to the applications context:
- Display: the application is in display mode,
- DisplayAndSelect: the application is in display mode with a contact being selected,
- Edition: the application is in edition mode.
A button or a set of buttons (through a div element) with a given class has to be displayed when the application is in corresponding context, or hide otherwise. This is done by modifying the rules of the relevant class.
End of explanation
"""


show(BODY_BUTTONS,100)

"""
Explanation: Of course, not all buttons will be displayed when the application is dunning; the code which will show/hide the buttons following the context will be seen later.
End of explanation
"""


BODY_FORM = f"""
<fieldset>
  {BODY_CONTACT}
  {BODY_BUTTONS}
</fieldset>    
"""

show(BODY_FORM,300)

"""
Explanation: Now, let's put both part together.
End of explanation
"""


BODY_CONTACTS = """
<div style="display: table; margin: 10px auto auto auto; border-collapse: collapse;">
  <table id="Contacts" style="cursor: default; border-collapse: collapse;">
    <thead>
      <th>Name</th>
      <th>Address</th>
      <th>Phone</th>
      <th>Note</th>
    </thead>
    <tbody id="Content">
        <tr id="0" style="cursor: pointer;">
            <td>Holmes, Sherlock</td>
            <td>221B Baker Street, Londres</td>
            <td>(use telegraph)</td><td>Great detective!</td>
        </tr>
        <tr id="1" style="cursor: pointer;">
            <td>Holmes, Mycroft</td>
            <td>Diogenes Club, Pall Mall, Londres</td>
            <td>(use telegraph)</td>
            <td>Works for the British government. Brother of Holmes, Sherlock.</td>
        </tr>
  </table>
</div>
"""

"""
Explanation: The HTML code displaying all the contacts, with some content. Of course, the content of the tbody element will be generated dynamically.
End of explanation
"""


show(BODY_CONTACTS,100)

"""
Explanation: Looks a little bit messy, but this will change with the coming CSS rules.
End of explanation
"""


BODY = BODY_FORM + BODY_CONTACTS

"""
Explanation: And the final HTML code.
End of explanation
"""


show(BODY,400)

"""
Explanation: Which will look like this:
End of explanation
"""


HEAD_LOOK = """
<title>Address book</title>
<link rel="icon" type="image/png"
  href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAMFBMVEUEAvyEhsxERuS8urQsKuycnsRkYtzc2qwUFvRUVtysrrx0ctTs6qTMyrSUksQ0NuyciPBdAAABHklEQVR42mNgwAa8zlxjDd2A4POfOXPmzZkFCAH2M8fNzyALzDlzg2ENssCbMwkMOsgCa858YOjBKxBzRoHhD7LAHiBH5swCT9HQ6A9ggZ4zp7YCrV0DdM6pBpAAG5Blc2aBDZA68wCsZPuZU0BDH07xvHOmAGKKvgMP2NA/Zw7ADIYJXGDgLQeBBSCBFu0aoAPYQUadMQAJAE29zwAVWMCWpgB08ZnDQGsbGhpsgCqBQHNfzRkDEIPlzFmo0T5nzoMovjPHoAK8Zw5BnA5yDosDSAVYQOYMKIDZzkoDzagAsjhqzjRAfXTmzAQgi/vMQZA6pjtAvhEk0E+ATWRRm6YBZuScCUCNN5szH1D4TGdOoSrggtiNAH3vBBjwAQCglIrSZkf1MQAAAABJRU5ErkJggg==" />
<style>
  #Contact label {
    font-weight: bold;
  }

  #Contact span {
    text-align: left;
  }

  #Contacts th,
  #Contacts td {
    border: 1px solid black;
    padding: 0% 5px;
  }

  #Contacts td:nth-child(3) {
    text-align: right;
  }

  #Contacts tr:nth-child(even)
  {
    background: #CCC
  }

  #Contacts tr:nth-child(odd)
  {
    background: #FFF
  }

  #Contact *:disabled {
    background-color: snow;
    color: initial;
  }
</style>
"""

"""
Explanation: The meta-data HTML code
Now the HTML code which will be placed in the head section of the main HTML page, beginning with the part dedicated to the appearance of the application.
End of explanation
"""


HEAD_BUTTONS = """
<style id="HideDisplay">
  .Display {
    display: none;
  }
</style>
<style id="HideDisplayAndSelect">
  .DisplayAndSelect {
    display: none;
  }
</style>
<style id="HideEdition">
  .Edition {
    display: none;
  }
</style>
"""

"""
Explanation: And now the part with the elements containing CSS rules which will be used to show/hide the buttons.
End of explanation
"""


HEAD = HEAD_LOOK + HEAD_BUTTONS

"""
Explanation: Let's put both part together.
End of explanation
"""


def ac_connect(dom):
  dom.inner("",BODY) # The "" id refers to the root element.

"""
Explanation: Displaying the interface
This is the function which will be called on each new connection, and which will display the application's main page.
End of explanation
"""


callbacks = {
    "": ac_connect # The "" action label is for the connection action, which will be called on each new connection.
} 

"""
Explanation: Let's assign this function to the connection action.
Apart from the empty key (""), all keys of the callbacks dictionnary will refer to the a value of a xdh:onevent in the HTML code.
End of explanation
"""


atlastk.launch(callbacks,headContent = HEAD)

"""
Explanation: And let's launch the application. At this point, you cannot interact with the application.
End of explanation
"""


EXAMPLE = [
  {
    "Name": "Holmes, Sherlock",
    "Address": "221B Baker Street, Londres",
    "Phone": "(use telegraph)",
    "Note": "Great detective!"
  },
  {
    "Name": "Holmes, Mycroft",
    "Address": "Diogenes Club, Pall Mall, Londres",
    "Phone": "(use telegraph)",
    "Note": "Works for the British government.\nBrother of Holmes, Sherlock."
  },
  {
    "Name": "Tintin",
    "Address": "Château de Moulinsart",
    "Phone": "421",
    "Note": "Has a dog named Snowy."
  },
  {
    "Name": "Tournesol, Tryphon (prof.)",
    "Address": "Château de Moulinsart",
    "Phone": "421",
    "Note": "Creator of the Bianca rose."
  }
]

contacts = EXAMPLE

"""
Explanation: The contacts list
In order to keep the application uncomplicated, it is a dictionary, stored in the contacts variable, that will act as a database. Let's store some examplerecors in it.
End of explanation
"""


def display_contacts(dom):
  html = ""

  for contactId in range(len(contacts)):
    contact = contacts[contactId]
    html += f'<tr id="{contactId}" xdh:onevent="Select" style="cursor: pointer;">'
    for key in contact:
      html += f'<td>{contact[key]}</td>'
    html += '</td>'

  dom.inner("Content", html) # 'Content' is the id of an element of the main HTML page (look at the above definition of the 'BODY_CONTACTS' variable).

"""
Explanation: This function which will generate the HTML code displaying the list of the contacts .
End of explanation
"""


def ac_connect(dom):
  dom.inner("",BODY)
  display_contacts(dom)
    
callbacks[""] = ac_connect

"""
Explanation: We redefine and reaffect the function launched on each new connections, so that it will also display the list of the contacts.
End of explanation
"""


atlastk.launch(callbacks)

"""
Explanation: Let's look the result. You still can not interact with the application.
End of explanation
"""


def display_contact(contactId,dom):
  dom.set_values(contacts[contactId]) # The key of the dictionnaries entries matches the id of the *HTML* code
                                      # (see the above definition of the 'BODY_CONTACT' variable),
                                      # so we can use the entries of the 'contacts' variable as is.

"""
Explanation: The details of a contact
We now need a function which will display a specific contact.
End of explanation
"""


def ac_select(dom,id):
  display_contact(int(id),dom)

callbacks["Select"] = ac_select

"""
Explanation: We use this function in the function which will be launched when an entry of the contact list is selected.
As previously stated, the Select key is the value of a xdh:onevent attribute, as you can see it in the above definition of the display_contacts function.
End of explanation
"""


atlastk.launch(callbacks)

"""
Explanation: Let's see if it works: selecting a contact in the list will now display this contact in the contact form.
End of explanation
"""


FIELDS = [
  "Name",
  "Address",
  "Phone",
  "Note"
]

"""
Explanation: Disabling the fields and displaying the New button
The contact fields are currently enabled, but we need them to be disabled, so let's define an array containing their ids.
End of explanation
"""


def update_outfit(dom):
  dom.disable_elements(FIELDS)
  dom.disable_element("HideDisplay") # Disables the element of id 'HideDisplay' defined in `HEAD_BUTTONS` (see above),
                                     # which defines a *CSS* rule hiding element with the 'Display' class.
                                     # Hence, the 'New' button is displayed.

"""
Explanation: This function will disable the fields and show the New button. It will be later updated to enable/disable the fields and show/hide the proper button depending on the application current state.
End of explanation
"""


def ac_connect(dom):
  dom.inner("",BODY)
  display_contacts(dom)
  update_outfit(dom)

callbacks[""] = ac_connect

"""
Explanation: Let's redefine and reaffect the function assigned to the connection action, so it uses the above function.
End of explanation
"""


def ac_select(dom,id):
  display_contact(int(id),dom)
  update_outfit(dom)
    
callbacks["Select"] = ac_select

"""
Explanation: Same for the function assigned to the action launched by selecting an item in the list of contacts.
End of explanation
"""


atlastk.launch(callbacks)

"""
Explanation: Let's see the result. The fields are disabled, and the New button is displayed; we have yet to react appropriately when we click on this button.
End of explanation
"""


import enum

class State(enum.Enum):
  DISPLAY = enum.auto()
  EDIT = enum.auto()

"""
Explanation: Creating a new contact
Let's enumerate the two states in which the application can be.
End of explanation
"""


class Board:
  def __init__(self):
    self.state = State.DISPLAY

"""
Explanation: Each user can be in a different state, so let's define an object Board which will be user-specific to store this state.
End of explanation
"""


def update_outfit(board,dom):
  if board.state == State.DISPLAY:
    dom.disable_elements(FIELDS)
    dom.disable_element("HideDisplay")
  elif board.state == State.EDIT:
    dom.enable_elements(FIELDS)
    dom.enable_elements("HideDisplay")

"""
Explanation: This object has to be give to the function which enable/disable the fields, and hide/show the buttons. We also update this function so it handles the fields and the buttons according to the state stored in board, which is an instance of above Board.
End of explanation
"""


def ac_connect(board,dom):
  dom.inner("",BODY)
  display_contacts(dom)
  board.state = State.DISPLAY
  update_outfit(board,dom)

callbacks[""] = ac_connect

def ac_select(board,dom,id):
  display_contact(int(id),dom)
  board.state = State.DISPLAY
  update_outfit(board,dom)
    
callbacks["Select"] = ac_select

"""
Explanation: We also have to update and, hence, to reaffect, the functions calling this function, so it can pass to it the board parameter.
End of explanation
"""


EMPTY_CONTACT = {
  "Name": "",
  "Address": "",
  "Phone": "",
  "Note": ""
}

"""
Explanation: We define an empty contact, which will be used when no contact is selected.
End of explanation
"""


def display_contact(contactId,dom):
  dom.set_values(EMPTY_CONTACT if contactId == None else contacts[contactId])

"""
Explanation: We update the function which displays a contact so the fields where be emptied when there is no selected contact.
End of explanation
"""


def ac_new(board,dom):
  board.state = State.EDIT
  display_contact(None,dom)
  update_outfit(board,dom)
  dom.focus("Name")

callbacks["New"] = ac_new

"""
Explanation: This function allows to creation a new contact, and we assign it to the proper action.
End of explanation
"""


atlastk.launch(callbacks,Board) # Note the 'Board' parameter, which is the constructor of a user-specific object.

"""
Explanation: Clicking on the New button will now be handled properly. You can also launch other instances of this application (click on the URL, or use the QR Code), and they will react independently from each others.
End of explanation
"""


def update_outfit(board,dom):
  if board.state == State.DISPLAY:
    dom.disable_elements(FIELDS)
    dom.disable_element("HideDisplay")
    dom.enable_element("HideEdition")
  elif board.state == State.EDIT:
    dom.enable_elements(FIELDS)
    dom.enable_element("HideDisplay")
    dom.disable_element("HideEdition")

"""
Explanation: The edition buttons
Let's now add the handling of the Submit and Cancel buttons.
End of explanation
"""


def ac_cancel(board,dom):
  if dom.confirm("Are you sure?"):
    display_contact(None,dom)
    board.state = State.DISPLAY
    update_outfit(board,dom)
    
callbacks["Cancel"] = ac_cancel

"""
Explanation: Let's define and assign the function used for the Cancel button.
End of explanation
"""


def ac_submit(board,dom):
  idsAndValues = dom.get_values(FIELDS)

  if not idsAndValues['Name'].strip():
    dom.alert("The name field can not be empty!")
  else:
    board.state = State.DISPLAY
    contacts.append(idsAndValues)
    display_contact(None,dom)
    display_contacts(dom)
    update_outfit(board,dom)
    
callbacks["Submit"] = ac_submit

"""
Explanation: And also the function for the Submit button.
End of explanation
"""


atlastk.launch(callbacks,Board)

"""
Explanation: The handling of a new contact works now.
End of explanation
"""


class Board:
  def __init__(self):
    self.state = State.DISPLAY
    self.contactId = None

"""
Explanation: The remaining buttons
Time to address the Edit and Delete buttons.
Let's update the definition of the used-specific object, so we can handle the modification of an existing contact.
End of explanation
"""


def ac_select(board,dom,id):
  board.contactId = int(id)
  display_contact(board.contactId,dom)  
  board.state = State.DISPLAY
  update_outfit(board,dom)
    
callbacks["Select"] = ac_select

"""
Explanation: We need to update accordingly the function called when selecting a contact.
End of explanation
"""


def update_outfit(board,dom):
  if board.state == State.DISPLAY:
    dom.disable_elements(FIELDS)
    dom.disable_element("HideDisplay")
    dom.enable_element("HideEdition")
    if board.contactId == None:
      dom.enable_element("HideDisplayAndSelect")
    else:
      dom.disable_element("HideDisplayAndSelect")
  elif board.state == State.EDIT:
    dom.enable_elements(FIELDS)
    dom.enable_elements(("HideDisplay","HideDisplayAndSelect"))
    dom.disable_element("HideEdition")

"""
Explanation: And also the function which hides/shows the buttons.
End of explanation
"""


def ac_edit(board,dom):
  board.state = State.EDIT
  display_contact(board.contactId,dom)
  update_outfit(board,dom)
  dom.focus("Name")
    
callbacks["Edit"] = ac_edit

"""
Explanation: And we need to create and assign the function used for the Edit button.
End of explanation
"""


def ac_cancel(board,dom):
  if dom.confirm("Are you sure?"):
    display_contact(board.contactId,dom)
    board.state = State.DISPLAY
    update_outfit(board,dom)
    
callbacks["Cancel"]=ac_cancel

"""
Explanation: And to update and reassign the function behind the Cancel button.
End of explanation
"""


def ac_submit(board,dom):
  idsAndValues = dom.get_values(FIELDS)

  if not idsAndValues['Name'].strip():
    dom.alert("The name field can not be empty!")
  else:
    board.state = State.DISPLAY
    if board.contactId == None:
      contacts.append(idsAndValues)
    else:
      contacts[board.contactId] = idsAndValues
    display_contact(board.contactId,dom)
    display_contacts(dom)
    update_outfit(board,dom)
    
callbacks["Submit"]=ac_submit    

"""
Explanation: And also the one behind the Submit button.
End of explanation
"""


def ac_delete(board,dom):
  contacts.pop(board.contactId)
  board.contactId = None;
  display_contact(None,dom)
  display_contacts(dom)
  update_outfit(board,dom)

callbacks["Delete"] = ac_delete

"""
Explanation: Let's now create and assign the function for the Delete button.
End of explanation
"""


atlastk.launch(callbacks,Board)

"""
Explanation: And let's verify of all this buttons are now working.
End of explanation
"""


def ac_refresh(board,dom):
  display_contacts(dom)

callbacks["Refresh"] = ac_refresh

"""
Explanation: The bonus
If you open multiple instances, you will see that the modification/creation/deletion of a contact will not be reflected immediately in the other instances. Let's fix these.
Let's begin with a function which refresh the contact list, and which will be affected to an action which is not defined as value of a xdh:onevent attribute.
End of explanation
"""


def ac_submit(board,dom):
  idsAndValues = dom.get_values(FIELDS)

  if not idsAndValues['Name'].strip():
    dom.alert("The name field can not be empty!")
  else:
    board.state = State.DISPLAY
    if board.contactId == None:
      contacts.append(idsAndValues)
    else:
      contacts[board.contactId] = idsAndValues
    display_contact(board.contactId,dom)
    atlastk.broadcast_action("Refresh")
    update_outfit(board,dom)
    
callbacks["Submit"] = ac_submit

"""
Explanation: In the function behind the Submit button, instead of calling directly the function which refreshes the contact list, let's use the atlastk.broadcast_action("Refresh") function instead. This function will launch the Refresh action, to which the above function is assigned, on all instances. Hence, the list of contacts will now be updated on all instances.
End of explanation
"""


def ac_delete(board,dom):
  contacts.pop(board.contactId)
  board.contactId = None;
  display_contact(None,dom)
  atlastk.broadcast_action("Refresh")
  update_outfit(board,dom)

callbacks["Delete"] = ac_delete

"""
Explanation: Same for the function behind the Delete button.
End of explanation
"""


atlastk.launch(callbacks,Board)

"""
Explanation: Let's admire the result. Open several instances, modify/create/delete a contact in one of them, and you will see that the contact list in all instances will be updated accordingly.
End of explanation
"""


