Technical description of the Story Page

The Story page handles the story viewer and the editor, the latter being provided by a JavaScript library called CKEditor. There are many editor libraries but this one is among the most comprehensive, and is free.

The Story module is an EasyCoder script kept in a table in the database. When the page loads, a call is made to the REST server to fetch the Story script, which is then compiled and initialized, ready for use. Every time the script receives a "trigger" it displays either the story viewer or the editor and manages interactions with each of them.

The script starts by naming itself. Then it names the variables it expects to import from the calling module, and then declares all the other variables it will need locally.

  script Stories

  import div ParentDiv and variable Record and variable MyEmail

  div ContainerDiv
  div EditorDiv
  div TextFieldsDiv
  div TitleDiv
  div TagsDiv
  div StoryDiv
  div AuthorDiv
  div ButtonsDiv
  h3 TitleHeader
  label TitleLabel
  label TagsLabel
  label Label
  input TitleInput
  input TagsInput
  span Status
  span Text
  img DeleteIcon
  img FolderIcon
  img SaveIcon
  img ViewIcon
  a EditorLink
  a AuthorLink
  a TagLink
  a Link
  module FileManager
  variable ID
  variable Author
  variable Email
  variable Latitude
  variable Longitude
  variable Zoom
  variable Title
  variable Tags
  variable Story
  variable TextBoxWidth
  variable Script
  variable FileManagerRunning
  variable Message
  variable Record2
  variable SavedTitle
  variable SavedTags
  variable SavedStory
  variable TagCount
  variable N
  variable Flag

 

The main container for this part of the page is created, giving the named element as its parent. It's initially set to be invisible, with a border, a margin and some padding.

  create ContainerDiv in ParentDiv
  set the style of ContainerDiv to
    `display:none;border:1px solid lightgray;margin-top:0.5em;padding:0.5em`

 

The script will only ever be sent one type of message, so there's no need for it to be JSON-formatted. The message tells the script to hide the main container and all its contents.

  on message
  begin
    if the message is `hide`
    begin
      set style `display` of ContainerDiv to `none`
    end
  end

 

That's all the initialization done, so the script waits for a trigger and signals that it's ready. Scripts block while they are loading, to avoid multiple requests interfering with each other, so since JavaScript is single-threaded they should aim to load as quickly as possible. Fortunately, scripts only have to be compiled once per session. For the Story script this takes typically 50ms or so the first time the user clicks a pin on the map; not enough for most people to notice. The 'set ready' command signals that both the load and the compilation are complete and the parent script may continue.

  on trigger go to ShowStory
  set ready
  stop

 

The imported record is a JSON structure containing everything about the pin except its tags and the story itself. The script extracts all the fields into local variables, then retrieves the remaining information with a separate REST call.

The reason it's done this way is that when tags are returned from a search there may be up to 20 of them, this being the maximum number the map will display at one time to avoid crowding. Since the user probably won't click all of them before moving the map there's no sense in retrieving the bulky story and the relatively time-consuming tag search for all 20; instead, this is done here when a specific pin has been selected.

The tags arrive as a JSON array; at the end f this block this array is turned into a comma-delimited list suitable for display.

ShowStory:
  put property `id` of Record into ID
  put property `email` of Record into Email
  put property `latitude` of Record into Latitude
  put property `longitude` of Record into Longitude
  put property `zoom` of Record into Zoom
  put decode property `title` of Record into Title
  rest get Record2 from `_/ec_markers/story/` cat ID or
  begin
    alert `The data for this pin failed to load. Please try again.`
      cat newline cat newline
      cat `The reported error message was:` cat newline cat the error
  end
  put property `author` of Record2 into Author
  put property `tags` of Record2 into Tags
  put decode property `story` of Record2 into Story
  put Title into SavedTitle
  put Story into SavedStory
  put the json count of Tags into TagCount
  put `` into SavedTags
  clear Flag
  put 0 into N
  while N is less than TagCount
  begin
    if Flag put SavedTags cat `,` into SavedTags
    put SavedTags cat element N of Tags into SavedTags
    set Flag
    add 1 to N
  end

 

The main container is now made visible...

  set style `display` of ContainerDiv to `block`

 

... and a test is made to see if this pin belongs to the current user. If it does, control is transferred to the editor; otherwise it continues with the viewer.

  if MyEmail is Email go to Editor

 

The viewer starts by clearing the container in case it may be holding a previous pin story. It then checks the ownership of the pin (because the viewer can be called from the editor) and if necessary sets up a hyperlink to return to the editor.

View:
  clear ContainerDiv

  if MyEmail is Email
  begin
    create EditorLink in ContainerDiv
    set the style of EditorLink to `float:right`
    set the content of EditorLink to `Go to Editor`
    on click EditorLink go to Editor
  end

 

