summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSébastien Dailly <sebastien@chimrod.com>2021-03-21 20:24:15 +0100
committerSébastien Dailly <sebastien@dailly.me>2022-02-07 16:43:33 +0100
commit210a4d94836d07bb71cad46b3e670c1977cfe833 (patch)
tree406de4546a3530416a5672a16561fbd2b9bd627e
parentccd68208146e87b9f185f543aadccc1110e72642 (diff)
Updated PM examples
-rwxr-xr-xeditor/dune1
-rw-r--r--editor/editor.css40
-rwxr-xr-xeditor/editor.ml47
-rwxr-xr-xeditor/footnotes.ml257
-rwxr-xr-xeditor/plugins.ml1
-rwxr-xr-xeditor/prosemirror/bindings.ml577
-rwxr-xr-xeditor/prosemirror/prosemirror.ml74
-rwxr-xr-xeditor/prosemirror/prosemirror.mli140
-rwxr-xr-xeditor/tooltip.ml123
9 files changed, 900 insertions, 360 deletions
diff --git a/editor/dune b/editor/dune
index b16e85f..767d35e 100755
--- a/editor/dune
+++ b/editor/dune
@@ -4,6 +4,7 @@
brr
brr.note
elements
+ js_lib
prosemirror
blog
)
diff --git a/editor/editor.css b/editor/editor.css
index c8c2aeb..75d9495 100644
--- a/editor/editor.css
+++ b/editor/editor.css
@@ -392,3 +392,43 @@ li.ProseMirror-selectednode:after {
left: 0;
top: 24px;
}
+
+
+ .ProseMirror {
+ counter-reset: prosemirror-footnote;
+ }
+ footnote {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+ }
+ footnote::after {
+ content: counter(prosemirror-footnote);
+ vertical-align: super;
+ font-size: 75%;
+ counter-increment: prosemirror-footnote;
+ }
+ .ProseMirror-hideselection .footnote-tooltip *::selection { background-color: transparent; }
+ .ProseMirror-hideselection .footnote-tooltip *::-moz-selection { background-color: transparent; }
+ .footnote-tooltip {
+ cursor: auto;
+ position: absolute;
+ left: -30px;
+ top: calc(100% + 10px);
+ background: silver;
+ padding: 3px;
+ border-radius: 2px;
+ width: 500px;
+ }
+ .footnote-tooltip::before {
+ border: 5px solid silver;
+ border-top-width: 0px;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ position: absolute;
+ top: -5px;
+ left: 27px;
+ content: " ";
+ height: 0;
+ width: 0;
+ }
diff --git a/editor/editor.ml b/editor/editor.ml
index 5aecef0..19480e2 100755
--- a/editor/editor.ml
+++ b/editor/editor.ml
@@ -58,13 +58,18 @@ let prosemirror id content =
let module PM = Prosemirror in
let pm = PM.v () in
+ let schema = (PM.SchemaBasic.schema pm) in
+ let schema = Footnotes.footnote_schema pm schema in
+
+ Console.(log [ schema ]);
+
let specs = PM.Model.schema_spec
(PM.SchemaList.add_list_nodes
pm
- ((PM.SchemaBasic.schema pm)##.spec##.nodes)
+ (schema##.spec##.nodes)
(Jstr.v "paragraph block*")
(Some (Jstr.v "block")))
- (Some (PM.SchemaBasic.schema pm)##.spec##.marks)
+ (Some schema##.spec##.marks)
None in
let mySchema = PM.Model.schema pm specs in
@@ -80,46 +85,26 @@ let prosemirror id content =
view##updateState state
);
+ let view' = (Footnotes.footnote_view pm) in
+
+ let nodes = PM.O.init
+ [| ("footnote", view') |] in
+ props##.nodeViews := nodes;
+
let view = PM.View.editor_view
pm
(Jv.Id.of_jv id)
props in
- view##setProps props;
+ Console.(log [Jstr.v "main view"; view]);
(* Attach an event on focus out *)
let _ = Brr_note.Evr.on_el
(Ev.focusout)
- (fun _ ->
-(*
- let props = view##.props in
- props##.editable := Js.wrap_callback (fun _ -> Js._false);
- view##update props;
-*)
- save_storage view
- )
+ (fun _ -> save_storage view)
(Jv.Id.of_jv id) in
-
-(*
- let default_editable = view##.props##.editable in
- let _ = Brr_note.Evr.on_el
- (Ev.dblclick)
- (fun e ->
- let target = Ev.target e in
- let (el:El.t) = Jv.Id.(of_jv @@ to_jv target) in
- if (view##.editable == Js._false && (El.tag_name el <> Jstr.v "a")) then (
- let props = view##.props in
- props##.editable := default_editable;
- view##update props;
- Console.(log [el]);
- El.set_has_focus true (Jv.Id.of_jv id);
- )
-
- )
- (Jv.Id.of_jv id) in
-*)
()
- | _, _-> Console.(error [str "No element with id '%s' '%s' found"; id ; content])
+ | _, _ -> Console.(error [str "No element with id '%s' '%s' found"; id ; content])
end
diff --git a/editor/footnotes.ml b/editor/footnotes.ml
new file mode 100755
index 0000000..a2bc9c6
--- /dev/null
+++ b/editor/footnotes.ml
@@ -0,0 +1,257 @@
+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*"
+ 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
+
+
+ let obj =
+ 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
+ )
+
+ (* TODO
+ https://prosemirror.net/docs/ref/#view.NodeView.update *)
+ 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
+ in
+ obj
diff --git a/editor/plugins.ml b/editor/plugins.ml
index 8dd960a..69319c4 100755
--- a/editor/plugins.ml
+++ b/editor/plugins.ml
@@ -116,6 +116,7 @@ let default pm schema =
let props = PM.Example.options schema in
props##.menuBar := Js.some Js._true;
props##.floatingMenu := Js.some Js._true;
+ props##.menuContent := (Footnotes.build_menu pm schema)##.fullMenu;
let setup = PM.Example.example_setup pm props in
let keymaps =
diff --git a/editor/prosemirror/bindings.ml b/editor/prosemirror/bindings.ml
index 4b95b73..1711829 100755
--- a/editor/prosemirror/bindings.ml
+++ b/editor/prosemirror/bindings.ml
@@ -16,6 +16,12 @@ module TypedObject : sig
val set'
: 'a t -> Jv.prop' -> 'a -> unit
+ val create
+ : unit -> 'a t
+
+ val init
+ : (Jv.prop * 'a) array -> 'a t
+
end = struct
type 'a t = Jv.t
@@ -40,6 +46,14 @@ end = struct
= fun o prop v ->
Jv.set' o prop (Jv.Id.to_jv v)
+ let create
+ : unit -> 'a t
+ = fun () -> Jv.obj [||]
+
+ let init
+ : (Jv.prop * 'a) array -> 'a t
+ = fun param -> Jv.obj (Obj.magic param)
+
end
class type ['a] ordered_map = object ('this)
@@ -54,102 +68,148 @@ class type ['a] ordered_map = object ('this)
Jstr.t -> 'this meth
method addToStart:
- Jstr.t -> 'a t -> 'this meth
+ Jstr.t -> 'a t -> 'this t meth
method addToEnd:
- Jstr.t -> 'a t -> 'this meth
+ Jstr.t -> 'a t -> 'this t meth
end
-module Model = struct
+module Classes = struct
+
+ type 'a meta_data
type domOutputSpec
+ type parse_rule
+
+ type content_match
+
+ type slice
class type _node_props = object ('this)
method inlineContent:
- bool readonly_prop
+ bool t readonly_prop
(** True if this node type has inline content. *)
method isBlock:
- bool readonly_prop
+ bool t readonly_prop
method isText:
- bool readonly_prop
+ bool t readonly_prop
method isInline:
- bool readonly_prop
+ bool t readonly_prop
method isTextblock:
- bool readonly_prop
+ bool t readonly_prop
method isLeaf:
- bool readonly_prop
+ bool t readonly_prop
method isAtom:
- bool readonly_prop
+ bool t readonly_prop
end
+ type depth = int opt
class type mark = object ('this)
- method eq:
- 'this t -> bool t meth
+ method eq
+ : 'this t -> bool t meth
- method isInSet:
- mark t js_array t -> mark t opt meth
+ method isInSet
+ : mark t js_array t -> mark t opt meth
end
- type node_spec
+ class type node_spec = object ('this)
- type content_match
+ method content
+ : Jstr.t opt prop
- type slice
+ method marks
+ : Jstr.t opt prop
- type depth = int opt
+ method group
+ : Jstr.t opt prop
- class type resolved_pos = object ('this)
+ method inline
+ : bool t opt prop
- method pos:
- int readonly_prop
+ method atom
+ : bool t opt prop
- method depth:
- int readonly_prop
+ method attrs
+ : < .. > t opt prop
- method parent:
- node t readonly_prop
+ method selectable
+ : bool t opt prop
- method doc:
- node t readonly_prop
+ method draggable
+ : bool t opt prop
- method node:
- depth -> node t meth
+ method code
+ : bool t opt prop
- method index:
- depth -> int meth
+ method defining
+ : bool t opt prop
- method after:
- depth -> int meth
+ method isolating
+ : bool t opt prop
- method nodeAfter:
- node t opt readonly_prop
+ method toDOM
+ : (node t -> domOutputSpec t) callback prop
- method nodeBefore:
- node t opt readonly_prop
+ method parseDom
+ : parse_rule t js_array t opt prop
- method marks:
- unit -> mark t js_array t meth
+ end
+
+ and resolved_pos = object ('this)
+
+ method pos
+ : int readonly_prop
+
+ method depth
+ : int readonly_prop
+
+ method parentOffset
+ : int readonly_prop
+
+ method parent
+ : node t readonly_prop
- method sameParent:
- 'this -> bool t meth
+ method doc
+ : node t readonly_prop
- method max:
- 'this -> 'this t meth
+ method node
+ : depth -> node t meth
- method min:
- 'this -> 'this t meth
+ method index
+ : depth -> int meth
+
+ method after
+ : depth -> int meth
+
+ method nodeAfter
+ : node t opt readonly_prop
+
+ method nodeBefore
+ : node t opt readonly_prop
+
+ method marks
+ : unit -> mark t js_array t meth
+
+ method sameParent
+ : 'this t -> bool t meth
+
+ method max
+ : 'this t -> 'this t meth
+
+ method min
+ : 'this t -> 'this t meth
end
and mark_spec = object ('this)
@@ -222,8 +282,8 @@ module Model = struct
method hasRequiredAttrs:
unit -> bool t meth
- method create_withFragment:
- < .. > t -> fragment t opt -> mark t opt -> node t meth
+ method create_withFragmentContent:
+ < .. > t opt -> fragment t opt -> mark t opt -> node t meth
end
@@ -268,7 +328,7 @@ module Model = struct
(** Compare this element to another one. *)
method cut:
- int -> int opt -> 'this meth
+ int -> int opt -> 'this t meth
(** Cut out the element between the two given positions. *)
method toString:
@@ -284,128 +344,80 @@ module Model = struct
inherit _element
- method size:
- int readonly_prop
+ method size
+ : int readonly_prop
(** The size of the fragment, which is the total of the size of its
content nodes. *)
- method append:
- 'this t -> 'this t meth
+ method append
+ : 'this t -> 'this t meth
+
+ method lastChild
+ : node t opt readonly_prop
- method lastChild:
- node t opt readonly_prop
+ method firstChild
+ : node t opt readonly_prop
- method firstChild:
- node t opt readonly_prop
+ method findDiffStart
+ : 'this t -> int opt meth
+
+ method findDiffEnd
+ : 'this t -> < a: int prop; b: int prop> t opt meth
end
+ (** https://prosemirror.net/docs/ref/#model.Node *)
and node = object ('this)
inherit _element
inherit _node_props
- method _type:
- node_type t readonly_prop
-
- method attrs:
- < .. > t prop
+ method _type
+ : node_type t readonly_prop
- method content:
- fragment t prop
-
- method copy:
- fragment t -> 'this t meth
-
- method resolve:
- int -> resolved_pos t meth
-
- method nodeAt:
- int -> 'this t opt meth
-
- method marks:
- mark t js_array t readonly_prop
-
- method sameMarkupd:
- node t -> bool t meth
-
- method text:
- Jstr.t opt prop
-
- end
-
-end
+ method attrs
+ : < .. > t prop
-module Transform = struct
+ method content
+ : fragment t prop
- type step_result
+ method copy
+ : fragment t -> 'this t meth
- class type step = object ('this)
+ method slice
+ : from:int -> to_:int opt -> slice t meth
- end
+ method resolve
+ : int -> resolved_pos t meth
- class type replace_step = object ('this)
+ method nodeAt
+ : int -> 'this t opt meth
- inherit step
+ method marks
+ : mark t js_array t readonly_prop
- end
+ method sameMarkup
+ : node t -> bool t meth
- class type replace_around_step = object ('this)
-
- inherit step
+ method text
+ : Jstr.t opt prop
end
- class type add_mark_step = object ('this)
-
- inherit step
-
- end
-
-
- class type transform = object ('this)
-
- method doc:
- Model.node t readonly_prop
-
- method steps:
- step t js_array t readonly_prop
-
- method docs:
- Model.node t js_array t readonly_prop
-
- method step:
- step t -> 'this t meth
-
- method addMark:
- from:int -> to_:int -> Model.mark t -> 'this t meth
-
- method delete:
- from:int -> to_:int -> 'this t meth
-
- method insert:
- pos:int -> Model.node t -> 'this t meth
-
- method replaceRangeWith:
- from:int -> to_:int -> Model.node t -> 'this t meth
-
- method setBlockType:
- from:int -> to_:int -> Model.node_type t -> < .. > t -> 'this t meth
-
- end
-
-end
+ (** View *)
+ and editor_props = object ('this)
-module Classes = struct
+ method editable
+ : (editor_state t -> bool t) callback prop
+ method handleDOMEvents
+ : (editor_view t -> Jv.t -> bool t) callback TypedObject.t prop
- (** View *)
- class type editor_props = object ('this)
+ method nodeViews
+ : (node t -> editor_view t -> (unit -> int) -> < .. > t) TypedObject.t prop
- method editable:
- (editor_state t -> bool t) callback prop
end
and direct_editor_props = object ('this)
@@ -418,6 +430,7 @@ module Classes = struct
(** The call back is called with this = instance of editor_view *)
method dispatchTransaction:
(editor_view t, transaction t -> unit) meth_callback writeonly_prop
+
end
and editor_view = object ('this)
@@ -443,12 +456,21 @@ module Classes = struct
method updateState:
editor_state t -> unit meth
+ method hasFocus:
+ unit -> bool t meth
+
+ method focus:
+ unit -> unit meth
+
method posAtCoords:
< left: float prop ; top: float prop > t -> < pos: int prop; inside: int prop> t meth
method coordsAtPos:
int -> int opt -> < left: float prop; right: float prop; top: float prop; bottom: float prop > t meth
+ method destroy
+ : unit meth
+
method dispatch:
transaction t -> unit meth
@@ -483,13 +505,13 @@ module Classes = struct
'this t -> bool t meth
method content:
- unit -> Model.slice t meth
+ unit -> slice t meth
method replace:
- transaction t -> Model.slice t -> unit meth
+ transaction t -> slice t -> unit meth
method replaceWith:
- transaction t -> Model.node t -> unit meth
+ transaction t -> node t -> unit meth
end
@@ -505,70 +527,132 @@ module Classes = struct
end
+ (* Transform *)
+
+ and mappable = object ('this)
+
+ end
+
+ and step_map = object ('this)
+
+ inherit mappable
+
+ end
+
+ and step = object ('this)
+
+ method map
+ : mappable t -> 'this t meth
+
+ end
+
+ and transform = object ('this)
+
+ method doc
+ : node t readonly_prop
+
+ method steps
+ : step t js_array t readonly_prop
+
+ method docs
+ : node t js_array t readonly_prop
+
+ method step
+ : step t -> 'this t meth
+
+ method docChanged
+ : bool t prop
+
+ method addMark
+ : from:int -> to_:int -> mark t -> 'this t meth
+
+ method replace
+ : from:int -> to_:int -> slice t opt -> 'this t meth
+
+ method delete
+ : from:int -> to_:int -> 'this t meth
+
+ method insert
+ : pos:int -> node t -> 'this t meth
+
+ method replaceRangeWith
+ : from:int -> to_:int -> node t -> 'this t meth
+
+ method setBlockType
+ : from:int -> to_:int -> node_type t -> < .. > t -> 'this t meth
+
+ end
+
and transaction = object ('this)
- inherit Transform.transform
+ inherit transform
method time:
int readonly_prop
- method setTime:
- int -> 'this t meth
+ method setTime
+ : int -> 'this t meth
- method storedMarks:
- Model.mark t js_array t opt readonly_prop
+ method storedMarks
+ : mark t js_array t opt readonly_prop
- method setStoredMarks:
- Model.mark t js_array t opt -> 'this t meth
+ method setStoredMarks
+ : mark t js_array t opt -> 'this t meth
- method addStoredMark:
- Model.mark t -> 'this t meth
+ method addStoredMark
+ : mark t -> 'this t meth
- method removeStoredMark_mark:
- Model.mark t -> 'this t meth
+ method removeStoredMark_mark
+ : mark t -> 'this t meth
- method removeStoredMark_marktype:
- Model.mark_type t -> 'this t meth
+ method removeStoredMark_marktype
+ : mark_type t -> 'this t meth
- method ensureMarks:
- Model.mark t js_array t -> 'this t meth
+ method ensureMarks
+ : mark t js_array t -> 'this t meth
- method storedMarksSet:
- bool readonly_prop
+ method storedMarksSet
+ : bool readonly_prop
- method selection:
- selection t readonly_prop
+ method selection
+ : selection t readonly_prop
- method setSelection:
- selection t -> 'this t meth
+ method setSelection
+ : selection t -> 'this t meth
- method deleteSelection:
- 'this t meth
+ method deleteSelection
+ : 'this t meth
- method replaceSelection:
- Model.slice t -> 'this t meth
+ method replaceSelection
+ : slice t -> 'this t meth
- method replaceSelectionWith:
- Model.node t -> bool t -> 'this t meth
+ method replaceSelectionWith
+ : node t -> bool t opt -> 'this t meth
- method selectionSet:
- bool readonly_prop
+ method selectionSet
+ : bool readonly_prop
- method before:
- Model.node t readonly_prop
+ method before
+ : node t readonly_prop
- method insertText:
- Jstr.t -> from:int opt -> to_:int opt -> 'this t meth
+ method insertText
+ : Jstr.t -> from:int opt -> to_:int opt -> 'this t meth
- method scrollIntoView :
- unit -> 'this t meth
+ method setMeta
+ : 'a meta_data t -> 'a -> 'this t meth
+
+ method getMeta
+ : 'a meta_data t -> 'a optdef meth
+
+ method scrollIntoView
+ : unit -> 'this t meth
end
and configuration_prop = object ('this)
method schema:
- Model.schema t opt prop
+ schema t opt prop
method plugins:
plugin t js_array t opt prop
@@ -580,29 +664,29 @@ module Classes = struct
inherit configuration_prop
method doc:
- Model.node t opt prop
+ node t opt prop
method selection:
selection t opt prop
method storedMarks:
- Model.mark t js_array t opt prop
+ mark t js_array t opt prop
end
and editor_state = object ('this)
method doc :
- Model.node t readonly_prop
+ node t readonly_prop
method selection:
selection t readonly_prop
method storedMarks:
- Model.mark t js_array t opt readonly_prop
+ mark t js_array t opt readonly_prop
method schema:
- Model.schema t readonly_prop
+ schema t readonly_prop
method plugins:
plugin t js_array t readonly_prop
@@ -610,6 +694,10 @@ module Classes = struct
method apply:
transaction t -> 'this t meth
+ method applyTransaction
+ : transaction t ->
+ < state: 'this t prop; transactions : transaction t js_array t prop> t meth
+
method tr:
transaction t readonly_prop
@@ -623,8 +711,70 @@ module Classes = struct
end
+module Model = struct
+
+ type parse_rule = Classes.parse_rule
+
+ type domOutputSpec = Classes.domOutputSpec
+
+ type depth = Classes.depth
+
+ class type mark = Classes.mark
+
+ class type fragment = Classes.fragment
+
+ class type node_spec = Classes.node_spec
+
+ class type resolved_pos = Classes.resolved_pos
+
+ class type mark_spec = Classes.mark_spec
+
+ class type schema_spec = Classes.schema_spec
+
+ class type schema = Classes.schema
+
+ class type node_type = Classes.node_type
+
+ class type mark_type = Classes.mark_type
+
+ class type node = Classes.node
+
+end
+
+module Transform = struct
+
+ type step_result
+
+ class type step_map = Classes.step_map
+
+ class type step = Classes.step
+
+ class type replace_step = object ('this)
+
+ inherit step
+
+ end
+
+ class type replace_around_step = object ('this)
+
+ inherit step
+
+ end
+
+ class type add_mark_step = object ('this)
+
+ inherit step
+
+ end
+
+ class type transform = Classes.transform
+
+
+end
+
module State = struct
+ type 'a meta_data = 'a Classes.meta_data
class type plugin = Classes.plugin
class type selection = Classes.selection
class type text_selection = Classes.text_selection
@@ -710,21 +860,66 @@ module SchemaBasic = struct
end
+module Menu = struct
+
+ class type menuElement = object ('this)
+ end
+
+ class type menuItemSpec = object ('this)
+ method title
+ : Jstr.t opt prop
+
+ method label
+ : Jstr.t opt prop
+
+ method select
+ : (menuItem t, State.editor_state t -> bool t) meth_callback prop
+
+ method run
+ : (menuItem t, State.editor_state t -> (State.transaction t -> unit) -> View.editor_view t -> 'a Brr.Ev.t -> unit) meth_callback prop
+ end
+
+ and menuItem = object ('this)
+ inherit menuElement
+ end
+
+ class type dropdown = object ('this)
+
+ inherit menuElement
+
+ method content
+ : menuItem t js_array t prop
+ end
+end
+
module Example = struct
+ class type menuItems = object ('this)
+
+ method insertMenu
+ : Menu.dropdown t prop
+
+ method fullMenu
+ : Menu.menuElement t js_array t prop
+
+ end
+
class type options = object ('this)
- method schema:
- Model.schema t prop
+ method schema
+ : Model.schema t prop
+
+ method menuBar
+ : bool t opt prop
- method menuBar:
- bool t opt prop
+ method floatingMenu
+ : bool t opt prop
- method floatingMenu:
- bool t opt prop
+ method history
+ : bool t opt prop
- method history:
- bool t opt prop
+ method menuContent
+ : Menu.menuElement t js_array t prop
end
diff --git a/editor/prosemirror/prosemirror.ml b/editor/prosemirror/prosemirror.ml
index e37cc3b..8c436a3 100755
--- a/editor/prosemirror/prosemirror.ml
+++ b/editor/prosemirror/prosemirror.ml
@@ -108,6 +108,16 @@ module Model = struct
: < dom: node Js.t Js.readonly_prop ; contentDOM : node Js.t Js.opt Js.readonly_prop > Js.t -> domOutputSpec Js.t
= of_
end
+
+ module ParseRule = struct
+
+ let tag
+ : Jstr.t -> parse_rule Js.t
+ = fun name ->
+ Jv.obj [| "tag", Jv.of_jstr name |]
+ |> Jv.Id.of_jv
+
+ end
end
module State = struct
@@ -115,12 +125,12 @@ module State = struct
include Bindings.State
let configuration_prop
- : unit -> configuration_prop Js_of_ocaml.Js.t
- = fun () -> Js_of_ocaml.Js.Unsafe.obj [||]
+ : unit -> configuration_prop Js.t
+ = fun () -> Js.Unsafe.obj [||]
let creation_prop
: unit -> creation_prop Js.t
- = fun () -> Js_of_ocaml.Js.Unsafe.obj [||]
+ = fun () -> Js.Unsafe.obj [||]
let create
: t -> creation_prop Js.t -> editor_state Js.t
@@ -131,13 +141,18 @@ module State = struct
|> Jv.Id.of_jv
let fromJSON
- : t -> configuration_prop Js_of_ocaml.Js.t -> Brr.Json.t -> editor_state Js.t
+ : t -> configuration_prop Js.t -> Brr.Json.t -> editor_state Js.t
= fun t config json ->
let state = Jv.get t "state" in
let editor_state = Jv.get state "EditorState" in
Jv.call editor_state "fromJSON" [|Jv.Id.to_jv config ; json |]
|> Jv.Id.of_jv
+ let selection_from
+ : selection Js.t -> Model.resolved_pos Js.t
+ = fun selection ->
+ Jv.Id.(of_jv @@ Jv.get (to_jv selection) "$from")
+
let selection_to
: selection Js.t -> Model.resolved_pos Js.t
= fun selection ->
@@ -183,6 +198,10 @@ module State = struct
: selection Js.t -> Model.resolved_pos Js.t Js.opt
= fun selection ->
Jv.Id.(of_jv @@ Jv.get (to_jv selection) "$cursor")
+
+ let create_str_meta_data
+ : Jstr.t -> 'a meta_data Js.t
+ = Obj.magic
end
(* Editor view *)
@@ -196,7 +215,7 @@ module View = struct
include Bindings.View
let direct_editor_props
: unit -> direct_editor_props Js.t
- = fun () -> Js_of_ocaml.Js.Unsafe.obj [||]
+ = fun () -> Js.Unsafe.obj [||]
let editor_view
: t -> El.t -> direct_editor_props Js.t -> editor_view Js.t
@@ -206,6 +225,26 @@ module View = struct
end
+module Transform = struct
+
+ include Bindings.Transform
+
+ let offset
+ : t -> int -> step_map Js.t
+ = fun t n ->
+ let stepmap = Jv.get (Jv.get t "transform") "StepMap" in
+ Jv.call stepmap "offset" [|Jv.Id.to_jv n|]
+ |> Jv.Id.of_jv
+
+ let insertPoint
+ : t -> Model.node Js.t -> pos:int -> Model.node_type Js.t -> int Js.opt
+ = fun t node ~pos node_t ->
+ let transform = Jv.get t "transform" in
+ Jv.call transform "insertPoint" Jv.Id.[|to_jv node ; to_jv pos; to_jv node_t|]
+ |> Jv.Id.of_jv
+
+end
+
module Commands = struct
type t = State.editor_state Js.t -> State.dispatch Js.opt -> bool Js.t
@@ -238,7 +277,7 @@ module History = struct
let history_prop
: unit -> history_prop Js.t
- = fun () -> Js_of_ocaml.Js.Unsafe.obj [||]
+ = fun () -> Js.Unsafe.obj [||]
let history
: t -> history_prop Js.t -> State.plugin Js.t
@@ -324,6 +363,22 @@ module SchemaList = struct
end
+module Menu = struct
+
+ include Bindings.Menu
+
+ let menuItemSpec
+ : unit -> menuItemSpec Js.t
+ = fun () -> Js.Unsafe.obj [||]
+
+ let menu_item
+ : t -> menuItemSpec Js.t -> menuItem Js.t
+ = fun t spec ->
+ let menu = Jv.get t "menu" in
+ Jv.new' (Jv.get menu "MenuItem") [| Jv.Id.to_jv spec |]
+ |> Jv.Id.of_jv
+
+end
(* Example Setup *)
@@ -343,4 +398,11 @@ module Example = struct
let setup = Jv.get t "example_setup" in
Jv.call setup "exampleSetup" [|Jv.Id.to_jv options|]
|> Jv.Id.of_jv
+
+ let buildMenuItems
+ : t -> Model.schema Js.t -> menuItems Js.t
+ = fun t schema ->
+ let setup = Jv.get t "example_setup" in
+ Jv.call setup "buildMenuItems" [|Jv.Id.to_jv schema|]
+ |> Jv.Id.of_jv
end
diff --git a/editor/prosemirror/prosemirror.mli b/editor/prosemirror/prosemirror.mli
index eac895a..76545d2 100755
--- a/editor/prosemirror/prosemirror.mli
+++ b/editor/prosemirror/prosemirror.mli
@@ -10,58 +10,9 @@ val v
module O = Bindings.TypedObject
-module rec Model : sig
-
- include module type of Bindings.Model
-
- val schema_spec:
- node_spec Bindings.ordered_map Js.t
- -> mark_spec Bindings.ordered_map Js.t option
- -> string option
- -> schema_spec Js.t
-
- val schema
- : t -> schema_spec Js.t -> schema Js.t
-
- module DOMParser : sig
-
- type parser
-
- val from_schema
- : t -> schema Js.t -> parser
-
- val parse
- : parser -> El.t -> node Js.t
-
- end
-
- val empty_fragment
- : t -> fragment Js.t
-
- module Dom_output_spec : sig
-
- val v
- : ?attrs:< .. > -> string -> domOutputSpec Js.t list -> domOutputSpec Js.t
-
- (** Hole element inside an output_spec element *)
- val hole
- : domOutputSpec Js.t
-
- val of_el
- : Brr.El.t -> domOutputSpec Js.t
-
- val of_jstr
- : Jstr.t -> domOutputSpec Js.t
-
- val of_obj
- : < dom: node Js.t Js.readonly_prop ; contentDOM : node Js.t Js.opt Js.readonly_prop > Js.t -> domOutputSpec Js.t
- end
-
-end
-
(* State *)
-and State : sig
+module rec State : sig
include module type of Bindings.State
@@ -77,6 +28,9 @@ and State : sig
val fromJSON
: t -> configuration_prop Js.t -> Brr.Json.t -> editor_state Js.t
+ val selection_from
+ : selection Js.t -> Model.resolved_pos Js.t
+
val selection_to
: selection Js.t -> Model.resolved_pos Js.t
@@ -98,6 +52,9 @@ and State : sig
val cursor
: selection Js.t -> Model.resolved_pos Js.t Js.opt
+ val create_str_meta_data
+ : Jstr.t -> 'a meta_data Js.t
+
end
(* Editor view *)
@@ -120,6 +77,73 @@ and View : sig
end
+and Model : sig
+
+ include module type of Bindings.Model
+
+ val schema_spec:
+ node_spec Bindings.ordered_map Js.t
+ -> mark_spec Bindings.ordered_map Js.t option
+ -> string option
+ -> schema_spec Js.t
+
+ val schema
+ : t -> schema_spec Js.t -> schema Js.t
+
+ module DOMParser : sig
+
+ type parser
+
+ val from_schema
+ : t -> schema Js.t -> parser
+
+ val parse
+ : parser -> El.t -> node Js.t
+
+ end
+
+ val empty_fragment
+ : t -> fragment Js.t
+
+ module Dom_output_spec : sig
+
+ val v
+ : ?attrs:< .. > -> string -> domOutputSpec Js.t list -> domOutputSpec Js.t
+
+ (** Hole element inside an output_spec element *)
+ val hole
+ : domOutputSpec Js.t
+
+ val of_el
+ : Brr.El.t -> domOutputSpec Js.t
+
+ val of_jstr
+ : Jstr.t -> domOutputSpec Js.t
+
+ val of_obj
+ : < dom: node Js.t Js.readonly_prop ; contentDOM : node Js.t Js.opt Js.readonly_prop > Js.t -> domOutputSpec Js.t
+ end
+
+ module ParseRule : sig
+
+ val tag: Jstr.t -> parse_rule Js.t
+
+ end
+
+end
+
+and Transform : sig
+
+ include module type of Bindings.Transform
+
+ val offset
+ : t -> int -> step_map Js.t
+
+ val insertPoint
+ : t -> Model.node Js.t -> pos:int -> Model.node_type Js.t -> int Js.opt
+
+end
+
module Commands : sig
type t = State.editor_state Js.t -> State.dispatch Js.opt -> bool Js.t
@@ -190,6 +214,17 @@ module SchemaList : sig
end
+module Menu : sig
+
+ include module type of Bindings.Menu
+
+ val menuItemSpec
+ : unit -> menuItemSpec Js.t
+
+ val menu_item
+ : t -> menuItemSpec Js.t -> menuItem Js.t
+end
+
(* Example Setup *)
module Example : sig
@@ -201,4 +236,7 @@ module Example : sig
val example_setup
: t -> options Js.t -> State.plugin Js.t Js.js_array Js.t
-end
+
+ val buildMenuItems
+ : t -> Model.schema Js.t -> menuItems Js.t
+end
diff --git a/editor/tooltip.ml b/editor/tooltip.ml
index 693d68d..43c345f 100755
--- a/editor/tooltip.ml
+++ b/editor/tooltip.ml
@@ -29,10 +29,9 @@ let set_position
Jstr.( (of_float ( box_bottom -. start##.top )) + (v "px") )
el
-let tooltip
+let boldtip
: PM.View.editor_view Js.t -> < .. > Js.t
= fun view ->
-
(* Create the element which will be displayed over the editor *)
let tooltip = El.div []
~at:At.([class' (Jstr.v "tooltip")]) in
@@ -45,86 +44,48 @@ let tooltip
: PM.View.editor_view Js.t -> PM.State.editor_state Js. t Js.opt -> unit
= fun view state_opt ->
- Js.Opt.iter state_opt
- (fun previous_state ->
- if ((view##.state##.doc##eq previous_state##.doc) = Js._true)
- && ((previous_state##.selection##eq view##.state##.selection) = Js._true)
- then
- ()
- else (
- if (view##.state##.selection##.empty) = Js._true then
- (* Hide the tooltip if the selection is empty *)
- El.set_inline_style El.Style.display (Jstr.v "none") tooltip
- else (
- (* otherwise, reposition it and update its content *)
- set_position view tooltip;
- El.set_prop
- (El.Prop.jstr (Jstr.v "textContent"))
- (Jstr.of_int
- (view##.state##.selection##._to - view##.state##.selection##.from))
- tooltip)))
- and destroy () = El.remove tooltip in
-
- object%js
- val update = Js.wrap_callback update
- val destroy= Js.wrap_callback destroy
- end
-
-let tooltip_plugin
- : PM.t -> PM.State.plugin Js.t
- = fun t ->
- let state = Jv.get (Jv.Id.to_jv t) "state" in
-
- let params = object%js
- val view = (fun view -> tooltip view)
- end in
-
- Jv.new' (Jv.get state "Plugin") [| Jv.Id.to_jv params |]
- |> Jv.Id.of_jv
-
-
-let boldtip
- : PM.View.editor_view Js.t -> < .. > Js.t
- = fun view ->
- (* Create the element which will be displayed over the editor *)
- let tooltip = El.div []
- ~at:At.([class' (Jstr.v "tooltip")]) in
- El.set_inline_style El.Style.display (Jstr.v "none") tooltip;
-
- let parent = Jv.(Id.of_jv @@ get (Jv.Id.to_jv view##.dom) "parentNode") in
- let () = El.append_children parent [tooltip] in
-
- let update
- : PM.View.editor_view Js.t -> PM.State.editor_state Js. t Js.opt -> unit
- = fun view _state_opt ->
+ (* Compare the previous and actual state. If the stored marks are the
+ same, just return *)
let state = view##.state in
- let is_bold = Option.bind (PM.O.get state##.schema##.marks "strong")
- (fun mark_type ->
- let is_strong =
- Js.Opt.bind state##.storedMarks
- (fun t -> mark_type##isInSet t) in
- Js.Opt.case is_strong
- (fun () -> None)
- (fun _ -> Some (Jstr.v "gras"))) in
- let is_em = Option.bind (PM.O.get state##.schema##.marks "em")
- (fun mark_type ->
- let is_strong =
- Js.Opt.bind state##.storedMarks
- (fun t -> mark_type##isInSet t) in
- Js.Opt.case is_strong
- (fun () -> None)
- (fun _ -> Some (Jstr.(v "emphase")))) in
-
- let marks = List.filter_map [is_bold; is_em] ~f:(fun x -> x) in
- match marks with
- | [] -> El.set_inline_style El.Style.display (Jstr.v "none") tooltip
- | _ ->
- (* The mark is present, add in the content *)
- set_position view tooltip;
- El.set_prop
- (El.Prop.jstr (Jstr.v "textContent"))
- (Jstr.concat marks ~sep:(Jstr.v ", "))
- tooltip
+ let previous_stored_marks =
+ Js.Opt.bind state_opt (fun state -> state##.storedMarks)
+ |> Js.Opt.to_option
+ and current_stored_marks = state##.storedMarks in
+ let same = match previous_stored_marks, Js.Opt.to_option current_stored_marks with
+ | Some arr1, Some arr2 ->
+ Js_lib.Array.compare arr1 arr2 ~f:(fun v1 v2 -> v1##eq v2)
+ | None, None -> Js._true
+ | _, _ -> Js._false in
+
+ if same <> Js._true then
+
+ let is_bold = Option.bind (PM.O.get state##.schema##.marks "strong")
+ (fun mark_type ->
+ let is_strong =
+ Js.Opt.bind current_stored_marks
+ (fun t -> mark_type##isInSet t) in
+ Js.Opt.case is_strong
+ (fun () -> None)
+ (fun _ -> Some (Jstr.v "gras"))) in
+ let is_em = Option.bind (PM.O.get state##.schema##.marks "em")
+ (fun mark_type ->
+ let is_strong =
+ Js.Opt.bind current_stored_marks
+ (fun t -> mark_type##isInSet t) in
+ Js.Opt.case is_strong
+ (fun () -> None)
+ (fun _ -> Some (Jstr.(v "emphase")))) in
+
+ let marks = List.filter_map [is_bold; is_em] ~f:(fun x -> x) in
+ match marks with
+ | [] -> El.set_inline_style El.Style.display (Jstr.v "none") tooltip
+ | _ ->
+ (* The mark is present, add in the content *)
+ set_position view tooltip;
+ El.set_prop
+ (El.Prop.jstr (Jstr.v "textContent"))
+ (Jstr.concat marks ~sep:(Jstr.v ", "))
+ tooltip
and destroy () = El.remove tooltip in