diff options
Diffstat (limited to 'src')
33 files changed, 971 insertions, 245 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 90213270f..d70e95c0a 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/util/ProsemirrorKeymap.ts b/src/client/util/ProsemirrorKeymap.ts new file mode 100644 index 000000000..00d086b97 --- /dev/null +++ b/src/client/util/ProsemirrorKeymap.ts @@ -0,0 +1,100 @@ +import { Schema } from "prosemirror-model"; +import { + wrapIn, setBlockType, chainCommands, toggleMark, exitCode, + joinUp, joinDown, lift, selectParentNode +} from "prosemirror-commands"; +import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirror-schema-list"; +import { undo, redo } from "prosemirror-history"; +import { undoInputRule } from "prosemirror-inputrules"; +import { Transaction, EditorState } from "prosemirror-state"; + +const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; + +export type KeyMap = { [key: string]: any }; + +export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap { + let keys: { [key: string]: any } = {}, type; + + function bind(key: string, cmd: any) { + if (mapKeys) { + let mapped = mapKeys[key]; + if (mapped === false) return; + if (mapped) key = mapped; + } + keys[key] = cmd; + } + + bind("Mod-z", undo); + bind("Shift-Mod-z", redo); + bind("Backspace", undoInputRule); + + if (!mac) { + bind("Mod-y", redo); + } + + bind("Alt-ArrowUp", joinUp); + bind("Alt-ArrowDown", joinDown); + bind("Mod-BracketLeft", lift); + bind("Escape", selectParentNode); + + if (type = schema.marks.strong) { + bind("Mod-b", toggleMark(type)); + bind("Mod-B", toggleMark(type)); + } + if (type = schema.marks.em) { + bind("Mod-i", toggleMark(type)); + bind("Mod-I", toggleMark(type)); + } + if (type = schema.marks.code) { + bind("Mod-`", toggleMark(type)); + } + + if (type = schema.nodes.bullet_list) { + bind("Ctrl-b", wrapInList(type)); + } + if (type = schema.nodes.ordered_list) { + bind("Ctrl-n", wrapInList(type)); + } + if (type = schema.nodes.blockquote) { + bind("Ctrl->", wrapIn(type)); + } + if (type = schema.nodes.hard_break) { + let br = type, cmd = chainCommands(exitCode, (state, dispatch) => { + if (dispatch) { + dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView()); + return true; + } + return false; + }); + bind("Mod-Enter", cmd); + bind("Shift-Enter", cmd); + if (mac) { + bind("Ctrl-Enter", cmd); + } + } + if (type = schema.nodes.list_item) { + bind("Enter", splitListItem(type)); + bind("Shift-Tab", liftListItem(type)); + bind("Tab", sinkListItem(type)); + } + if (type = schema.nodes.paragraph) { + bind("Shift-Ctrl-0", setBlockType(type)); + } + if (type = schema.nodes.code_block) { + bind("Shift-Ctrl-\\", setBlockType(type)); + } + if (type = schema.nodes.heading) { + for (let i = 1; i <= 6; i++) { + bind("Shift-Ctrl-" + i, setBlockType(type, { level: i })); + } + } + if (type = schema.nodes.horizontal_rule) { + let hr = type; + bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); + return true; + }); + } + + return keys; +}
\ No newline at end of file diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 92944bec0..9ef71e305 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -7,135 +7,134 @@ import { EditorState, Transaction, NodeSelection, } from "prosemirror-state"; import { EditorView, } from "prosemirror-view"; const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], - preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; - + preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; // :: Object // [Specs](#model.NodeSpec) for the nodes defined in this schema. export const nodes: { [index: string]: NodeSpec } = { - // :: NodeSpec The top level document node. - doc: { - content: "block+" - }, - - // :: NodeSpec A plain paragraph textblock. Represented in the DOM - // as a `<p>` element. - paragraph: { - content: "inline*", - group: "block", - parseDOM: [{ tag: "p" }], - toDOM() { return pDOM; } - }, - - // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. - blockquote: { - content: "block+", - group: "block", - defining: true, - parseDOM: [{ tag: "blockquote" }], - toDOM() { return blockquoteDOM; } - }, - - // :: NodeSpec A horizontal rule (`<hr>`). - horizontal_rule: { - group: "block", - parseDOM: [{ tag: "hr" }], - toDOM() { return hrDOM; } - }, - - // :: NodeSpec A heading textblock, with a `level` attribute that - // should hold the number 1 to 6. Parsed and serialized as `<h1>` to - // `<h6>` elements. - heading: { - attrs: { level: { default: 1 } }, - content: "inline*", - group: "block", - defining: true, - parseDOM: [{ tag: "h1", attrs: { level: 1 } }, - { tag: "h2", attrs: { level: 2 } }, - { tag: "h3", attrs: { level: 3 } }, - { tag: "h4", attrs: { level: 4 } }, - { tag: "h5", attrs: { level: 5 } }, - { tag: "h6", attrs: { level: 6 } }], - toDOM(node: any) { return ["h" + node.attrs.level, 0]; } - }, - - // :: NodeSpec A code listing. Disallows marks or non-text inline - // nodes by default. Represented as a `<pre>` element with a - // `<code>` element inside of it. - code_block: { - content: "text*", - marks: "", - group: "block", - code: true, - defining: true, - parseDOM: [{ tag: "pre", preserveWhitespace: "full" }], - toDOM() { return preDOM; } - }, - - // :: NodeSpec The text node. - text: { - group: "inline" - }, - - // :: NodeSpec An inline image (`<img>`) node. Supports `src`, - // `alt`, and `href` attributes. The latter two default to the empty - // string. - image: { - inline: true, - attrs: { - src: {}, - alt: { default: null }, - title: { default: null } - }, - group: "inline", - draggable: true, - parseDOM: [{ - tag: "img[src]", getAttrs(dom: any) { - return { - src: dom.getAttribute("src"), - title: dom.getAttribute("title"), - alt: dom.getAttribute("alt") - }; - } - }], - toDOM(node: any) { return ["img", node.attrs]; } - }, - - // :: NodeSpec A hard line break, represented in the DOM as `<br>`. - hard_break: { - inline: true, - group: "inline", - selectable: false, - parseDOM: [{ tag: "br" }], - toDOM() { return brDOM; } - }, - - ordered_list: { - ...orderedList, - content: 'list_item+', - group: 'block' - }, - //this doesn't currently work for some reason - bullet_list: { - ...bulletList, - content: 'list_item+', - group: 'block', - // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }], - // toDOM() { return ulDOM } - }, - //bullet_list: { - // content: 'list_item+', - // group: 'block', - //active: blockActive(schema.nodes.bullet_list), - //enable: wrapInList(schema.nodes.bullet_list), - //run: wrapInList(schema.nodes.bullet_list), - //select: state => true, - // }, - list_item: { - ...listItem, - content: 'paragraph block*' - } + // :: NodeSpec The top level document node. + doc: { + content: "block+" + }, + + // :: NodeSpec A plain paragraph textblock. Represented in the DOM + // as a `<p>` element. + paragraph: { + content: "inline*", + group: "block", + parseDOM: [{ tag: "p" }], + toDOM() { return pDOM; } + }, + + // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. + blockquote: { + content: "block+", + group: "block", + defining: true, + parseDOM: [{ tag: "blockquote" }], + toDOM() { return blockquoteDOM; } + }, + + // :: NodeSpec A horizontal rule (`<hr>`). + horizontal_rule: { + group: "block", + parseDOM: [{ tag: "hr" }], + toDOM() { return hrDOM; } + }, + + // :: NodeSpec A heading textblock, with a `level` attribute that + // should hold the number 1 to 6. Parsed and serialized as `<h1>` to + // `<h6>` elements. + heading: { + attrs: { level: { default: 1 } }, + content: "inline*", + group: "block", + defining: true, + parseDOM: [{ tag: "h1", attrs: { level: 1 } }, + { tag: "h2", attrs: { level: 2 } }, + { tag: "h3", attrs: { level: 3 } }, + { tag: "h4", attrs: { level: 4 } }, + { tag: "h5", attrs: { level: 5 } }, + { tag: "h6", attrs: { level: 6 } }], + toDOM(node: any) { return ["h" + node.attrs.level, 0]; } + }, + + // :: NodeSpec A code listing. Disallows marks or non-text inline + // nodes by default. Represented as a `<pre>` element with a + // `<code>` element inside of it. + code_block: { + content: "text*", + marks: "", + group: "block", + code: true, + defining: true, + parseDOM: [{ tag: "pre", preserveWhitespace: "full" }], + toDOM() { return preDOM; } + }, + + // :: NodeSpec The text node. + text: { + group: "inline" + }, + + // :: NodeSpec An inline image (`<img>`) node. Supports `src`, + // `alt`, and `href` attributes. The latter two default to the empty + // string. + image: { + inline: true, + attrs: { + src: {}, + alt: { default: null }, + title: { default: null } + }, + group: "inline", + draggable: true, + parseDOM: [{ + tag: "img[src]", getAttrs(dom: any) { + return { + src: dom.getAttribute("src"), + title: dom.getAttribute("title"), + alt: dom.getAttribute("alt") + }; + } + }], + toDOM(node: any) { return ["img", node.attrs]; } + }, + + // :: NodeSpec A hard line break, represented in the DOM as `<br>`. + hard_break: { + inline: true, + group: "inline", + selectable: false, + parseDOM: [{ tag: "br" }], + toDOM() { return brDOM; } + }, + + ordered_list: { + ...orderedList, + content: 'list_item+', + group: 'block' + }, + //this doesn't currently work for some reason + bullet_list: { + ...bulletList, + content: 'list_item+', + group: 'block', + // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }], + // toDOM() { return ulDOM } + }, + //bullet_list: { + // content: 'list_item+', + // group: 'block', + //active: blockActive(schema.nodes.bullet_list), + //enable: wrapInList(schema.nodes.bullet_list), + //run: wrapInList(schema.nodes.bullet_list), + //select: state => true, + // }, + list_item: { + ...listItem, + content: 'paragraph block*' + } }; const emDOM: DOMOutputSpecArray = ["em", 0]; @@ -145,84 +144,186 @@ const underlineDOM: DOMOutputSpecArray = ["underline", 0]; // :: Object [Specs](#model.MarkSpec) for the marks in the schema. export const marks: { [index: string]: MarkSpec } = { - // :: MarkSpec A link. Has `href` and `title` attributes. `title` - // defaults to the empty string. Rendered and parsed as an `<a>` - // element. - link: { - attrs: { - href: {}, - title: { default: null } - }, - inclusive: false, - parseDOM: [{ - tag: "a[href]", getAttrs(dom: any) { - return { href: dom.getAttribute("href"), title: dom.getAttribute("title") }; - } - }], - toDOM(node: any) { return ["a", node.attrs, 0]; } - }, - - // :: MarkSpec An emphasis mark. Rendered as an `<em>` element. - // Has parse rules that also match `<i>` and `font-style: italic`. - em: { - parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }], - toDOM() { return emDOM; } - }, - - // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules - // also match `<b>` and `font-weight: bold`. - strong: { - parseDOM: [{ tag: "strong" }, - { tag: "b" }, - { style: "font-weight" }], - toDOM() { return strongDOM; } - }, - - underline: { - parseDOM: [ - { tag: 'u' }, - { style: 'text-decoration=underline' } - ], - toDOM: () => ['span', { - style: 'text-decoration:underline' - }] - }, - - strikethrough: { - parseDOM: [ - { tag: 'strike' }, - { style: 'text-decoration=line-through' }, - { style: 'text-decoration-line=line-through' } - ], - toDOM: () => ['span', { - style: 'text-decoration-line:line-through' - }] - }, - - subscript: { - excludes: 'superscript', - parseDOM: [ - { tag: 'sub' }, - { style: 'vertical-align=sub' } - ], - toDOM: () => ['sub'] - }, - - superscript: { - excludes: 'subscript', - parseDOM: [ - { tag: 'sup' }, - { style: 'vertical-align=super' } - ], - toDOM: () => ['sup'] - }, - - - // :: MarkSpec Code font mark. Represented as a `<code>` element. - code: { - parseDOM: [{ tag: "code" }], - toDOM() { return codeDOM; } - } + // :: MarkSpec A link. Has `href` and `title` attributes. `title` + // defaults to the empty string. Rendered and parsed as an `<a>` + // element. + link: { + attrs: { + href: {}, + title: { default: null } + }, + inclusive: false, + parseDOM: [{ + tag: "a[href]", getAttrs(dom: any) { + return { href: dom.getAttribute("href"), title: dom.getAttribute("title") }; + } + }], + toDOM(node: any) { return ["a", node.attrs, 0]; } + }, + + // :: MarkSpec An emphasis mark. Rendered as an `<em>` element. + // Has parse rules that also match `<i>` and `font-style: italic`. + em: { + parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }], + toDOM() { return emDOM; } + }, + + // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules + // also match `<b>` and `font-weight: bold`. + strong: { + parseDOM: [{ tag: "strong" }, + { tag: "b" }, + { style: "font-weight" }], + toDOM() { return strongDOM; } + }, + + underline: { + parseDOM: [ + { tag: 'u' }, + { style: 'text-decoration=underline' } + ], + toDOM: () => ['span', { + style: 'text-decoration:underline' + }] + }, + + strikethrough: { + parseDOM: [ + { tag: 'strike' }, + { style: 'text-decoration=line-through' }, + { style: 'text-decoration-line=line-through' } + ], + toDOM: () => ['span', { + style: 'text-decoration-line:line-through' + }] + }, + + subscript: { + excludes: 'superscript', + parseDOM: [ + { tag: 'sub' }, + { style: 'vertical-align=sub' } + ], + toDOM: () => ['sub'] + }, + + superscript: { + excludes: 'subscript', + parseDOM: [ + { tag: 'sup' }, + { style: 'vertical-align=super' } + ], + toDOM: () => ['sup'] + }, + + + // :: MarkSpec Code font mark. Represented as a `<code>` element. + code: { + parseDOM: [{ tag: "code" }], + toDOM() { return codeDOM; } + }, + + + /* FONTS */ + timesNewRoman: { + parseDOM: [{ style: 'font-family: "Times New Roman", Times, serif;' }], + toDOM: () => ['span', { + style: 'font-family: "Times New Roman", Times, serif;' + }] + }, + + arial: { + parseDOM: [{ style: 'font-family: Arial, Helvetica, sans-serif;' }], + toDOM: () => ['span', { + style: 'font-family: Arial, Helvetica, sans-serif;' + }] + }, + + georgia: { + parseDOM: [{ style: 'font-family: Georgia, serif;' }], + toDOM: () => ['span', { + style: 'font-family: Georgia, serif;' + }] + }, + + comicSans: { + parseDOM: [{ style: 'font-family: "Comic Sans MS", cursive, sans-serif;' }], + toDOM: () => ['span', { + style: 'font-family: "Comic Sans MS", cursive, sans-serif;' + }] + }, + + tahoma: { + parseDOM: [{ style: 'font-family: Tahoma, Geneva, sans-serif;' }], + toDOM: () => ['span', { + style: 'font-family: Tahoma, Geneva, sans-serif;' + }] + }, + + impact: { + parseDOM: [{ style: 'font-family: Impact, Charcoal, sans-serif;' }], + toDOM: () => ['span', { + style: 'font-family: Impact, Charcoal, sans-serif;' + }] + }, + + crimson: { + parseDOM: [{ style: 'font-family: "Crimson Text", sans-serif;' }], + toDOM: () => ['span', { + style: 'font-family: "Crimson Text", sans-serif;' + }] + }, + + /** FONT SIZES */ + + p10: { + parseDOM: [{ style: 'font-size: 10px;' }], + toDOM: () => ['span', { + style: 'font-size: 10px;' + }] + }, + + p12: { + parseDOM: [{ style: 'font-size: 12px;' }], + toDOM: () => ['span', { + style: 'font-size: 12px;' + }] + }, + + p16: { + parseDOM: [{ style: 'font-size: 16px;' }], + toDOM: () => ['span', { + style: 'font-size: 16px;' + }] + }, + + p24: { + parseDOM: [{ style: 'font-size: 24px;' }], + toDOM: () => ['span', { + style: 'font-size: 24px;' + }] + }, + + p32: { + parseDOM: [{ style: 'font-size: 32px;' }], + toDOM: () => ['span', { + style: 'font-size: 32px;' + }] + }, + + p48: { + parseDOM: [{ style: 'font-size: 48px;' }], + toDOM: () => ['span', { + style: 'font-size: 48px;' + }] + }, + + p72: { + parseDOM: [{ style: 'font-size: 72px;' }], + toDOM: () => ['span', { + style: 'font-size: 72px;' + }] + }, }; // :: Schema diff --git a/src/client/util/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx new file mode 100644 index 000000000..55e0eb909 --- /dev/null +++ b/src/client/util/TooltipLinkingMenu.tsx @@ -0,0 +1,86 @@ +import { action, IReactionDisposer, reaction } from "mobx"; +import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css +import { baseKeymap, lift } from "prosemirror-commands"; +import { history, redo, undo } from "prosemirror-history"; +import { keymap } from "prosemirror-keymap"; +import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; +import { schema } from "./RichTextSchema"; +import { Schema, NodeType, MarkType } from "prosemirror-model"; +import React = require("react"); +import "./TooltipTextMenu.scss"; +const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list'; +import { + faListUl, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { FieldViewProps } from "../views/nodes/FieldView"; +import { throwStatement } from "babel-types"; + +const SVG = "http://www.w3.org/2000/svg"; + +//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. +export class TooltipLinkingMenu { + + private tooltip: HTMLElement; + private view: EditorView; + private editorProps: FieldViewProps; + + constructor(view: EditorView, editorProps: FieldViewProps) { + this.view = view; + this.editorProps = editorProps; + this.tooltip = document.createElement("div"); + this.tooltip.className = "tooltipMenu linking"; + + //add the div which is the tooltip + view.dom.parentNode!.parentNode!.appendChild(this.tooltip); + + let target = "https://www.google.com"; + + let link = document.createElement("a"); + link.href = target; + link.textContent = target; + link.target = "_blank"; + link.style.color = "white"; + this.tooltip.appendChild(link); + + this.update(view, undefined); + } + + //updates the tooltip menu when the selection changes + update(view: EditorView, lastState: EditorState | undefined) { + let state = view.state; + // Don't do anything if the document/selection didn't change + if (lastState && lastState.doc.eq(state.doc) && + lastState.selection.eq(state.selection)) return; + + // Hide the tooltip if the selection is empty + if (state.selection.empty) { + this.tooltip.style.display = "none"; + return; + } + + console.log("STORED:"); + console.log(state.doc.content.firstChild!.content); + + // Otherwise, reposition it and update its content + this.tooltip.style.display = ""; + let { from, to } = state.selection; + let start = view.coordsAtPos(from), end = view.coordsAtPos(to); + // The box in which the tooltip is positioned, to use as base + let box = this.tooltip.offsetParent!.getBoundingClientRect(); + // Find a center-ish x position from the selection endpoints (when + // crossing lines, end may be more to the left) + let left = Math.max((start.left + end.left) / 2, start.left + 3); + this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale; + let mid = Math.min(start.left, end.left) + width; + + this.tooltip.style.width = "auto"; + this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + } + + destroy() { this.tooltip.remove(); } +} diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index 7deea3be6..5c2d66480 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -1,8 +1,248 @@ @import "../views/globalCssVariables"; +.ProseMirror-textblock-dropdown { + min-width: 3em; + } + + .ProseMirror-menu { + margin: 0 -4px; + line-height: 1; + } + + .ProseMirror-tooltip .ProseMirror-menu { + width: -webkit-fit-content; + width: fit-content; + white-space: pre; + } + + .ProseMirror-menuitem { + margin-right: 3px; + display: inline-block; + } + + .ProseMirror-menuseparator { + // border-right: 1px solid #ddd; + margin-right: 3px; + } + + .ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu { + font-size: 90%; + white-space: nowrap; + } + + .ProseMirror-menu-dropdown { + vertical-align: 1px; + cursor: pointer; + position: relative; + padding-right: 15px; + } + + .ProseMirror-menu-dropdown-wrap { + padding: 1px 0 1px 4px; + display: inline-block; + position: relative; + } + + .ProseMirror-menu-dropdown:after { + content: ""; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid currentColor; + opacity: .6; + position: absolute; + right: 4px; + top: calc(50% - 2px); + } + + .ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { + position: absolute; + background: $dark-color; + color:white; + border: 1px solid rgb(223, 223, 223); + padding: 2px; + } + + .ProseMirror-menu-dropdown-menu { + z-index: 15; + min-width: 6em; + } + + .linking { + text-align: center; + } + + .ProseMirror-menu-dropdown-item { + cursor: pointer; + padding: 2px 8px 2px 4px; + width: auto; + } + + .ProseMirror-menu-dropdown-item:hover { + background: #2e2b2b; + } + + .ProseMirror-menu-submenu-wrap { + position: relative; + margin-right: -4px; + } + + .ProseMirror-menu-submenu-label:after { + content: ""; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 4px solid currentColor; + opacity: .6; + position: absolute; + right: 4px; + top: calc(50% - 4px); + } + + .ProseMirror-menu-submenu { + display: none; + min-width: 4em; + left: 100%; + top: -3px; + } + + .ProseMirror-menu-active { + background: #eee; + border-radius: 4px; + } + + .ProseMirror-menu-active { + background: #eee; + border-radius: 4px; + } + + .ProseMirror-menu-disabled { + opacity: .3; + } + + .ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu { + display: block; + } + + .ProseMirror-menubar { + border-top-left-radius: inherit; + border-top-right-radius: inherit; + position: relative; + min-height: 1em; + color: white; + padding: 1px 6px; + top: 0; left: 0; right: 0; + border-bottom: 1px solid silver; + background:$dark-color; + z-index: 10; + -moz-box-sizing: border-box; + box-sizing: border-box; + overflow: visible; + } + + .ProseMirror-icon { + display: inline-block; + line-height: .8; + vertical-align: -2px; /* Compensate for padding */ + padding: 2px 8px; + cursor: pointer; + } + + .ProseMirror-menu-disabled.ProseMirror-icon { + cursor: default; + } + + .ProseMirror-icon svg { + fill: currentColor; + height: 1em; + } + + .ProseMirror-icon span { + vertical-align: text-top; + } +.ProseMirror-example-setup-style hr { + padding: 2px 10px; + border: none; + margin: 1em 0; + } + + .ProseMirror-example-setup-style hr:after { + content: ""; + display: block; + height: 1px; + background-color: silver; + line-height: 2px; + } + + .ProseMirror ul, .ProseMirror ol { + padding-left: 30px; + } + + .ProseMirror blockquote { + padding-left: 1em; + border-left: 3px solid #eee; + margin-left: 0; margin-right: 0; + } + + .ProseMirror-example-setup-style img { + cursor: default; + } + + .ProseMirror-prompt { + background: white; + padding: 5px 10px 5px 15px; + border: 1px solid silver; + position: fixed; + border-radius: 3px; + z-index: 11; + box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2); + } + + .ProseMirror-prompt h5 { + margin: 0; + font-weight: normal; + font-size: 100%; + color: #444; + } + + .ProseMirror-prompt input[type="text"], + .ProseMirror-prompt textarea { + background: #eee; + border: none; + outline: none; + } + + .ProseMirror-prompt input[type="text"] { + padding: 0 4px; + } + + .ProseMirror-prompt-close { + position: absolute; + left: 2px; top: 1px; + color: #666; + border: none; background: transparent; padding: 0; + } + + .ProseMirror-prompt-close:after { + content: "✕"; + font-size: 12px; + } + + .ProseMirror-invalid { + background: #ffc; + border: 1px solid #cc7; + border-radius: 4px; + padding: 5px 10px; + position: absolute; + min-width: 10em; + } + + .ProseMirror-prompt-buttons { + margin-top: 5px; + display: none; + } + .tooltipMenu { position: absolute; - z-index: 20; + z-index: 200; background: $dark-color; border: 1px solid silver; border-radius: 4px; @@ -10,6 +250,7 @@ margin-bottom: 7px; -webkit-transform: translateX(-50%); transform: translateX(-50%); + pointer-events: all; } .tooltipMenu:before { @@ -39,7 +280,7 @@ display: inline-block; border-right: 1px solid rgba(0, 0, 0, 0.2); //color: rgb(19, 18, 18); - color: $light-color; + color: white; line-height: 1; padding: 0px 2px; margin: 1px; @@ -52,4 +293,8 @@ .underline {text-decoration: underline} .superscript {vertical-align:super} .subscript { vertical-align:sub } - .strikethrough {text-decoration-line:line-through}
\ No newline at end of file + .strikethrough {text-decoration-line:line-through} + .font-size-indicator { + font-size: 12px; + padding-right: 0px; + } diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 7951e5686..a92cbd263 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -1,28 +1,43 @@ import { action, IReactionDisposer, reaction } from "mobx"; -import { baseKeymap } from "prosemirror-commands"; +import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css +import { baseKeymap, lift } from "prosemirror-commands"; import { history, redo, undo } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; -import { EditorState, Transaction, NodeSelection } from "prosemirror-state"; +import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { schema } from "./RichTextSchema"; -import { Schema, NodeType } from "prosemirror-model"; +import { Schema, NodeType, MarkType } from "prosemirror-model"; import React = require("react"); import "./TooltipTextMenu.scss"; const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); import { library } from '@fortawesome/fontawesome-svg-core'; -import { wrapInList, bulletList } from 'prosemirror-schema-list'; -import { faListUl } from '@fortawesome/free-solid-svg-icons'; +import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list'; +import { + faListUl, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FieldViewProps } from "../views/nodes/FieldView"; import { throwStatement } from "babel-types"; +const SVG = "http://www.w3.org/2000/svg"; //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { private tooltip: HTMLElement; + private num_icons = 0; + private view: EditorView; + private fontStyles: MarkType[]; + private fontSizes: MarkType[]; private editorProps: FieldViewProps; + private state: EditorState; + private fontSizeToNum: Map<MarkType, number>; + private fontStylesToName: Map<MarkType, string>; + private fontSizeIndicator: HTMLSpanElement = document.createElement("span"); constructor(view: EditorView, editorProps: FieldViewProps) { + this.view = view; + this.state = view.state; this.editorProps = editorProps; this.tooltip = document.createElement("div"); this.tooltip.className = "tooltipMenu"; @@ -32,7 +47,6 @@ export class TooltipTextMenu { //add additional icons library.add(faListUl); - //add the buttons to the tooltip let items = [ { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") }, @@ -41,36 +55,145 @@ export class TooltipTextMenu { { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") }, { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") }, { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") }, - //this doesn't work currently - look into notion of active block { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, + { command: lift, dom: this.icon("<", "lift") }, ]; - items.forEach(({ dom }) => this.tooltip.appendChild(dom)); - - //pointer down handler to activate button effects - this.tooltip.addEventListener("pointerdown", e => { - e.preventDefault(); - view.focus(); - items.forEach(({ command, dom }) => { - if (dom.contains(e.target as Node)) { - let active = command(view.state, view.dispatch, view); - //uncomment this if we want the bullet button to disappear if current selection is bulleted - // dom.style.display = active ? "" : "none" - } + //add menu items + items.forEach(({ dom, command }) => { + this.tooltip.appendChild(dom); + + //pointer down handler to activate button effects + dom.addEventListener("pointerdown", e => { + e.preventDefault(); + view.focus(); + command(view.state, view.dispatch, view); }); + }); + //list of font styles + this.fontStylesToName = new Map(); + this.fontStylesToName.set(schema.marks.timesNewRoman, "Times New Roman"); + this.fontStylesToName.set(schema.marks.arial, "Arial"); + this.fontStylesToName.set(schema.marks.georgia, "Georgia"); + this.fontStylesToName.set(schema.marks.comicSans, "Comic Sans"); + this.fontStylesToName.set(schema.marks.tahoma, "Tahoma"); + this.fontStylesToName.set(schema.marks.impact, "Impact"); + this.fontStylesToName.set(schema.marks.crimson, "Crimson Text"); + this.fontStyles = Array.from(this.fontStylesToName.keys()); + + //font sizes + this.fontSizeToNum = new Map(); + this.fontSizeToNum.set(schema.marks.p10, 10); + this.fontSizeToNum.set(schema.marks.p12, 12); + this.fontSizeToNum.set(schema.marks.p16, 16); + this.fontSizeToNum.set(schema.marks.p24, 24); + this.fontSizeToNum.set(schema.marks.p32, 32); + this.fontSizeToNum.set(schema.marks.p48, 48); + this.fontSizeToNum.set(schema.marks.p72, 72); + this.fontSizes = Array.from(this.fontSizeToNum.keys()); + + this.addFontDropdowns(); + this.update(view, undefined); } + //adds font size and font style dropdowns + addFontDropdowns() { + //filtering function - might be unecessary + let cut = (arr: MenuItem[]) => arr.filter(x => x); + //font STYLES + let fontBtns: MenuItem[] = []; + this.fontStylesToName.forEach((name, mark) => { + fontBtns.push(this.dropdownBtn(name, "font-family: " + name + ", sans-serif; width: 120px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles)); + }); + + //font size indicator + this.fontSizeIndicator = this.icon("12", "font-size-indicator"); + + //font SIZES + let fontSizeBtns: MenuItem[] = []; + this.fontSizeToNum.forEach((number, mark) => { + fontSizeBtns.push(this.dropdownBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes)); + }); + + //dropdown to hold font btns + let dd_fontStyle = new Dropdown(cut(fontBtns), { label: "Font Style", css: "color:white;" }) as MenuItem; + let dd_fontSize = new Dropdown(cut(fontSizeBtns), { label: "Font Size", css: "color:white; margin-left: -6px;" }) as MenuItem; + this.tooltip.appendChild(dd_fontStyle.render(this.view).dom); + this.tooltip.appendChild(this.fontSizeIndicator); + this.tooltip.appendChild(dd_fontSize.render(this.view).dom); + dd_fontStyle.render(this.view).dom.nodeValue = "TEST"; + console.log(dd_fontStyle.render(this.view).dom.nodeValue); + } + + //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text + changeToMarkInGroup(markType: MarkType, view: EditorView, fontMarks: MarkType[]) { + let { empty, $cursor, ranges } = view.state.selection as TextSelection; + let state = view.state; + let dispatch = view.dispatch; + + //remove all other active font marks + fontMarks.forEach((type) => { + if (dispatch) { + if ($cursor) { + if (type.isInSet(state.storedMarks || $cursor.marks())) { + dispatch(state.tr.removeStoredMark(type)); + } + } else { + let has = false, tr = state.tr; + for (let i = 0; !has && i < ranges.length; i++) { + let { $from, $to } = ranges[i]; + has = state.doc.rangeHasMark($from.pos, $to.pos, type); + } + for (let i of ranges) { + let { $from, $to } = i; + if (has) { + toggleMark(type)(view.state, view.dispatch, view); + } + } + } + } + }); //actually apply font + return toggleMark(markType)(view.state, view.dispatch, view); + } + + //makes a button for the drop down + //css is the style you want applied to the button + dropdownBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) { + return new MenuItem({ + title: "", + label: label, + execEvent: "", + class: "menuicon", + css: css, + enable(state) { return true; }, + run() { + changeToMarkInGroup(markType, view, groupMarks); + } + }); + } // Helper function to create menu icons icon(text: string, name: string) { let span = document.createElement("span"); - span.className = "menuicon " + name; + span.className = name + " menuicon"; span.title = name; span.textContent = text; + span.style.color = "white"; return span; } + //method for checking whether node can be inserted + canInsert(state: EditorState, nodeType: NodeType<Schema<string, string>>) { + let $from = state.selection.$from; + for (let d = $from.depth; d >= 0; d--) { + let index = $from.index(d); + if ($from.node(d).canReplaceWith(index, index, nodeType)) return true; + } + return false; + } + + //adapted this method - use it to check if block has a tag (ie bulleting) blockActive(type: NodeType<Schema<string, string>>, state: EditorState) { let attrs = {}; @@ -89,15 +212,6 @@ export class TooltipTextMenu { } } - //this doesn't currently work but could be used to use icons for buttons - unorderedListIcon(): HTMLSpanElement { - let span = document.createElement("span"); - let icon = document.createElement("FontAwesomeIcon"); - icon.className = "menuicon fa fa-smile-o"; - span.appendChild(icon); - return span; - } - // Create an icon for a heading at the given level heading(level: number) { return { @@ -132,10 +246,52 @@ export class TooltipTextMenu { let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale; let mid = Math.min(start.left, end.left) + width; - //THIS WIDTH IS 15 * NUMBER OF ICONS + 15 - this.tooltip.style.width = 122 + "px"; + this.tooltip.style.width = 220 + "px"; this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + + let activeStyles = this.activeMarksOnSelection(this.fontStyles); + if (activeStyles.length === 1) { + // if we want to update something somewhere with active font name + let fontName = this.fontStylesToName.get(activeStyles[0]); + } else if (activeStyles.length === 0) { + //crimson on default + } + + //update font size indicator + let activeSizes = this.activeMarksOnSelection(this.fontSizes); + if (activeSizes.length === 1) { //if there's only one active font size + let size = this.fontSizeToNum.get(activeSizes[0]); + if (size) { + this.fontSizeIndicator.innerHTML = String(size); + } + //should be 14 on default + } else if (activeSizes.length === 0) { + this.fontSizeIndicator.innerHTML = "14"; + //multiple font sizes selected + } else { + this.fontSizeIndicator.innerHTML = ""; + } + } + + //finds all active marks on selection + activeMarksOnSelection(markGroup: MarkType[]) { + //current selection + let { empty, $cursor, ranges } = this.view.state.selection as TextSelection; + let state = this.view.state; + let dispatch = this.view.dispatch; + + let activeMarks = markGroup.filter(mark => { + if (dispatch) { + let has = false, tr = state.tr; + for (let i = 0; !has && i < ranges.length; i++) { + let { $from, $to } = ranges[i]; + return state.doc.rangeHasMark($from.pos, $to.pos, mark); + } + } + return false; + }); + return activeMarks; } destroy() { this.tooltip.remove(); } -}
\ No newline at end of file +} diff --git a/src/client/views/_global_variables.ts b/src/client/views/_global_variables.ts new file mode 100644 index 000000000..10482bc8d --- /dev/null +++ b/src/client/views/_global_variables.ts @@ -0,0 +1,8 @@ +import * as globalStyleVariables from "../views/globalCssVariables.scss" + +export interface I_globalScss { + contextMenuZindex: string; // context menu shows up over everything +} +let globalStyles = globalStyleVariables as any as I_globalScss; + +export default globalStyles;
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 40e49bb5f..c8bfedff4 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -15,6 +15,11 @@ padding: 0px; font-size: 100%; } + +ul { + list-style-type: disc; +} + #schema-options-header { text-align: center; padding: 0px; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 973eead97..8ecc5b67b 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -50,15 +50,15 @@ .docContainer:hover { .delete-button { display: inline; - width: auto; + // width: auto; } } .delete-button { color: $intermediate-color; - float: right; + // float: right; margin-left: 15px; - margin-top: 3px; + // margin-top: 3px; display: inline; } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 26c794e91..392bd514f 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -48,6 +48,7 @@ } .formattedTextBox-cont { background: $light-color-secondary; + overflow: visible; } opacity: 0.99; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 8963484b6..c380ef650 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -7,6 +7,7 @@ import { EditorView } from "prosemirror-view"; import { FieldWaiting, Opt } from "../../../fields/Field"; import { RichTextField } from "../../../fields/RichTextField"; import { inpRules } from "../../util/RichTextRules"; +import { Schema } from "prosemirror-model"; import { schema } from "../../util/RichTextSchema"; import { TooltipTextMenu } from "../../util/TooltipTextMenu"; import { ContextMenu } from "../../views/ContextMenu"; @@ -14,8 +15,11 @@ import { Main } from "../Main"; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import React = require("react"); +import { undoItem } from "prosemirror-menu"; +import buildKeymap from "../../util/ProsemirrorKeymap"; import { TextField } from "../../../fields/TextField"; import { KeyStore } from "../../../fields/KeyStore"; +import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu"; import { MainOverlayTextBox } from "../MainOverlayTextBox"; import { observer } from "mobx-react"; const { buildMenuItems } = require("prosemirror-example-setup"); @@ -83,12 +87,18 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte inpRules, //these currently don't do anything, but could eventually be helpful plugins: this.props.isOverlay ? [ history(), - keymap({ "Mod-z": undo, "Mod-y": redo }), + keymap(buildKeymap(schema)), keymap(baseKeymap), - this.tooltipMenuPlugin() + this.tooltipTextMenuPlugin(), + // this.tooltipLinkingMenuPlugin(), + new Plugin({ + props: { + attributes: { class: "ProseMirror-example-setup-style" } + } + }) ] : [ history(), - keymap({ "Mod-z": undo, "Mod-y": redo }), + keymap(buildKeymap(schema)), keymap(baseKeymap), ] }; @@ -172,13 +182,16 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte } onPointerDown = (e: React.PointerEvent): void => { if (e.button === 1 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { + console.log("first"); e.stopPropagation(); } if (e.button === 2) { + console.log("second"); e.preventDefault(); } } onPointerUp = (e: React.PointerEvent): void => { + console.log("pointer up"); if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { e.stopPropagation(); } @@ -223,7 +236,7 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte } } - tooltipMenuPlugin() { + tooltipTextMenuPlugin() { let myprops = this.props; return new Plugin({ view(_editorView) { @@ -231,8 +244,19 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte } }); } + + tooltipLinkingMenuPlugin() { + let myprops = this.props; + return new Plugin({ + view(_editorView) { + return new TooltipLinkingMenu(_editorView, myprops); + } + }); + } + onKeyPress(e: React.KeyboardEvent) { e.stopPropagation(); + if (e.keyCode === 9) e.preventDefault(); // stop propagation doesn't seem to stop propagation of native keyboard events. // so we set a flag on the native event that marks that the event's been handled. // (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; diff --git a/src/server/public/files/upload_1b4818f39ea324b5a687bb1ade3dca6c.jpg b/src/server/public/files/upload_1b4818f39ea324b5a687bb1ade3dca6c.jpg Binary files differnew file mode 100644 index 000000000..aeb10c4b0 --- /dev/null +++ b/src/server/public/files/upload_1b4818f39ea324b5a687bb1ade3dca6c.jpg diff --git a/src/server/public/files/upload_1f1c6cfef33e5992fa860802e8c466a7.jpg b/src/server/public/files/upload_1f1c6cfef33e5992fa860802e8c466a7.jpg Binary files differnew file mode 100644 index 000000000..a2c1d8a46 --- /dev/null +++ b/src/server/public/files/upload_1f1c6cfef33e5992fa860802e8c466a7.jpg diff --git a/src/server/public/files/upload_2045f363aa9cf281407703ca242aad1a.jpg b/src/server/public/files/upload_2045f363aa9cf281407703ca242aad1a.jpg Binary files differnew file mode 100644 index 000000000..c19b31a38 --- /dev/null +++ b/src/server/public/files/upload_2045f363aa9cf281407703ca242aad1a.jpg diff --git a/src/server/public/files/upload_25bffd90c080c27f5ac822984406b958.jpg b/src/server/public/files/upload_25bffd90c080c27f5ac822984406b958.jpg Binary files differnew file mode 100644 index 000000000..3614b42eb --- /dev/null +++ b/src/server/public/files/upload_25bffd90c080c27f5ac822984406b958.jpg diff --git a/src/server/public/files/upload_261f11dc39ad568212b5c7e39d1e6d13.jpg b/src/server/public/files/upload_261f11dc39ad568212b5c7e39d1e6d13.jpg Binary files differnew file mode 100644 index 000000000..ecd12d9cb --- /dev/null +++ b/src/server/public/files/upload_261f11dc39ad568212b5c7e39d1e6d13.jpg diff --git a/src/server/public/files/upload_26bcc62639141ba64e603daebb5bf5d3.png b/src/server/public/files/upload_26bcc62639141ba64e603daebb5bf5d3.png Binary files differnew file mode 100644 index 000000000..e2297cb3c --- /dev/null +++ b/src/server/public/files/upload_26bcc62639141ba64e603daebb5bf5d3.png diff --git a/src/server/public/files/upload_2d77d0773612e4723b78118ac50a2929.jpg b/src/server/public/files/upload_2d77d0773612e4723b78118ac50a2929.jpg Binary files differnew file mode 100644 index 000000000..261a0ceff --- /dev/null +++ b/src/server/public/files/upload_2d77d0773612e4723b78118ac50a2929.jpg diff --git a/src/server/public/files/upload_2de9ad4dc687c53760c39f724c9a08a5.jpg b/src/server/public/files/upload_2de9ad4dc687c53760c39f724c9a08a5.jpg Binary files differnew file mode 100644 index 000000000..6b6ec3c3f --- /dev/null +++ b/src/server/public/files/upload_2de9ad4dc687c53760c39f724c9a08a5.jpg diff --git a/src/server/public/files/upload_4abb568aa7cce9d291532c3d0da97102.jpg b/src/server/public/files/upload_4abb568aa7cce9d291532c3d0da97102.jpg Binary files differnew file mode 100644 index 000000000..f6332670c --- /dev/null +++ b/src/server/public/files/upload_4abb568aa7cce9d291532c3d0da97102.jpg diff --git a/src/server/public/files/upload_54c34aaca5a7bf510cebad461ec39512.png b/src/server/public/files/upload_54c34aaca5a7bf510cebad461ec39512.png Binary files differnew file mode 100644 index 000000000..e2297cb3c --- /dev/null +++ b/src/server/public/files/upload_54c34aaca5a7bf510cebad461ec39512.png diff --git a/src/server/public/files/upload_562b1e527300df8b350eeab094b3e1f1.jpg b/src/server/public/files/upload_562b1e527300df8b350eeab094b3e1f1.jpg Binary files differnew file mode 100644 index 000000000..db40705dd --- /dev/null +++ b/src/server/public/files/upload_562b1e527300df8b350eeab094b3e1f1.jpg diff --git a/src/server/public/files/upload_6a26d3f7008a8c79ee5fc8054ba69996.jpg b/src/server/public/files/upload_6a26d3f7008a8c79ee5fc8054ba69996.jpg Binary files differnew file mode 100644 index 000000000..f0417a752 --- /dev/null +++ b/src/server/public/files/upload_6a26d3f7008a8c79ee5fc8054ba69996.jpg diff --git a/src/server/public/files/upload_70fa5e0c3f393504349d5865e28f4cac.jpg b/src/server/public/files/upload_70fa5e0c3f393504349d5865e28f4cac.jpg Binary files differnew file mode 100644 index 000000000..395f8ec21 --- /dev/null +++ b/src/server/public/files/upload_70fa5e0c3f393504349d5865e28f4cac.jpg diff --git a/src/server/public/files/upload_8155b5b0f57da107bb07083c04e78943.jpg b/src/server/public/files/upload_8155b5b0f57da107bb07083c04e78943.jpg Binary files differnew file mode 100644 index 000000000..53d9315a9 --- /dev/null +++ b/src/server/public/files/upload_8155b5b0f57da107bb07083c04e78943.jpg diff --git a/src/server/public/files/upload_88f588574e0efc415186af935114af9a.jpg b/src/server/public/files/upload_88f588574e0efc415186af935114af9a.jpg Binary files differnew file mode 100644 index 000000000..b72dbc482 --- /dev/null +++ b/src/server/public/files/upload_88f588574e0efc415186af935114af9a.jpg diff --git a/src/server/public/files/upload_8d1c253f93f77c69c0c04ae3efb7d714.png b/src/server/public/files/upload_8d1c253f93f77c69c0c04ae3efb7d714.png Binary files differnew file mode 100644 index 000000000..e2297cb3c --- /dev/null +++ b/src/server/public/files/upload_8d1c253f93f77c69c0c04ae3efb7d714.png diff --git a/src/server/public/files/upload_9ef80158609f5ff739087aecad367b9d.jpg b/src/server/public/files/upload_9ef80158609f5ff739087aecad367b9d.jpg Binary files differnew file mode 100644 index 000000000..84423538c --- /dev/null +++ b/src/server/public/files/upload_9ef80158609f5ff739087aecad367b9d.jpg diff --git a/src/server/public/files/upload_c39a7e0d7e8d35bb18461a5a0aa063bf.jpg b/src/server/public/files/upload_c39a7e0d7e8d35bb18461a5a0aa063bf.jpg Binary files differnew file mode 100644 index 000000000..dc7ec2f33 --- /dev/null +++ b/src/server/public/files/upload_c39a7e0d7e8d35bb18461a5a0aa063bf.jpg diff --git a/src/server/public/files/upload_c6b81ab4eb70465a7e9b45d5c8f3ecaa.jpg b/src/server/public/files/upload_c6b81ab4eb70465a7e9b45d5c8f3ecaa.jpg Binary files differnew file mode 100644 index 000000000..4422124a1 --- /dev/null +++ b/src/server/public/files/upload_c6b81ab4eb70465a7e9b45d5c8f3ecaa.jpg diff --git a/src/server/public/files/upload_c99ec7a8a2df0b2f90479fde7d70c2eb.jpg b/src/server/public/files/upload_c99ec7a8a2df0b2f90479fde7d70c2eb.jpg Binary files differnew file mode 100644 index 000000000..3747ca985 --- /dev/null +++ b/src/server/public/files/upload_c99ec7a8a2df0b2f90479fde7d70c2eb.jpg diff --git a/src/server/public/files/upload_cec1cfcc67cfe5889de4098a49fec45e.jpg b/src/server/public/files/upload_cec1cfcc67cfe5889de4098a49fec45e.jpg Binary files differnew file mode 100644 index 000000000..95053d772 --- /dev/null +++ b/src/server/public/files/upload_cec1cfcc67cfe5889de4098a49fec45e.jpg diff --git a/src/server/public/files/upload_f27688fe92dc7de398e957e5d96e1a22.jpg b/src/server/public/files/upload_f27688fe92dc7de398e957e5d96e1a22.jpg Binary files differnew file mode 100644 index 000000000..9841bad3f --- /dev/null +++ b/src/server/public/files/upload_f27688fe92dc7de398e957e5d96e1a22.jpg |