open Brr module PM = Prosemirror module Js = Js_of_ocaml.Js (** This is the state for the application *) type state = { editable : bool ; view : PM.View.editor_view Js.t ; last_backup: float ; page_id: Jstr.t option ; window : El.t list } type events = | DeleteEvent | StoreEvent | LoadEvent of Jstr.t option | AddEvent | CloseEvent of Forms.Events.kind option let set_title : Storage.content Js.t -> unit = fun content -> let title = Js.Opt.get content##.title (fun () -> Jstr.empty) in let title_element = Document.find_el_by_id G.document (Jstr.v "title") in Option.iter (fun el -> El.set_prop (El.Prop.value) title el) title_element let key_of_title : Jstr.t -> Jstr.t = fun title -> title let state_of_storage : PM.t -> Storage.content Js.t -> PM.Model.schema Js.t -> PM.State.editor_state Js.t = fun pm content schema -> Js.Opt.case content##.content (fun () -> let obj = PM.State.creation_prop () in obj##.plugins := Plugins.default pm schema; obj##.schema := Js.some schema; PM.State.create pm obj) (fun page_content -> let obj = PM.State.configuration_prop () in obj##.plugins := Plugins.default pm schema; obj##.schema := Js.some schema; PM.State.fromJSON pm obj page_content) (** Create a new editor view [build_view element state] will create the editor and attach it to [element]. *) let build_view : PM.t -> Jstr.t option -> El.t -> PM.View.editor_view Js.t * float = fun pm page_id editor -> (* Remove all the elements if any *) El.set_children editor []; (* TODO This could be improved, instead of creating a new schema, just fetch the node and marks from the plungin *) let custom_schema = Footnotes.footnote_schema pm (PM.SchemaBasic.schema pm) in (* Recreate the full schema by adding all the nodes and marks from the plugings *) let specs = PM.Model.schema_spec (PM.SchemaList.add_list_nodes pm (custom_schema##.spec##.nodes) (Jstr.v "paragraph block*") (Some (Jstr.v "block"))) (Some custom_schema##.spec##.marks) None in let full_schema = PM.Model.schema pm specs in let stored_content = Storage.load page_id in (* This variable contains the last update time, either because it is stored, or because it is the date where we create the first page. *) let last_backup = Js.Opt.get stored_content##.date (fun () -> (new%js Js.date_now)##getTime) in let props = PM.View.direct_editor_props () in props##.state := state_of_storage pm stored_content full_schema; (* Add the custom nodes *) props##.nodeViews := PM.O.init [| ( "footnote", (Footnotes.footnote_view pm)) |]; let view = PM.View.editor_view pm editor props in view, last_backup let load_page : PM.t -> Jstr.t option -> state -> Storage.content Js.t -> state = fun pm page_id state json -> let editor_state = state_of_storage pm json state.view##.state##.schema in let () = state.view##updateState editor_state and () = set_title json in { state with page_id } (** [update] is the event loop. The function take a new event, and apply it to the current state. *) let update : PM.t -> 'a option Note.E.send -> (events, state) Application.t = fun pm close_sender event state -> match event with | AddEvent -> let title = Jstr.v "Nouvelle page" in let popup = Ui.popup ~title ~form:(Some (Forms.Add_page.create ())) close_sender in { state with window = popup::state.window} | DeleteEvent -> begin match state.page_id with | None -> state | Some page_id -> let title = Jstr.v "Confirmation" in let popup = Ui.popup ~title ~form:(Some (Forms.Delete_page.create page_id)) close_sender in { state with window = popup::state.window} end | CloseEvent res -> let state = match state.window with | [] -> { state with window = [] } | el::tl -> El.remove el ; { state with window = tl } in (* The actions is confirmed by the user. Handle the form result *) begin match res with (* Delete the current page, then load the home page *) | Some (Forms.Delete_page.DeletePage id) -> Storage.delete (fun () -> Some id); let json = Storage.load None in load_page pm None state json (* Add a new page *) | Some (Forms.Add_page.AddPage {title}) -> let page_id = key_of_title title in Console.(log [title]); let new_date = (new%js Js.date_now)##getTime in let content_obj = object%js val content = Js.null val title = Js.some title val date = Js.some new_date end in load_page pm (Some page_id) state content_obj | _ -> state end | StoreEvent -> let title_element = Document.find_el_by_id G.document (Jstr.v "title") in let content = Option.map (fun el -> El.prop (El.Prop.value) el) title_element in let new_date = (new%js Js.date_now)##getTime in let content_obj = object%js val content = Js.some @@ Jv.Id.to_jv (state.view##.state##toJSON ()) val title = Js.Opt.option content val date = Js.some new_date end in let save = Storage.save content_obj state.page_id ~check:(fun previous_state -> Js.Opt.case previous_state##.date (fun () -> true) (fun date -> (* I do not figure how the previous date could be older than the last backup. It could be either : - equal (if we are the only one to update it) - more recent (if the content has been updated elsewhere) but older shoud be a bug. *) date <= state.last_backup)) in begin match save with | Ok true -> { state with last_backup = new_date } | _ -> (* TODO In case of error, notify the user *) state end | LoadEvent page_id -> let json = Storage.load page_id in load_page pm page_id state json let app id content = (* Check the pre-requisite *) let events_opt = Actions.populate_menu () in match (Jv.is_none id), (Jv.is_none content), events_opt with | false, false, Some btn_events -> let pm = PM.v () in let editor:El.t = Jv.Id.of_jv id in (* Load the cache for the given page *) let page_id = Storage.page_id () in let view, last_backup = build_view pm page_id editor in (* This event is used in the pop process. The sender is given to the subroutine in order to track the window closing *) let event, sender = Note.E.create () in let _ = sender in let init_state = { editable = true ; view ; last_backup ; page_id ; window = [] } in let app_state = Application.run (update pm sender) init_state (Note.E.select [ Note.E.map (fun () -> DeleteEvent) btn_events.Actions.delete ; Brr_note.Evr.on_el Ev.focusout (fun _ -> StoreEvent) editor ; Note.E.map (fun v -> LoadEvent v) btn_events.Actions.redirect ; Note.E.map (fun () -> AddEvent) btn_events.Actions.add ; Note.E.map (fun v -> CloseEvent v) event ]) in let () = Note.S.log app_state (fun _ -> ()) |> Note.Logr.hold in () | _, _, _ -> Console.(error [str "No element with id '%s' '%s' found"; id ; content]) let () = let open Jv in let editor = obj [| "attach_prosemirror", (repr app) |] in set global "editor" editor