aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoranika-ahluwalia <anika.ahluwalia@gmail.com>2020-07-01 13:14:30 -0500
committeranika-ahluwalia <anika.ahluwalia@gmail.com>2020-07-01 13:14:30 -0500
commit92b5c8bc048190ac6c90a6fdf0f04b9f053f412f (patch)
tree95c95eed84fa4018e492f5ec8ad036bffc2772b3
parent581ba904dc9bd02f6e2d81f42c45f38f6ad26cab (diff)
parentf38485f2778c9dbcc4dd663527c58ea450acb832 (diff)
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into anika_linking
-rw-r--r--src/Utils.ts16
-rw-r--r--src/client/documents/Documents.ts32
-rw-r--r--src/client/util/CurrentUserUtils.ts12
-rw-r--r--src/client/util/DocumentManager.ts8
-rw-r--r--src/client/views/AntimodeMenu.tsx9
-rw-r--r--src/client/views/ContextMenu.tsx2
-rw-r--r--src/client/views/DocumentDecorations.tsx92
-rw-r--r--src/client/views/GlobalKeyHandler.ts6
-rw-r--r--src/client/views/MainView.tsx4
-rw-r--r--src/client/views/PreviewCursor.tsx21
-rw-r--r--src/client/views/collections/CollectionSubView.tsx61
-rw-r--r--src/client/views/collections/CollectionView.tsx12
-rw-r--r--src/client/views/nodes/DocumentView.tsx3
-rw-r--r--src/client/views/nodes/FontIconBox.tsx9
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx41
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts24
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx177
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts3
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts2
19 files changed, 343 insertions, 191 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index dba802f98..a01a94134 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -7,6 +7,22 @@ import { ColorState } from 'react-color';
export namespace Utils {
export let DRAG_THRESHOLD = 4;
+ export function readUploadedFileAsText(inputFile: File) {
+ const temporaryFileReader = new FileReader();
+
+ return new Promise((resolve, reject) => {
+ temporaryFileReader.onerror = () => {
+ temporaryFileReader.abort();
+ reject(new DOMException("Problem parsing input file."));
+ };
+
+ temporaryFileReader.onload = () => {
+ resolve(temporaryFileReader.result);
+ };
+ temporaryFileReader.readAsText(inputFile);
+ });
+ }
+
export function GenerateGuid(): string {
return v4();
}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 3d4694bc5..85da621e0 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,5 +1,5 @@
import { runInAction } from "mobx";
-import { extname } from "path";
+import { extname, basename } from "path";
import { DateField } from "../../fields/DateField";
import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc";
import { HtmlField } from "../../fields/HtmlField";
@@ -48,6 +48,8 @@ import { PresElementBox } from "../views/presentationview/PresElementBox";
import { RecommendationsBox } from "../views/RecommendationsBox";
import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
import { DocumentType } from "./DocumentTypes";
+import { Networking } from "../Network";
+import { Upload } from "../../server/SharedMediaTypes";
const path = require('path');
export interface DocumentOptions {
@@ -977,7 +979,7 @@ export namespace DocUtils {
});
ContextMenu.Instance.addItem({
description: "Add Template Doc ...",
- subitems: DocListCast(Cast(Doc.UserDoc().dockedBtns, Doc, null)?.data).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc).map((dragDoc, i) => ({
+ subitems: DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc).map((dragDoc, i) => ({
description: ":" + StrCast(dragDoc.title),
event: (args: { x: number, y: number }) => {
const newDoc = Doc.ApplyTemplate(dragDoc);
@@ -1104,6 +1106,32 @@ export namespace DocUtils {
});
return optionsCollection;
}
+
+ export async function uploadFilesToDocs(files: File[], options: DocumentOptions) {
+ const generatedDocuments: Doc[] = [];
+ for (const { source: { name, type }, result } of await Networking.UploadFilesToServer(files)) {
+ if (result instanceof Error) {
+ alert(`Upload failed: ${result.message}`);
+ return [];
+ }
+ const full = { ...options, _width: 400, title: name };
+ const pathname = Utils.prepend(result.accessPaths.agnostic.client);
+ const doc = await DocUtils.DocumentFromType(type, pathname, full);
+ if (!doc) {
+ continue;
+ }
+ const proto = Doc.GetProto(doc);
+ proto.text = result.rawText;
+ proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "");
+ if (Upload.isImageInformation(result)) {
+ proto["data-nativeWidth"] = (result.nativeWidth > result.nativeHeight) ? 400 * result.nativeWidth / result.nativeHeight : 400;
+ proto["data-nativeHeight"] = (result.nativeWidth > result.nativeHeight) ? 400 : 400 / (result.nativeWidth / result.nativeHeight);
+ proto.contentSize = result.contentSize;
+ }
+ generatedDocuments.push(doc);
+ }
+ return generatedDocuments;
+ }
}
Scripting.addGlobal("Docs", Docs);
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 09e4d2bb1..ef216001c 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -364,16 +364,22 @@ export class CurrentUserUtils {
doc.emptyCollection = Docs.Create.FreeformDocument([],
{ _nativeWidth: undefined, _nativeHeight: undefined, _width: 150, _height: 100, title: "freeform" });
}
+ if (doc.emptyComparison === undefined) {
+ doc.emptyComparison = Docs.Create.ComparisonDocument({ title: "compare", _width: 300, _height: 300 });
+ }
+ if (doc.emptyScript === undefined) {
+ doc.emptyScript = Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250, title: "script" })
+ }
if (doc.emptyDocHolder === undefined) {
doc.emptyDocHolder = Docs.Create.DocumentDocument(
ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]") as any,
{ _width: 250, _height: 250, title: "container" });
}
if (doc.emptyWebpage === undefined) {
- doc.emptyWebpage = Docs.Create.WebDocument("", { title: "New Webpage", _nativeWidth: 850, _nativeHeight: 962, _width: 600, UseCors: true });
+ doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _nativeHeight: 962, _width: 600, UseCors: true });
}
return [
- { title: "Drag a comparison box", label: "Comp", icon: "columns", ignoreClick: true, drag: 'Docs.Create.ComparisonDocument()' },
+ { title: "Drag a comparison box", label: "Comp", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc },
{ title: "Drag a collection", label: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc },
{ title: "Drag a web page", label: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc },
{ title: "Drag a cat image", label: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' },
@@ -383,7 +389,7 @@ export class CurrentUserUtils {
{ title: "Drag a clickable button", label: "Btn", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding:10, _yPadding: 10, title: "Button" })' },
{ title: "Drag a presentation view", label: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory,true)`, dragFactory: doc.emptyPresentation as Doc },
{ title: "Drag a search box", label: "Query", icon: "search", ignoreClick: true, drag: 'Docs.Create.QueryDocument({ _width: 200, title: "an image of a cat" })' },
- { title: "Drag a scripting box", label: "Script", icon: "terminal", ignoreClick: true, drag: 'Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250 title: "untitled script" })' },
+ { title: "Drag a scripting box", label: "Script", icon: "terminal", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScript as Doc },
{ title: "Drag an import folder", label: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
{ title: "Drag a mobile view", label: "Phone", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
{ title: "Drag an instance of the device collection", label: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' },
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index fb5d1717e..1fa5faeb3 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -146,7 +146,7 @@ export class DocumentManager {
};
const docView = getFirstDocView(targetDoc, originatingDoc);
let annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
- if (annotatedDoc) {
+ if (annotatedDoc && !linkDoc?.isPushpin) {
const first = getFirstDocView(annotatedDoc);
if (first) {
annotatedDoc = first.props.Document;
@@ -156,7 +156,11 @@ export class DocumentManager {
}
}
if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight?
- docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish);
+ if (linkDoc?.isPushpin) docView.props.Document.hidden = !docView.props.Document.hidden;
+ else {
+ docView.props.Document.hidden && (docView.props.Document.hidden = undefined);
+ docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish);
+ }
highlight();
} else {
const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 3e4d20fea..a4634103c 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -51,18 +51,15 @@ export default abstract class AntimodeMenu extends React.Component {
if (this._opacity === 0.2) {
this._transitionProperty = "opacity";
this._transitionDuration = "0.1s";
- this._transitionDelay = "";
- this._opacity = 0;
- this._left = this._top = -300;
}
if (forceOut) {
this._transitionProperty = "";
this._transitionDuration = "";
- this._transitionDelay = "";
- this._opacity = 0;
- this._left = this._top = -300;
}
+ this._transitionDelay = "";
+ this._opacity = 0;
+ this._left = this._top = -300;
}
}
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 941d7b44a..07f7b8e6d 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -155,9 +155,11 @@ export class ContextMenu extends React.Component {
@action
closeMenu = () => {
+ const wasOpen = this._display;
this.clearItems();
this._display = false;
this._shouldDisplay = false;
+ return wasOpen;
}
@computed get filteredItems(): (OriginalMenuProps | string[])[] {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4fda10926..a45ef8862 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -600,52 +600,54 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0,
}} onPointerDown={this.onBackgroundDown} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} >
</div>
- <div className="documentDecorations-container" ref={this.setTextBar} style={{
- width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
- height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px",
- left: bounds.x - this._resizeBorderWidth / 2,
- top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
- }}>
- {maximizeIcon}
- {titleArea}
- {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) :
- <div className="documentDecorations-iconifyButton" title={`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`} onPointerDown={this.onIconifyDown}>
- {"_"}
- </div>}
- <div className="documentDecorations-closeButton" title="Open Document in Tab" onPointerDown={this.onMaximizeDown}>
- {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."}
+ {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <>
+ <div className="documentDecorations-container" key="container" ref={this.setTextBar} style={{
+ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px",
+ left: bounds.x - this._resizeBorderWidth / 2,
+ top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
+ }}>
+ {maximizeIcon}
+ {titleArea}
+ {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) :
+ <div className="documentDecorations-iconifyButton" title={`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`} onPointerDown={this.onIconifyDown}>
+ {"_"}
+ </div>}
+ <div className="documentDecorations-closeButton" title="Open Document in Tab" onPointerDown={this.onMaximizeDown}>
+ {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."}
+ </div>
+ <div id="documentDecorations-rotation" className="documentDecorations-rotation"
+ onPointerDown={this.onRotateDown}> ⟲ </div>
+ <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-topResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-leftResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-centerCont"></div>
+ <div id="documentDecorations-rightResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
+ <div id="documentDecorations-levelSelector" className="documentDecorations-selector"
+ title="tap to select containing document" onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}>
+ <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
+ </div>}
+ <div id="documentDecorations-borderRadius" className="documentDecorations-radius"
+ onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div>
+
+ </div >
+ <div className="link-button-container" key="links" style={{ left: bounds.x - this._resizeBorderWidth / 2 + 10, top: bounds.b + this._resizeBorderWidth / 2 }}>
+ <DocumentButtonBar views={SelectionManager.SelectedDocuments} />
</div>
- <div id="documentDecorations-rotation" className="documentDecorations-rotation"
- onPointerDown={this.onRotateDown}> ⟲ </div>
- <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer"
- onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-topResizer" className="documentDecorations-resizer"
- onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer"
- onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-leftResizer" className="documentDecorations-resizer"
- onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-centerCont"></div>
- <div id="documentDecorations-rightResizer" className="documentDecorations-resizer"
- onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer"
- onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer"
- onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer"
- onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
- <div id="documentDecorations-levelSelector" className="documentDecorations-selector"
- title="tap to select containing document" onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}>
- <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
- </div>}
- <div id="documentDecorations-borderRadius" className="documentDecorations-radius"
- onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div>
-
- </div >
- <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2 + 10, top: bounds.b + this._resizeBorderWidth / 2 }}>
- <DocumentButtonBar views={SelectionManager.SelectedDocuments} />
- </div>
+ </>}
</div >
);
}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index e3546dece..c85849adb 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -20,6 +20,7 @@ import { MainView } from "./MainView";
import { DocumentView } from "./nodes/DocumentView";
import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
import PDFMenu from "./pdf/PDFMenu";
+import { ContextMenu } from "./ContextMenu";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -81,16 +82,17 @@ export default class KeyManager {
DocumentLinksButton.StartLink = undefined;
const main = MainView.Instance;
Doc.SetSelectedTool(InkTool.None);
+ var doDeselect = true;
if (main.isPointerDown) {
DragManager.AbortDrag();
} else {
if (CollectionDockingView.Instance.HasFullScreen()) {
CollectionDockingView.Instance.CloseFullScreen();
} else {
- SelectionManager.DeselectAll();
+ doDeselect = !ContextMenu.Instance.closeMenu();
}
}
- SelectionManager.DeselectAll();
+ doDeselect && SelectionManager.DeselectAll();
DictationManager.Controls.stop();
// RecommendationsBox.Instance.closeMenu();
GoogleAuthenticationManager.Instance.cancel();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 200486279..f6db1af66 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -5,7 +5,7 @@ import {
faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt,
faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter,
faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote,
- faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown
+ faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight
} from '@fortawesome/free-solid-svg-icons';
import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -142,7 +142,7 @@ export class MainView extends React.Component {
faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt,
faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter,
faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faTrashAlt, faAngleRight, faBell,
- faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown);
+ faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight);
this.initEventListeners();
this.initAuthenticationRouters();
}
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 1ab99881d..6583589f3 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -3,13 +3,18 @@ import { observer } from 'mobx-react';
import "normalize.css";
import * as React from 'react';
import "./PreviewCursor.scss";
-import { Docs } from '../documents/Documents';
+import { Docs, DocUtils } from '../documents/Documents';
import { Doc } from '../../fields/Doc';
import { Transform } from "../util/Transform";
import { DocServer } from '../DocServer';
-import { undoBatch } from '../util/UndoManager';
+import { undoBatch, UndoManager } from '../util/UndoManager';
import { NumCast } from '../../fields/Types';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import * as rp from 'request-promise';
+import { Utils } from '../../Utils';
+import { Networking } from '../Network';
+import { Upload } from '../../server/SharedMediaTypes';
+import { basename } from 'path';
@observer
export class PreviewCursor extends React.Component<{}> {
@@ -26,7 +31,7 @@ export class PreviewCursor extends React.Component<{}> {
document.addEventListener("paste", this.paste);
}
- paste = (e: ClipboardEvent) => {
+ paste = async (e: ClipboardEvent) => {
if (PreviewCursor.Visible && e.clipboardData) {
const newPoint = PreviewCursor._getTransform().transformPoint(PreviewCursor._clickPoint[0], PreviewCursor._clickPoint[1]);
runInAction(() => PreviewCursor.Visible = false);
@@ -104,6 +109,16 @@ export class PreviewCursor extends React.Component<{}> {
x: newPoint[0],
y: newPoint[1],
})))();
+ } else if (e.clipboardData.items.length) {
+ const batch = UndoManager.StartBatch("collection view drop");
+ const files: File[] = [];
+ for (let i = 0; i < e.clipboardData.items.length; i++) {
+ const file = e.clipboardData.items[i].getAsFile();
+ file && files.push(file);
+ }
+ const generatedDocuments = await DocUtils.uploadFilesToDocs(files, { x: newPoint[0], y: newPoint[1] });
+ generatedDocuments.forEach(PreviewCursor._addDocument);
+ batch.end();
}
}
}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index d107db86b..ecd16d22f 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -225,7 +225,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
const res = addedDocs.length ? this.addDocument(addedDocs) : true;
- added = movedDocs.length ? docDragData.moveDocument(movedDocs, this.props.Document, de.embedKey || !this.props.isAnnotationOverlay ? this.addDocument : returnFalse) : res;
+ added = movedDocs.length ? docDragData.moveDocument(movedDocs, this.props.Document, Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document) || de.embedKey || !this.props.isAnnotationOverlay ? this.addDocument : returnFalse) : res;
} else {
added = this.addDocument(docDragData.droppedDocuments);
}
@@ -238,21 +238,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
}
return false;
}
- readUploadedFileAsText = (inputFile: File) => {
- const temporaryFileReader = new FileReader();
- return new Promise((resolve, reject) => {
- temporaryFileReader.onerror = () => {
- temporaryFileReader.abort();
- reject(new DOMException("Problem parsing input file."));
- };
-
- temporaryFileReader.onload = () => {
- resolve(temporaryFileReader.result);
- };
- temporaryFileReader.readAsText(inputFile);
- });
- }
@undoBatch
@action
protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) {
@@ -271,8 +257,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
e.stopPropagation();
e.preventDefault();
- const { addDocument } = this;
- if (!addDocument) {
+ if (!this.addDocument) {
alert("this.props.addDocument does not exist. Aborting drop operation.");
return;
}
@@ -286,14 +271,14 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
DocServer.GetRefField(docid).then(f => {
if (f instanceof Doc) {
if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
- (f instanceof Doc) && addDocument(f);
+ (f instanceof Doc) && this.addDocument(f);
}
});
} else {
- addDocument(Docs.Create.WebDocument(href, { ...options, title: href }));
+ this.addDocument(Docs.Create.WebDocument(href, { ...options, title: href }));
}
} else if (text) {
- addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 }));
+ this.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 }));
}
return;
}
@@ -313,7 +298,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
if (source.startsWith("http")) {
const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 });
ImageUtils.ExtractExif(doc);
- addDocument(doc);
+ this.addDocument(doc);
}
return;
} else {
@@ -360,7 +345,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
if (text) {
if (text.includes("www.youtube.com/watch") || text.includes("www.youtube.com/embed")) {
const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/").split("&")[0];
- addDocument(Docs.Create.VideoDocument(url, {
+ this.addDocument(Docs.Create.VideoDocument(url, {
...options,
title: url,
_width: 400,
@@ -413,10 +398,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const file = item.getAsFile();
file?.type && files.push(file);
- file?.type === "application/json" && this.readUploadedFileAsText(file).then(result => {
+ file?.type === "application/json" && Utils.readUploadedFileAsText(file).then(result => {
console.log(result);
const json = JSON.parse(result as string);
- addDocument(Docs.Create.TreeDocument(
+ this.addDocument(Docs.Create.TreeDocument(
json["rectangular-puzzle"].crossword.clues[0].clue.map((c: any) => {
const label = Docs.Create.LabelDocument({ title: c["#text"], _width: 120, _height: 20 });
const proto = Doc.GetProto(label);
@@ -428,38 +413,18 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
});
}
}
- for (const { source: { name, type }, result } of await Networking.UploadFilesToServer(files)) {
- if (result instanceof Error) {
- alert(`Upload failed: ${result.message}`);
- return;
- }
- const full = { ...options, _width: 400, title: name };
- const pathname = Utils.prepend(result.accessPaths.agnostic.client);
- const doc = await DocUtils.DocumentFromType(type, pathname, full);
- if (!doc) {
- continue;
- }
- const proto = Doc.GetProto(doc);
- proto.text = result.rawText;
- proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "");
- if (Upload.isImageInformation(result)) {
- proto["data-nativeWidth"] = (result.nativeWidth > result.nativeHeight) ? 400 * result.nativeWidth / result.nativeHeight : 400;
- proto["data-nativeHeight"] = (result.nativeWidth > result.nativeHeight) ? 400 : 400 / (result.nativeWidth / result.nativeHeight);
- proto.contentSize = result.contentSize;
- }
- generatedDocuments.push(doc);
- }
+ generatedDocuments.push(...await DocUtils.uploadFilesToDocs(files, options));
if (generatedDocuments.length) {
const set = generatedDocuments.length > 1 && generatedDocuments.map(d => DocUtils.iconify(d));
if (set) {
- addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!);
+ this.addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!);
} else {
- generatedDocuments.forEach(addDocument);
+ generatedDocuments.forEach(this.addDocument);
}
completed?.();
} else {
if (text && !text.includes("https://")) {
- addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 }));
+ this.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 }));
}
}
batch.end();
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index dab82185a..705fbdd34 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -141,15 +141,17 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
} else {
added.map(doc => {
const context = Cast(doc.context, Doc, null);
- if (context && (context?.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
- const pushpin = Docs.Create.FreeformDocument([], {
- title: "pushpin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null),
- _width: 10, _height: 10, _backgroundColor: "red", isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null)
+ if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG) &&
+ !DocListCast(doc.links).some(d => d.isPushpin)) {
+ const pushpin = Docs.Create.FontIconDocument({
+ icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7",
+ _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null)
});
Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin);
- DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin");
+ const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin");
const first = DocListCast(pushpin.links).find(d => d instanceof Doc);
first && (first.hidden = true);
+ pushpinLink && (Doc.GetProto(pushpinLink).isPushpin = true);
doc.displayTimecode = undefined;
}
doc.context = this.props.Document;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 21b6d8310..09eeaee36 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1169,8 +1169,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
render() {
- if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null);
if (!(this.props.Document instanceof Doc)) return (null);
+ if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null);
+ if (this.props.Document.hidden) return (null);
const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document);
const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null)));
const finalOpacity = this.props.opacity ? this.props.opacity() : opacity;
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index cf0b16c7c..5e8dd2497 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -5,7 +5,7 @@ import { createSchema, makeInterface } from '../../../fields/Schema';
import { DocComponent } from '../DocComponent';
import './FontIconBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
-import { StrCast, Cast } from '../../../fields/Types';
+import { StrCast, Cast, NumCast } from '../../../fields/Types';
import { Utils } from "../../../Utils";
import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';
import { Doc } from '../../../fields/Doc';
@@ -59,13 +59,14 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
render() {
const referenceDoc = (this.layoutDoc.dragFactory instanceof Doc ? this.layoutDoc.dragFactory : this.layoutDoc);
- const referenceLayout = Doc.Layout(referenceDoc);
+ const refLayout = Doc.Layout(referenceDoc);
return <button className="fontIconBox-outerDiv" title={StrCast(this.layoutDoc.title)} ref={this._ref} onContextMenu={this.specificContextMenu}
style={{
- background: StrCast(referenceLayout.backgroundColor),
+ padding: Cast(this.layoutDoc._xPadding, "number", null),
+ background: StrCast(refLayout._backgroundColor, StrCast(refLayout.backgroundColor)),
boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined
}}>
- <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={this._foregroundColor} size="sm" />
+ <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={StrCast(this.layoutDoc.color, this._foregroundColor)} size="sm" />
{!this.rootDoc.label ? (null) : <div className="fontIconBox-label"> {StrCast(this.rootDoc.label).substring(0, 5)} </div>}
</button>;
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 2cb55e0fa..2a79e2f13 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -32,7 +32,7 @@ import { DocumentType } from '../../../documents/DocumentTypes';
import { DictationManager } from '../../../util/DictationManager';
import { DragManager } from "../../../util/DragManager";
import { makeTemplate } from '../../../util/DropConverter';
-import buildKeymap from "./ProsemirrorExampleTransfer";
+import buildKeymap, { updateBullets } from "./ProsemirrorExampleTransfer";
import RichTextMenu from './RichTextMenu';
import { RichTextRules } from "./RichTextRules";
@@ -86,6 +86,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
public static Instance: FormattedTextBox;
public ProseRef?: HTMLDivElement;
+ public get EditorView() { return this._editorView; }
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef();
private _editorView: Opt<EditorView>;
@@ -93,7 +94,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
private _searchIndex = 0;
private _undoTyping?: UndoManager.Batch;
private _disposers: { [name: string]: IReactionDisposer } = {};
- private dropDisposer?: DragManager.DragDropDisposer;
+ private _dropDisposer?: DragManager.DragDropDisposer;
@computed get _recording() { return this.dataDoc.audioState === "recording"; }
set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; }
@@ -291,8 +292,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
protected createDropTarget = (ele: HTMLDivElement) => {
this.ProseRef = ele;
- this.dropDisposer?.();
- ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc));
+ this._dropDisposer?.();
+ ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc));
}
@undoBatch
@@ -660,8 +661,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
incomingValue => {
if (incomingValue !== undefined && this._editorView && !this._applyingChange) {
const updatedState = JSON.parse(incomingValue);
- this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
- this.tryUpdateHeight();
+ if (JSON.stringify(this._editorView!.state.toJSON()) !== JSON.stringify(updatedState)) {
+ this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
+ this.tryUpdateHeight();
+ }
}
}
);
@@ -1046,6 +1049,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@action
onFocused = (e: React.FocusEvent): void => {
+ console.log("FOUCSS")
FormattedTextBox.FocusedBox = this;
this.tryUpdateHeight();
@@ -1163,18 +1167,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
});
}
- public static HadSelection: boolean = false;
- onBlur = (e: any) => {
- FormattedTextBox.HadSelection = window.getSelection()?.toString() !== "";
- //DictationManager.Controls.stop(false);
+ public startUndoTypingBatch() {
+ this._undoTyping = UndoManager.StartBatch("undoTyping");
+ }
+
+ public endUndoTypingBatch() {
+ const wasUndoing = this._undoTyping;
if (this._undoTyping) {
this._undoTyping.end();
this._undoTyping = undefined;
}
+ return wasUndoing;
+ }
+ public static HadSelection: boolean = false;
+ onBlur = (e: any) => {
+ console.log("BLURRR")
+ FormattedTextBox.HadSelection = window.getSelection()?.toString() !== "";
+ //DictationManager.Controls.stop(false);
+ this.endUndoTypingBatch();
this.doLinkOnDeselect();
// move the richtextmenu offscreen
- if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300);
+ if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide();
}
_lastTimedMark: Mark | undefined = undefined;
@@ -1208,11 +1222,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
// this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
if (!this._undoTyping) {
- this._undoTyping = UndoManager.StartBatch("undoTyping");
+ this.startUndoTypingBatch();
}
}
ondrop = (eve: React.DragEvent) => {
+ this._editorView!.dispatch(updateBullets(this._editorView!.state.tr, this._editorView!.state.schema));
eve.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash.
}
onscrolled = (ev: React.UIEvent) => {
@@ -1308,7 +1323,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
onScroll={this.onscrolled} onDrop={this.ondrop} >
<div className={`formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget}
style={{
- padding: this.layoutDoc._textBoxPadding ? this.layoutDoc._textBoxPadding : `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`,
+ padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`,
pointerEvents: !this.props.isSelected() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : "all") : undefined
}}
/>
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 1bbcb9fa8..9d69f4be7 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -1,7 +1,6 @@
import { chainCommands, exitCode, joinDown, joinUp, lift, deleteSelection, joinBackward, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn, newlineInCode } from "prosemirror-commands";
import { liftTarget } from "prosemirror-transform";
import { redo, undo } from "prosemirror-history";
-import { undoInputRule } from "prosemirror-inputrules";
import { Schema } from "prosemirror-model";
import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
import { splitListItem, wrapInList, } from "prosemirror-schema-list";
@@ -12,7 +11,6 @@ import { Doc, DataSym } from "../../../../fields/Doc";
import { FormattedTextBox } from "./FormattedTextBox";
import { Id } from "../../../../fields/FieldSymbols";
import { Docs } from "../../../documents/Documents";
-import { update } from "lodash";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
@@ -215,10 +213,13 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any
marks && tx3.setStoredMarks([...marks]);
dispatch(tx3);
})) {
+ const fromattrs = state.selection.$from.node().attrs;
if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
- splitMetadata(marks, tx3);
- if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
- dispatch(tx3);
+ const tonode = tx3.selection.$to.node();
+ const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks);
+ splitMetadata(marks, tx4);
+ if (!liftListItem(schema.nodes.list_item)(tx4, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
+ dispatch(tx4);
}
})) {
return false;
@@ -281,19 +282,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any
return false;
});
- // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- // let newNode = schema.nodes.footnote.create({});
- // if (dispatch && state.selection.from === state.selection.to) {
- // let tr = state.tr;
- // tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
- // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display
- // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
- // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))));
- // return true;
- // }
- // return false;
- // });
-
return keys;
}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 839943aac..6fdcaceda 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -1,8 +1,8 @@
import React = require("react");
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faBold, faCaretDown, faChevronLeft, faEyeDropper, faHighlighter, faIndent, faItalic, faLink, faPaintRoller, faPalette, faStrikethrough, faSubscript, faSuperscript, faUnderline } from "@fortawesome/free-solid-svg-icons";
+import { faBold, faCaretDown, faChevronLeft, faEyeDropper, faHighlighter, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faItalic, faLink, faPaintRoller, faPalette, faStrikethrough, faSubscript, faSuperscript, faUnderline } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable } from "mobx";
+import { action, observable, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
import { lift, wrapIn } from "prosemirror-commands";
import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model";
@@ -23,9 +23,10 @@ import { updateBullets } from "./ProsemirrorExampleTransfer";
import "./RichTextMenu.scss";
import { schema } from "./schema_rts";
import { TraceMobx } from "../../../../fields/util";
+import { UndoManager } from "../../../util/UndoManager";
const { toggleMark } = require("prosemirror-commands");
-library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
+library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
@observer
@@ -68,6 +69,8 @@ export default class RichTextMenu extends AntimodeMenu {
@observable private currentLink: string | undefined = "";
@observable private showLinkDropdown: boolean = false;
+ _reaction: IReactionDisposer | undefined;
+ _delayHide = false;
constructor(props: Readonly<{}>) {
super(props);
RichTextMenu.Instance = this;
@@ -138,6 +141,16 @@ export default class RichTextMenu extends AntimodeMenu {
];
}
+ componentDidMount() {
+ this._reaction = reaction(() => SelectionManager.SelectedDocuments(),
+ () => this._delayHide && !(this._delayHide = false) && this.fadeOut(true));
+ }
+ componentWillUnmount() {
+ this._reaction?.();
+ }
+
+ public delayHide = () => { this._delayHide = true; }
+
@action
changeView(view: EditorView) {
this.view = view;
@@ -147,16 +160,6 @@ export default class RichTextMenu extends AntimodeMenu {
this.updateFromDash(view, lastState, this.editorProps);
}
- public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => {
- if (this.view) {
- const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId });
- this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
- addMark(this.view.state.selection.from, this.view.state.selection.to, link));
- return this.view.state.selection.$from.nodeAfter?.text || "";
- }
- return "";
- }
-
@action
public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
if (!view) {
@@ -310,8 +313,11 @@ export default class RichTextMenu extends AntimodeMenu {
function onClick(e: React.PointerEvent) {
e.preventDefault();
e.stopPropagation();
- self.view && command && command(self.view.state, self.view.dispatch, self.view);
- self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view);
+ self.TextView.endUndoTypingBatch();
+ UndoManager.RunInBatch(() => {
+ self.view && command && command(self.view.state, self.view.dispatch, self.view);
+ self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view);
+ }, "rich text menu command");
self.setActiveMarkButtons(self.getActiveMarksOnSelection());
}
@@ -338,9 +344,10 @@ export default class RichTextMenu extends AntimodeMenu {
function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
e.stopPropagation();
e.preventDefault();
+ self.TextView.endUndoTypingBatch();
options.forEach(({ label, mark, command }) => {
if (e.target.value === label) {
- self.view && mark && command(mark, self.view);
+ UndoManager.RunInBatch(() => self.view && mark && command(mark, self.view), "text mark dropdown");
}
});
}
@@ -361,9 +368,10 @@ export default class RichTextMenu extends AntimodeMenu {
const self = this;
function onChange(val: string) {
+ self.TextView.endUndoTypingBatch();
options.forEach(({ label, node, command }) => {
if (val === label) {
- self.view && node && command(node);
+ UndoManager.RunInBatch(() => self.view && node && command(node), "nodes dropdown");
}
});
}
@@ -412,6 +420,85 @@ export default class RichTextMenu extends AntimodeMenu {
dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
return true;
}
+ alignCenter = (state: EditorState<any>, dispatch: any) => {
+ return this.alignParagraphs(state, "center", dispatch);
+ }
+ alignLeft = (state: EditorState<any>, dispatch: any) => {
+ return this.alignParagraphs(state, "left", dispatch);
+ }
+ alignRight = (state: EditorState<any>, dispatch: any) => {
+ return this.alignParagraphs(state, "right", dispatch);
+ }
+
+ alignParagraphs(state: EditorState<any>, align: "left" | "right" | "center", dispatch: any) {
+ var tr = state.tr;
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
+ if (node.type === schema.nodes.paragraph) {
+ tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align }, node.marks);
+ return false;
+ }
+ return true;
+ });
+ dispatch?.(tr);
+ return true;
+ }
+
+ insetParagraph(state: EditorState<any>, dispatch: any) {
+ var tr = state.tr;
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
+ if (node.type === schema.nodes.paragraph) {
+ const inset = (node.attrs.inset ? Number(node.attrs.inset) : 0) + 10;
+ tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks);
+ return false;
+ }
+ return true;
+ });
+ dispatch?.(tr);
+ return true;
+ }
+ outsetParagraph(state: EditorState<any>, dispatch: any) {
+ var tr = state.tr;
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
+ if (node.type === schema.nodes.paragraph) {
+ const inset = Math.max(0, (node.attrs.inset ? Number(node.attrs.inset) : 0) - 10);
+ tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks);
+ return false;
+ }
+ return true;
+ });
+ dispatch?.(tr);
+ return true;
+ }
+
+ indentParagraph(state: EditorState<any>, dispatch: any) {
+ var tr = state.tr;
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
+ if (node.type === schema.nodes.paragraph) {
+ const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined;
+ const indent = !nodeval ? 25 : nodeval < 0 ? 0 : nodeval + 25;
+ tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks);
+ return false;
+ }
+ return true;
+ });
+ dispatch?.(tr);
+ return true;
+ }
+
+ hangingIndentParagraph(state: EditorState<any>, dispatch: any) {
+ var tr = state.tr;
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
+ if (node.type === schema.nodes.paragraph) {
+ const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined;
+ const indent = !nodeval ? -25 : nodeval > 0 ? 0 : nodeval - 10;
+ tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks);
+ return false;
+ }
+ return true;
+ });
+ dispatch?.(tr);
+ return true;
+ }
insertBlockquote(state: EditorState<any>, dispatch: any) {
const path = (state.selection.$from as any).path;
@@ -423,6 +510,11 @@ export default class RichTextMenu extends AntimodeMenu {
return true;
}
+ insertHorizontalRule(state: EditorState<any>, dispatch: any) {
+ dispatch(state.tr.replaceSelectionWith(state.schema.nodes.horizontal_rule.create()).scrollIntoView());
+ return true;
+ }
+
@action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
// todo: add brushes to brushMap to save with a style name
@@ -439,7 +531,8 @@ export default class RichTextMenu extends AntimodeMenu {
function onBrushClick(e: React.PointerEvent) {
e.preventDefault();
e.stopPropagation();
- self.view && self.fillBrush(self.view.state, self.view.dispatch);
+ self.TextView.endUndoTypingBatch();
+ UndoManager.RunInBatch(() => self.view && self.fillBrush(self.view.state, self.view.dispatch), "rt brush");
}
let label = "Stored marks: ";
@@ -506,19 +599,24 @@ export default class RichTextMenu extends AntimodeMenu {
@action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; }
@action setActiveColor(color: string) { this.activeFontColor = color; }
+ get TextView() { return (this.view as any).TextView as FormattedTextBox }
createColorButton() {
const self = this;
function onColorClick(e: React.PointerEvent) {
e.preventDefault();
e.stopPropagation();
- self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ self.TextView.endUndoTypingBatch();
+ UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color");
+ self.TextView.EditorView!.focus()
}
function changeColor(e: React.PointerEvent, color: string) {
e.preventDefault();
e.stopPropagation();
self.setActiveColor(color);
- self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ self.TextView.endUndoTypingBatch();
+ UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color");
+ self.TextView.EditorView!.focus()
}
const button =
@@ -563,13 +661,15 @@ export default class RichTextMenu extends AntimodeMenu {
function onHighlightClick(e: React.PointerEvent) {
e.preventDefault();
e.stopPropagation();
- self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ self.TextView.endUndoTypingBatch();
+ UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highligher");
}
function changeHighlight(e: React.PointerEvent, color: string) {
e.preventDefault();
e.stopPropagation();
self.setActiveHighlight(color);
- self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ self.TextView.endUndoTypingBatch();
+ UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highlighter");
}
const button =
@@ -609,7 +709,8 @@ export default class RichTextMenu extends AntimodeMenu {
const self = this;
function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) {
- self.setCurrentLink(e.target.value);
+ self.TextView.endUndoTypingBatch();
+ UndoManager.RunInBatch(() => self.setCurrentLink(e.target.value), "link change");
}
const link = this.currentLink ? this.currentLink : "";
@@ -744,7 +845,7 @@ export default class RichTextMenu extends AntimodeMenu {
return ref_node;
}
- @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = true; }
+ @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; }
@action onPointerLeave(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; }
@action
@@ -768,18 +869,25 @@ export default class RichTextMenu extends AntimodeMenu {
TraceMobx();
const row1 = <div className="antimodeMenu-row" key="row1" style={{ display: this.collapsed ? "none" : undefined }}>{[
!this.collapsed ? this.getDragger() : (null),
- this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
- this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
- this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
- this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
- this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
- this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
+ !this.Pinned ? (null) : <> {[
+ this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
+ this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
+ this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
+ this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
+ this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
+ this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
+ ]}</>,
this.createColorButton(),
this.createHighlighterButton(),
this.createLinkButton(),
this.createBrushButton(),
- this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer),
- this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote),
+ this.createButton("align-left", "Align Left", undefined, this.alignLeft),
+ this.createButton("align-center", "Align Center", undefined, this.alignCenter),
+ this.createButton("align-right", "Align Right", undefined, this.alignRight),
+ this.createButton("indent", "Inset More", undefined, this.insetParagraph),
+ this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph),
+ this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph),
+ this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph),
]}</div>;
const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2">
@@ -787,7 +895,10 @@ export default class RichTextMenu extends AntimodeMenu {
<div key="row" style={{ display: this.collapsed ? "none" : undefined }}>
{[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"),
this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"),
- this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes")]}
+ this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes"),
+ this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer),
+ this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote),
+ this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule),]}
</div>
<div key="button">
{/* <div key="collapser">
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 6a85e3b7c..612a824fc 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -315,8 +315,6 @@ export class RichTextRules {
return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}),
-
-
// create an inline view of a tag stored under the '#' field
new InputRule(
new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_;\-0-9]*)\s$/),
@@ -374,7 +372,6 @@ export class RichTextRules {
new InputRule(
new RegExp(/%\)/),
(state, match, start, end) => {
-
return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create());
}),
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index afb1f57b7..f83cff9b9 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -312,7 +312,7 @@ export const nodes: { [index: string]: NodeSpec } = {
const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
return node.attrs.visibility ?
["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, 0] :
- ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, "..."];
+ ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, `${node.firstChild?.textContent}...`];
}
},
}; \ No newline at end of file