Above every story is a line displaying the name of the story author, which can be clicked to display pins from just that author. When the name is clicked a JSON message is built and sent to the parent (the home page) for action.

  create TitleHeader in ContainerDiv
  set the style of TitleHeader to `color:blue`
  set the content of TitleHeader to Title

  create AuthorDiv in ContainerDiv
  set the style of AuthorDiv to `font-size:80%`
  set the content of AuthorDiv to `Author: `
  create AuthorLink in AuthorDiv
  set the style of AuthorLink to `font-size:1em`
  set the content of AuthorLink to Author
  set attribute `title` of AuthorLink to `Show only pins from this author`
  on click AuthorLink
  begin
    put `{}` into Message
    json set property `request` of Message to `select`
    json set property `select` of Message to `author`
    json set property `author` of Message to Author
    json set property `email` of Message to Email
    send Message to parent
  end

 

After the author, another line displays all the tags for this pin. These are also clickable to select all pins with a given tag. This code is similar to that above for the author, except there can be any number of tags so a loop is needed. Since we are creating a clickable link for each tag we set the TagLink variable to have the right number of elements before we start to iterate the tags. The expression 'element N of Tags' extracts a single value from a JSON array; this is then set as the content of the corresponding element in the TagLink (array) variable.

You will see that although TagLink is iterated through all its elements there's only 1 'on click'. This is because when an event is set on a variable it is set on all the array elements at once. The index for the array will be set to the clicked element, so the correct action is performed.

  put the json count of Tags into TagCount
  if TagCount is greater than 0
  begin
    create TagsDiv in ContainerDiv
    set the style of TagsDiv to `margin-bottom:0.5em;font-size:80%`
    set the content of TagsDiv to `Tags: `
    set the elements of TagLink to TagCount
    clear Flag
    put 0 into N
    while N is less than TagCount
    begin
      index TagLink to N
      if Flag
      begin
        create Text in TagsDiv
        set the content of Text to `, `
      end
      create TagLink in TagsDiv
      set the style of TagLink to `font-size:1em`
      set the content of TagLink to element N of Tags
      set attribute `title` of TagLink to `Show only pins with this tag`
      set Flag
      add 1 to N
    end
    on click TagLink
    begin
      put `{}` into Message
      json set property `request` of Message to `select`
      json set property `select` of Message to `tag`
      json set property `tag` of Message to the content of TagLink
      json set property `email` of Message to Email
      send Message to parent
    end
  end

 

After all that it's very simple to set up the story itself. We create a new DIV for it and set its content to whatever returned from the database; a block of HTML markup. And that's done with the viewer.

  create StoryDiv in ContainerDiv
  set the content of StoryDiv to Story
  stop

 

The editor is a little more complicated. As with the viewer, we clear the container, then we create an Editor Div to hold the content, followed by a ButtonsDiv to hold the 4 graphic buttons that float to the right at the top of the editor.

Part of that button group is a status label we use to indicate that a save operation was successful. This is created first, and followed by the Delete, Folder, Save and View icons. It should be pretty easy to see how this works, as the code is just mirroring what happens in HTML. One note; you might ask why each icon is embedded in a link, even though it's the icon that is clickable. The answer is that unless this is done the cursor remains as a pointer when you hover over the button. Wrapping it in a link makes the hand cursor icon appear on hover.

