-- Render color picker view menu item.
+### In-Progress
+
+- Modal Dialogs for details such as links
+ - Got dialog + form + input ready
+ - Next stage is creating a button (eg, anchor insert) which toggles/shows dialog box.
+ - Dialog box should attach at bottom of dom (Prevent z-index issues).
+ - At some level a layer is needed to wire up the existing components.
### Features
import crel from "crelt"
-const prefix = "ProseMirror-menu"
+import {prefix} from "./menu-utils";
import {toggleMark} from "prosemirror-commands";
class ColorPickerGrid {
--- /dev/null
+// ::- Represents a submenu wrapping a group of elements that start
+// hidden and expand to the right when hovered over or tapped.
+import {prefix, renderItems} from "./menu-utils";
+import crel from "crelt";
+import {getIcon, icons} from "./icons";
+
+class DialogBox {
+ // :: ([MenuElement], ?Object)
+ // The following options are recognized:
+ //
+ // **`label`**`: string`
+ // : The label to show on the dialog.
+ // **`closer`**`: function`
+ // : The function to run when the dialog should close.
+ constructor(content, options) {
+ this.options = options || {};
+ this.content = Array.isArray(content) ? content : [content];
+
+ this.closeMouseDownListener = null;
+ this.wrap = null;
+ }
+
+ // :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
+ // Renders the submenu.
+ render(view) {
+ const items = renderItems(this.content, view)
+
+ const titleText = crel("div", {class: prefix + "-dialog-title-text"}, this.options.label);
+ const titleClose = crel("button", {class: prefix + "-dialog-title-close primary-background", type: "button"}, getIcon(icons.close));
+ const titleContent = crel("div", {class: prefix + "-dialog-title"}, titleText, titleClose);
+ const dialog = crel("div", {class: prefix + "-dialog"}, titleContent,
+ crel("div", {class: prefix + "-dialog-content"}, items.dom));
+ const wrap = crel("div", {class: prefix + "-dialog-wrap"}, dialog);
+ this.wrap = wrap;
+
+ this.closeMouseDownListener = (event) => {
+ if (!dialog.contains(event.target) || titleClose.contains(event.target)) {
+ this.close();
+ }
+ }
+
+ wrap.addEventListener("click", this.closeMouseDownListener);
+
+ function update(state) {
+ let inner = items.update(state)
+ wrap.style.display = inner ? "" : "none"
+ return inner;
+ }
+ return {dom: wrap, update}
+ }
+
+ close() {
+ if (this.options.closer) {
+ this.options.closer();
+ }
+
+ if (this.closeMouseDownListener) {
+ this.wrap.removeEventListener("click", this.closeMouseDownListener);
+ }
+ }
+}
+
+export default DialogBox;
\ No newline at end of file
--- /dev/null
+// ::- Represents a submenu wrapping a group of elements that start
+// hidden and expand to the right when hovered over or tapped.
+import {prefix, renderItems} from "./menu-utils";
+import crel from "crelt";
+
+class DialogForm {
+ // :: ([MenuElement], ?Object)
+ // The following options are recognized:
+ //
+ // **`action`**`: function(FormData)`
+ // : The submission action to run when the form is submitted.
+ // **`canceler`**`: function`
+ // : The cancel action to run when the form is cancelled.
+ constructor(content, options) {
+ this.options = options || {};
+ this.content = Array.isArray(content) ? content : [content];
+ }
+
+ // :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
+ // Renders the submenu.
+ render(view) {
+ const items = renderItems(this.content, view)
+
+ const formButtonCancel = crel("button", {class: prefix + "-dialog-button", type: "button"}, "Cancel");
+ const formButtonSave = crel("button", {class: prefix + "-dialog-button", type: "submit"}, "Save");
+ const footer = crel("div", {class: prefix + "-dialog-footer"}, formButtonCancel, formButtonSave);
+ const form = crel("form", {class: prefix + "-dialog-form", action: '#'}, items.dom, footer);
+
+ form.addEventListener('submit', event => {
+ event.preventDefault();
+ if (this.options.action) {
+ this.options.action(new FormData(form));
+ }
+ });
+
+ formButtonCancel.addEventListener('click', event => {
+ if (this.options.canceler) {
+ this.options.canceler();
+ }
+ });
+
+ function update(state) {
+ return items.update(state);
+ }
+
+ return {dom: form, update}
+ }
+
+}
+
+export default DialogForm;
\ No newline at end of file
--- /dev/null
+// ::- Represents a submenu wrapping a group of elements that start
+// hidden and expand to the right when hovered over or tapped.
+import {prefix, randHtmlId} from "./menu-utils";
+import crel from "crelt";
+
+class DialogInput {
+ // :: (?Object)
+ // The following options are recognized:
+ //
+ // **`label`**`: string`
+ // : The label to show for the input.
+ // **`id`**`: string`
+ // : The id to use for this input
+ // **`attrs`**`: Object`
+ // : The attributes to add to the input element.
+ // **`value`**`: function(state) -> string`
+ // : The getter for the input value.
+ constructor(options) {
+ this.options = options || {};
+ }
+
+ // :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
+ // Renders the submenu.
+ render(view) {
+ const id = randHtmlId();
+ const inputAttrs = Object.assign({type: "text", name: this.options.id, id: this.options.id}, this.options.attrs || {})
+ const input = crel("input", inputAttrs);
+ const label = crel("label", {for: id}, this.options.label);
+
+ const rowRap = crel("div", {class: prefix + '-dialog-form-row'}, label, input);
+
+ const update = (state) => {
+ input.value = this.options.value(state);
+ return true;
+ }
+
+ return {dom: rowRap, update}
+ }
+
+}
+
+export default DialogInput;
\ No newline at end of file
width: 24, height: 24,
path: "M3.27 5L2 6.27l6.97 6.97L6.5 19h3l1.57-3.66L16.73 21 18 19.73 3.55 5.27 3.27 5zM6 5v.18L8.82 8h2.4l-.72 1.68 2.1 2.1L14.21 8H20V5H6z"
},
+ close: {
+ width: 24, height: 24,
+ path: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
+ }
};
const SVG = "http://www.w3.org/2000/svg"
} from "./menu"
import {icons} from "./icons";
import ColorPickerGrid from "./ColorPickerGrid";
+import DialogBox from "./DialogBox";
import {toggleMark} from "prosemirror-commands";
import {menuBar} from "./menubar"
import schema from "../schema";
import {removeMarks} from "../commands";
+import DialogForm from "./DialogForm";
+import DialogInput from "./DialogInput";
function cmdItem(cmd, options) {
}),
];
+function getMarkAttribute(markType, attribute) {
+ return function(state) {
+ const marks = state.selection.$head.marks();
+ for (const mark of marks) {
+ if (mark.type === markType) {
+ return mark.attrs[attribute];
+ }
+ }
+
+ return null;
+ };
+}
+
+let box = new DialogBox([
+ new DialogForm([
+ new DialogInput({
+ label: 'URL',
+ id: 'url',
+ value: getMarkAttribute(schema.marks.link, 'href'),
+ }),
+ new DialogInput({
+ label: 'Title',
+ id: 'title',
+ value: getMarkAttribute(schema.marks.link, 'title'),
+ })
+ ], {
+ canceler: () => box.close(),
+ action: (data) => console.log('submit', data),
+ }),
+], {label: 'Insert Link', closer: () => {console.log('close')}});
+
const menu = menuBar({
floating: false,
content: [
lists,
inserts,
utilities,
+ [box]
],
});
--- /dev/null
+import crel from "crelt";
+
+export const prefix = "ProseMirror-menu";
+
+export function renderDropdownItems(items, view) {
+ let rendered = [], updates = []
+ for (let i = 0; i < items.length; i++) {
+ let {dom, update} = items[i].render(view)
+ rendered.push(crel("div", {class: prefix + "-dropdown-item"}, dom))
+ updates.push(update)
+ }
+ return {dom: rendered, update: combineUpdates(updates, rendered)}
+}
+
+export function renderItems(items, view) {
+ let rendered = [], updates = []
+ for (let i = 0; i < items.length; i++) {
+ let {dom, update} = items[i].render(view)
+ rendered.push(dom);
+ updates.push(update)
+ }
+ return {dom: rendered, update: combineUpdates(updates, rendered)}
+}
+
+export function combineUpdates(updates, nodes) {
+ return state => {
+ let something = false
+ for (let i = 0; i < updates.length; i++) {
+ let up = updates[i](state)
+ nodes[i].style.display = up ? "" : "none"
+ if (up) something = true
+ }
+ return something
+ }
+}
+
+export function randHtmlId() {
+ return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 9);
+}
\ No newline at end of file
import {lift, joinUp, selectParentNode, wrapIn, setBlockType, toggleMark} from "prosemirror-commands"
import {undo, redo} from "prosemirror-history"
import {setBlockAttr, insertBlockBefore} from "../commands";
+import {renderDropdownItems, combineUpdates} from "./menu-utils";
import {getIcon, icons} from "./icons"
-
-const prefix = "ProseMirror-menu"
+import {prefix} from "./menu-utils";
// ::- An icon or label that, when clicked, executes a command.
export class MenuItem {
}
}
-function renderDropdownItems(items, view) {
- let rendered = [], updates = []
- for (let i = 0; i < items.length; i++) {
- let {dom, update} = items[i].render(view)
- rendered.push(crel("div", {class: prefix + "-dropdown-item"}, dom))
- updates.push(update)
- }
- return {dom: rendered, update: combineUpdates(updates, rendered)}
-}
-
-function combineUpdates(updates, nodes) {
- return state => {
- let something = false
- for (let i = 0; i < updates.length; i++) {
- let up = updates[i](state)
- nodes[i].style.display = up ? "" : "none"
- if (up) something = true
- }
- return something
- }
-}
// ::- Represents a submenu wrapping a group of elements that start
// hidden and expand to the right when hovered over or tapped.
height: 20px;
border: 2px solid #FFF;
display: block;
+}
+
+.ProseMirror-menu-dialog-wrap {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.1);
+ z-index: 50;
+ display: grid;
+}
+
+.ProseMirror-menu-dialog-title {
+ padding: $-xs $-s;
+ border-bottom: 1px solid #DDD;
+ font-weight: bold;
+ position: relative;
+ margin-bottom: $-xs;
+}
+
+.ProseMirror-menu-dialog-footer {
+ padding: $-xs $-s;
+ border-top: 1px solid #DDD;
+ display: flex;
+ justify-content: end;
+ margin-top: $-xs;
+}
+
+.ProseMirror-menu-dialog-title-close {
+ color: #FFF;
+ position: absolute;
+ top: $-xs + 2px;
+ right: $-s;
+ border-radius: 9px;
+ height: 18px;
+ width: 18px;
+ text-align: center;
+ line-height: 0;
+ vertical-align: top;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.ProseMirror-menu-dialog {
+ background-color: #FFF;
+ border: 1px solid #DDD;
+ border-radius: 3px;
+ box-shadow: $bs-large;
+ width: fit-content;
+ min-width: 300px;
+ min-height: 100px;
+ margin: auto;
+}
+
+.ProseMirror-menu-dialog-button {
+ border: 1px solid #DDD;
+ padding: $-xs $-s;
+ color: #666;
+ min-width: 80px;
+ cursor: pointer;
+ &:hover {
+ background-color: #EEE;
+ }
+}
+
+.ProseMirror-menu-dialog-button + .ProseMirror-menu-dialog-button {
+ margin-left: $-xs;
+}
+
+.ProseMirror-menu-dialog-form-row {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ align-items: center;
+ padding: $-xs 0;
+ label {
+ padding: 0 $-s;
+ font-size: .9rem;
+ }
+ input {
+ margin: 0 $-s;
+ }
}
\ No newline at end of file
Some <span style="text-decoration: underline">Underlined content</span> Lorem ipsum dolor sit amet. <br>
Some <span style="text-decoration: line-through;">striked content</span> Lorem ipsum dolor sit amet. <br>
Some <span style="color: red;">Red Content</span> Lorem ipsum dolor sit amet. <br>
+ Some <a href="https://cats.com" target="_blank" title="link A">Linked Content</a> Lorem ipsum dolor sit amet. <br>
</p>
<p><img src="/user_avatar.png" alt="Logo"></p>
<ul>