mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-28 17:02:04 +03:00
Started menu dialog support
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
import crel from "crelt"
|
||||
const prefix = "ProseMirror-menu"
|
||||
import {prefix} from "./menu-utils";
|
||||
import {toggleMark} from "prosemirror-commands";
|
||||
|
||||
class ColorPickerGrid {
|
||||
|
63
resources/js/editor/menu/DialogBox.js
Normal file
63
resources/js/editor/menu/DialogBox.js
Normal file
@ -0,0 +1,63 @@
|
||||
// ::- 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;
|
51
resources/js/editor/menu/DialogForm.js
Normal file
51
resources/js/editor/menu/DialogForm.js
Normal file
@ -0,0 +1,51 @@
|
||||
// ::- 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;
|
42
resources/js/editor/menu/DialogInput.js
Normal file
42
resources/js/editor/menu/DialogInput.js
Normal file
@ -0,0 +1,42 @@
|
||||
// ::- 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;
|
@ -109,6 +109,10 @@ export const icons = {
|
||||
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"
|
||||
|
@ -4,10 +4,13 @@ import {
|
||||
} 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) {
|
||||
@ -161,6 +164,37 @@ const utilities = [
|
||||
}),
|
||||
];
|
||||
|
||||
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: [
|
||||
@ -172,6 +206,7 @@ const menu = menuBar({
|
||||
lists,
|
||||
inserts,
|
||||
utilities,
|
||||
[box]
|
||||
],
|
||||
});
|
||||
|
||||
|
39
resources/js/editor/menu/menu-utils.js
Normal file
39
resources/js/editor/menu/menu-utils.js
Normal file
@ -0,0 +1,39 @@
|
||||
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);
|
||||
}
|
@ -9,10 +9,10 @@ import crel from "crelt"
|
||||
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 {
|
||||
@ -212,27 +212,6 @@ export class Dropdown {
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
Reference in New Issue
Block a user