All the buttons except View transfer to a script label when clicked; View is done inline. (There's no particular reason for this.) View retrieves the values of each of the alterable fields and checks if any have changed since the last save. If so it pops up a warning and refuses to go to the viewer.

One more field is added, showing the zoom value for the pin, that is, the zoom at the time it was created. Zoom values range from 0 (the whole planet) to 22 (close enough to see what people are wearing).

Editor:
  clear ContainerDiv
  create EditorDiv in ContainerDiv
  create ButtonsDiv in EditorDiv
  set the style of ButtonsDiv to `float:right;text-align:right`

  create Status in ButtonsDiv
  set the style of Status to `color:green;padding-right:1em;margin-bottom:1em`
  create Link in ButtonsDiv
  create DeleteIcon in Link
  set the style of DeleteIcon to `width:25px;margin-right:10px`
  set attribute `src` of DeleteIcon to
    `https://hereonthemap.com/resources/system/delete.png`
  set attribute `title` of DeleteIcon to `Delete this pin`
  on click DeleteIcon go to Delete
  create FolderIcon in Link
  set the style of FolderIcon to `width:25px;margin-right:10px`
  set attribute `src` of FolderIcon to
    `https://hereonthemap.com/resources/system/media.png`
  set attribute `title` of FolderIcon to `Image manager`
  on click FolderIcon go to FileMan
  create Link in ButtonsDiv
  create SaveIcon in Link
  set the style of SaveIcon to `width:25px;margin-right:10px`
  set attribute `src` of SaveIcon to
    `https://hereonthemap.com/resources/system/save.png`
  set attribute `title` of SaveIcon to `Save changes`
  on click SaveIcon go to Save
  create Link in ButtonsDiv
  create ViewIcon in Link
  set the style of ViewIcon to `width:25px`
  set attribute `src` of ViewIcon to
    `https://hereonthemap.com/resources/system/binoculars.png`
  set attribute `title` of ViewIcon to `View article`
  on click ViewIcon
  begin
    put the text of TitleInput into Title
    put the text of TagsInput into Tags
    replace `'` with `` in Tags
    replace `"` with `` in Tags
    ckeditor get Story from StoryDiv
    if Title is not SavedTitle go to NotSaved
    if Tags is not SavedTags go to NotSaved
    if Story is not SavedStory go to NotSaved
    go to View
  end
  create Label in ButtonsDiv
  set the content of Label to `Z:` cat Zoom

 

Next we create the editable fields above the main story editor. These should need no explanation as they just describe HTML fields.

  create TextFieldsDiv in EditorDiv
  put 40 into TextBoxWidth

  create TitleDiv in TextFieldsDiv
  set the style of TitleDiv to `display:flex;height:1.5em;margin:0.2em 0 0.5em 0`
  create TitleLabel in TitleDiv
  set the style of TitleLabel to `width:7em;padding-top:0.2em`
  set the content of TitleLabel to `Title:`
  create TitleInput in TitleDiv
  set the size of TitleInput to TextBoxWidth
  set the content of TitleInput to Title

  create TagsDiv in TextFieldsDiv
  set the style of TagsDiv to `display:flex;height:1.5em;margin-bottom:0.5em`
  create TagsLabel in TagsDiv
  set the style of TagsLabel to `width:7em;padding-top:0.2em`
  set the content of TagsLabel to `Tags:`
  create TagsInput in TagsDiv
  set the size of TagsInput to TextBoxWidth
  put the json count of Tags into TagCount
  set the content of TagsInput to SavedTags

 

And finally the story itself; a DIV with a CKEditor component attached. For an unknown reason it's best to wait before we actualy load the story itself into the editor; half a second seems to do the job most of the time. A 'tick' is 10ms.

  create StoryDiv in ContainerDiv
  ckeditor attach to StoryDiv
  wait 50 ticks
  ckeditor set StoryDiv to Story
  stop

 

Here we throw up a warning when an action is taken without saving the data:

NotSaved:
  alert `The data is not saved.`
  stop

 

This is where the File Manager gets loaded. Its script is retrieved from the database and then run, after checking that this hasn't already been done.

FileMan:
  if not FileManagerRunning
  begin
    rest get Script from `ec_scripts/name/fileman` or
    begin
      alert `Failed to load the file manager script. Please try again.`
        cat newline cat newline
        cat `The reported error message was:` cat newline cat the error
    end
    run Script as FileManager
    set FileManagerRunning
  end

 

The next part deals with the browser 'Back' button, which some users click even if there's an explicit Close button on the page. The code isn't complex but it may not be obvious what is happening.

The File Manager runs as a new entry in the browser history, but doesn't rewrite the page URL so you don't see any change. When the user clicks the 'Close' button in the File Manager, its script invokes 'browser back', which causes a 'restore' event to arrive here. The File Manager has no way of telling this has happened because the user may have clicked the browser 'Back' button, which has the same effect.

If you don't use code of this kind, then although the Close button may work fine, when the user clicks Back they will leave this web page completely. Not what is intended in most cases.

The 'on restore' command traps the browser 'back' event so we can tell the File Manager to close itself:

  on restore close FileManager

 

In order for the above mechanism to work we have to 'set' the browser history before triggering the File Manager. That tells the browser where to return to when the user leaves the File Manager.

  history set
  trigger FileManager
  stop

 

To save our data we build a JSON data object with all the necessary information and send it to the server. Then we copy the current field values into the ones used to detect when the document has changed.

Save:
  put the text of TitleInput into Title
  put the text of TagsInput into Tags
  replace `'` with `` in Tags
  replace `"` with `` in Tags
  ckeditor get Story from StoryDiv
  put `{}` into Record
  json set property `id` of Record to ID
  json set property `title` of Record to encode Title
  json set property `tags` of Record to lowercase encode Tags
  json set property `story` of Record to encode Story
  rest post Record to `_/ec_markers/update/` cat ID or
  begin
    alert `The save failed. Please try again.` cat newline cat newline
      cat `The reported error message was:` cat newline cat the error
  end
  put Title into SavedTitle
  put Tags into SavedTags
  put Story into SavedStory
  set the content of Status to `Story saved`
  wait 3
  set the content of Status to ``
  stop

 

Finally, we can delete a pin, after confirming it's what the user really wanted.

Delete:
  if confirm `Please confirm you want to delete marker '`
    cat property `title` of Record cat `'`
  begin
    put `{}` into Record
    json set property `id` of Record to ID
    json set property `email` of Record to Email
    rest post Record to `_/ec_markers/delete` or
    begin
      alert `The delete failed. Please try again.` cat newline cat newline
        cat `The reported error message was:` cat newline cat the error
    end
    clear ContainerDiv
    put `{}` into Message
    json set property `request` of Message to `refresh`
    send Message to parent
  end
  stop