aboutsummaryrefslogtreecommitdiff
path: root/editor/footnotes.ml
blob: a3ba9cdfd8f41c072bd88114ad11ac905ab26c5f (plain)
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
open Brr
open Js_of_ocaml
module PM = Prosemirror

let footNoteSpec = object%js

  val mutable group = Jstr.v "inline"
  val mutable content = Jstr.v "inline*" (* The star is very important ! *)
  val mutable inline = Js._true
  val mutable draggable = Js._true
  (*  This makes the view treat the node as a leaf, even though it
      technically has content *)
  val mutable atom = Js._true

  val toDOM
    : (PM.Model.node Js.t -> PM.Model.domOutputSpec Js.t) Js.callback
    = Js.wrap_callback (fun _ ->
        let open PM.Model.Dom_output_spec in
        v "footnote"
          [ hole ])

  val parseDOM
    : PM.Model.parse_rule Js.t Js.js_array Js.t Js.opt
    = Js.some @@ Js.array
      [|PM.Model.ParseRule.tag (Jstr.v "footnote")|]

end

let footnote_schema pm defaultSchema =

  let nodes = defaultSchema##.spec##.nodes
  and marks = defaultSchema##.spec##.marks in

  let specs = PM.Model.schema_spec
      (nodes##addToEnd (Jstr.v "footnote") (Js.Unsafe.coerce footNoteSpec))
      (Some marks)
      None in

  PM.Model.schema pm
    specs

let build_menu pm schema =
  let menu = PM.Example.buildMenuItems pm schema in

  let itemSpec = PM.Menu.menuItemSpec () in
  itemSpec##.title := Js.some @@ Jstr.v "Insert footnote";
  itemSpec##.label := Js.some @@ Jstr.v "Footnote";
  itemSpec##.select := Js.wrap_meth_callback (fun _ (state:PM.State.editor_state Js.t) ->
      match PM.O.get schema##.nodes "footnote" with
      | None ->  Js._false
      | Some footnote_node ->
        let res = Js.Opt.test @@ PM.Transform.insertPoint
            pm
            state##.doc
            ~pos:state##.selection##.from
            footnote_node
        in
        Js.bool res);

  itemSpec##.run :=
    Js.wrap_meth_callback (fun _this state dispatch _ _ ->
        match PM.O.get schema##.nodes "footnote" with
        | None -> ()
        | Some footnote_node ->

          let from' = PM.State.selection_from state##.selection
          and to' = PM.State.selection_to state##.selection in

          let content =
            if state##.selection##.empty != Js._true
            && from'##sameParent to' = Js._true
            && from'##.parent##.inlineContent = Js._true then (
              from'##.parent##.content##cut
                (from'##.parentOffset)
                (Js.some @@ to'##.parentOffset)
            ) else (
              PM.Model.empty_fragment pm
            ) in
          let new_node = footnote_node##create_withFragmentContent
              Js.null
              (Js.some content)
              Js.null
          in
          dispatch @@
          state##.tr##replaceSelectionWith
            new_node
            Js.null
      );

  let item = PM.Menu.menu_item pm itemSpec in
  let _ = menu##.insertMenu##.content##push item in
  menu

let fromOutside
  : bool PM.State.meta_data Js.t
  = PM.State.create_str_meta_data (Jstr.v "fromOutside")

let footnote_view
  : PM.t -> PM.Model.node Js.t -> PM.View.editor_view Js.t -> (unit -> int) -> < .. > Js.t
  = fun pm init_node outerView get_pos ->

    (* These are used when the footnote is selected *)
    let innerView
      : PM.View.editor_view Js.t Js.opt ref
      = ref Js.null in

    let dispatchInner
      : PM.View.editor_view Js.t -> PM.State.transaction Js.t -> unit
      = fun view tr ->
        let res = view##.state##applyTransaction tr in
        view##updateState res##.state;

        let meta = Js.Optdef.get (tr##getMeta fromOutside) (fun () -> false) in
        if (not meta) then (
          let outerTr = outerView##.state##.tr
          and offsetMap = PM.Transform.offset pm ((get_pos()) + 1) in
          res##.transactions##forEach
            (Js.wrap_callback @@
             fun (elem:PM.State.transaction Js.t) _ _ ->
             elem##.steps##forEach
               (Js.wrap_callback @@ fun (step:PM.Transform.step Js.t) _ _ ->
                let _ = outerTr##step (step##map offsetMap) in
                ()
               ));
          if (outerTr##.docChanged = Js._true) then (
            outerView##dispatch outerTr)
        );
    in
    object%js (_self)

      val mutable node: PM.Model.node Js.t = init_node

      (* The node's representation in the editor (empty, for now) *)
      val dom = El.v (Jstr.v "footnote") []

      method _open =
        (* Append a tooltip to the outer node *)
        let tooltip = El.div []
            ~at:At.([class' (Jstr.v "footnote-tooltip")]) in
        El.append_children _self##.dom
          [ tooltip ];

        let dispatch_fn
          : PM.State.transaction Js.t -> unit
          = fun tr -> outerView##dispatch tr in

        let state_properties = Js.Unsafe.coerce (object%js
            val doc = Js.some _self##.node;
            val plugins = Js.some @@ Js.array @@ [|
                PM.Keymap.keymap pm
                  [| ( "Mod-z"
                     , (fun _ _ -> PM.History.undo pm outerView##.state (Js.some dispatch_fn)))
                   ; ( "Mod-y"
                     , (fun _ _ -> PM.History.redo pm outerView##.state (Js.some dispatch_fn)))
                  |]
              |];
          end) in

        let view_properties = PM.View.direct_editor_props () in
        view_properties##.state := PM.State.create pm state_properties;
        (* This is the magic part *)
        view_properties##.dispatchTransaction :=
          (Js.wrap_meth_callback dispatchInner);
        view_properties##.handleDOMEvents := PM.O.init
            [| ( "mousedown"
               , Js.wrap_callback (fun _ _ ->
                     (* Kludge to prevent issues due to the fact that the
                        whole footnote is node-selected (and thus DOM-selected)
                        when the parent editor is focused. *)
                     if (outerView##hasFocus () = Js._true) then (
                       Js.Opt.iter !innerView (fun view -> view##focus ())
                     );
                     Js._false ))|];

        innerView := Js.some
            (PM.View.editor_view pm
               tooltip
               view_properties);

      method close =
        Js.Opt.iter (!innerView)
          (fun view ->
             view##destroy;
             innerView := Js.null;
             El.set_prop
               (El.Prop.jstr (Jstr.v "textContent"))
               (Jstr.empty)
               _self##.dom
          )

      method update
        : PM.Model.node Js.t -> bool Js.t
        = fun node ->
          if (node##sameMarkup _self##.node = Js._false) then (
            Js._false
          ) else (
            _self##.node := node;
            Js.Opt.iter !innerView (fun view ->
                let state = view##.state in
                Js.Opt.iter (node##.content##findDiffStart state##.doc##.content) (fun start ->
                    let res_opt = (node##.content##findDiffEnd state##.doc##.content) in
                    Js.Opt.iter res_opt (fun end_diff ->
                        let overlap = start - (min end_diff##.a end_diff##.b) in
                        let endA, endB =
                          if overlap > 0 then
                            ( end_diff##.a + overlap
                            , end_diff##.b + overlap )
                          else
                            ( end_diff##.a
                            , end_diff##.b )
                        in
                        let tr =
                          state##.tr
                          ##(replace
                               ~from:start
                               ~to_:endB
                               (Js.some @@ node##slice ~from:start ~to_:(Js.some endA)))
                          ##(setMeta fromOutside true) in
                        view##dispatch tr)));
            Js._true
          )

      method destroy =
        Js.Opt.iter !innerView (fun _ -> _self##close)

      method stopEvent e =
        Js.Opt.case !innerView
          (fun () -> Js._false)
          (fun view ->
             let dom = view##.dom in
             Jv.call (Jv.Id.to_jv dom) "contains" [| e##.target|]
             |> Jv.Id.of_jv)

      method ignoreMutation =
        Js._true

      method selectNode =
        El.set_class (Jstr.v "ProseMirror-selectednode") true _self##.dom;
        if not (Js.Opt.test !innerView) then (
          _self##_open
        )

      method deselectNode =
        El.set_class (Jstr.v "ProseMirror-selectednode") false _self##.dom;
        if (Js.Opt.test !innerView) then
          _self##close

    end