aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/formattedText
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/formattedText')
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx2
-rw-r--r--src/client/views/nodes/formattedText/FootnoteView.tsx2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss66
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx55
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts78
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts61
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts2
7 files changed, 208 insertions, 58 deletions
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index b31fc01ff..648c579d0 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -181,6 +181,8 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
height: this._height,
position: 'absolute',
display: 'inline-block',
+ left: 0,
+ top: 0,
}}
onPointerLeave={this.onPointerLeave}
onPointerEnter={this.onPointerEnter}
diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx
index 531a60297..cf48e1250 100644
--- a/src/client/views/nodes/formattedText/FootnoteView.tsx
+++ b/src/client/views/nodes/formattedText/FootnoteView.tsx
@@ -83,13 +83,11 @@ export class FootnoteView {
};
toggle = () => {
- console.log('TOGGLE');
if (this.innerView) this.close();
else this.open();
};
close() {
- console.log('CLOSE');
this.innerView?.destroy();
this.innerView = null;
this.dom.textContent = '';
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index fd7fbb333..b5a3c5d84 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -24,6 +24,27 @@ audiotag:hover {
transform: scale(2);
transform-origin: bottom center;
}
+.formattedTextBox {
+ touch-action: none;
+ background: inherit;
+ padding: 0;
+ border-width: 0px;
+ border-radius: inherit;
+ border-color: $medium-gray;
+ box-sizing: border-box;
+ background-color: inherit;
+ border-style: solid;
+ overflow-y: auto;
+ overflow-x: hidden;
+ color: inherit;
+ display: flex;
+ flex-direction: row;
+ transition: opacity 1s;
+ width: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
.formattedTextBox-cont {
touch-action: none;
@@ -51,6 +72,17 @@ audiotag:hover {
position: absolute;
}
}
+.formattedTextBox-alternateButton {
+ align-items: center;
+ flex-direction: column;
+ position: absolute;
+ color: white;
+ background: black;
+ right: 0;
+ bottom: 0;
+ width: 11;
+ height: 11;
+}
.formattedTextBox-outer-selected,
.formattedTextBox-outer {
@@ -193,16 +225,15 @@ audiotag:hover {
}
footnote {
- display: inline-block;
+ display: inline-flex;
+ top: -0.5em;
position: relative;
cursor: pointer;
-
- div {
- padding: 0 !important;
- }
+ height: 1em;
+ width: 0.5em;
}
-footnote::after {
+footnote::before {
content: counter(prosemirror-footnote);
vertical-align: super;
font-size: 75%;
@@ -216,15 +247,14 @@ footnote::after {
.footnote-tooltip {
cursor: auto;
font-size: 75%;
- position: absolute;
- left: -30px;
- top: calc(100% + 10px);
+ position: relative;
background: silver;
- padding: 3px;
border-radius: 2px;
- max-width: 100px;
- min-width: 50px;
- width: max-content;
+ min-width: 100px;
+ top: 2em;
+ height: max-content;
+ left: -1em;
+ padding: 3px;
}
.prosemirror-attribution {
@@ -239,8 +269,7 @@ footnote::after {
border-left-color: transparent;
border-right-color: transparent;
position: absolute;
- top: -5px;
- left: 27px;
+ top: -0.5em;
content: ' ';
height: 0;
width: 0;
@@ -734,8 +763,8 @@ footnote::after {
cursor: auto;
font-size: 75%;
position: absolute;
- left: -30px;
- top: calc(100% + 10px);
+ // left: -30px;
+ // top: calc(100% + 10px);
background: silver;
padding: 3px;
border-radius: 2px;
@@ -756,8 +785,7 @@ footnote::after {
border-left-color: transparent;
border-right-color: transparent;
position: absolute;
- top: -5px;
- left: 27px;
+ top: -0.5em;
content: ' ';
height: 0;
width: 0;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index bbe38cf99..0610d1e45 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,9 +1,9 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
import { isEqual } from 'lodash';
import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { Configuration, OpenAIApi } from 'openai';
import { baseKeymap, selectAll } from 'prosemirror-commands';
import { history } from 'prosemirror-history';
import { inputRules } from 'prosemirror-inputrules';
@@ -68,6 +68,7 @@ import { schema } from './schema_rts';
import { SummaryView } from './SummaryView';
import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
+import { RTFMarkup } from '../../../util/RTFMarkup';
const translateGoogleApi = require('translate-google-api');
export const GoogleRef = 'googleDocId';
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@@ -852,8 +853,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const optionItems = options && 'subitems' in options ? options.subitems : [];
optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
- optionItems.push({ description: !this.Document._singleLine ? 'Make Single Line' : 'Make Multi Line', event: () => (this.layoutDoc._singleLine = !this.layoutDoc._singleLine), icon: !this.Document._singleLine ? 'grip-lines' : 'bars' });
+ optionItems.push({
+ description: !this.Document._singleLine ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns',
+ event: () => (this.layoutDoc._singleLine = !this.layoutDoc._singleLine),
+ icon: !this.Document._singleLine ? 'grip-lines' : 'bars',
+ });
optionItems.push({ description: `${this.Document._autoHeight ? 'Lock' : 'Auto'} Height`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: this.Document._autoHeight ? 'lock' : 'unlock' });
+ optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: 'text' });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
this._downX = this._downY = Number.NaN;
};
@@ -1928,6 +1934,45 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
</div>
);
}
+ @computed get overlayAlternateIcon() {
+ const usePath = this.rootDoc[`${this.props.fieldKey}-usePath`];
+ return (
+ <Tooltip
+ title={
+ <div className="dash-tooltip">
+ toggle between
+ <span style={{ color: usePath === undefined ? 'black' : undefined }}>
+ <em> primary, </em>
+ </span>
+ <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}>
+ <em>alternate, </em>
+ </span>
+ and show
+ <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}>
+ <em> alternate on hover</em>
+ </span>
+ </div>
+ }>
+ <div
+ className="formattedTextBox-alternateButton"
+ onPointerDown={e =>
+ setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.rootDoc[`_${this.props.fieldKey}-usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))
+ }
+ style={{
+ display: this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'flex' : 'none',
+ background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray',
+ color: usePath === undefined ? 'black' : 'white',
+ }}>
+ <FontAwesomeIcon icon="turn-up" size="sm" />
+ </div>
+ </Tooltip>
+ );
+ }
+ @computed get fieldKey() {
+ const usePath = StrCast(this.rootDoc[`${this.props.fieldKey}-usePath`]);
+ return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `-${usePath.replace(':hover', '')}` : '');
+ }
+ @observable _isHovering = false;
render() {
TraceMobx();
const active = this.props.isContentActive() || this.props.isSelected();
@@ -1944,7 +1989,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
return styleFromLayoutString?.height === '0px' ? null : (
<div
- className="formattedTextBox-cont"
+ className="formattedTextBox"
+ onPointerEnter={action(() => (this._isHovering = true))}
+ onPointerLeave={action(() => (this._isHovering = false))}
ref={r =>
r?.addEventListener(
'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
@@ -1966,6 +2013,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
width: `${100 / scale}%`,
height: `${100 / scale}%`,
}),
+ display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined,
transition: 'inherit',
// overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined,
color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
@@ -2017,6 +2065,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
{this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
{this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle}
{this.audioHandle}
+ {this.overlayAlternateIcon}
</div>
</div>
);
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 68b0488a2..4dfe07b24 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -8,6 +8,7 @@ import { AclAugment, AclSelfEdit, Doc } from '../../../../fields/Doc';
import { GetEffectiveAcl } from '../../../../fields/util';
import { Utils } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
+import { RTFMarkup } from '../../../util/RTFMarkup';
import { SelectionManager } from '../../../util/SelectionManager';
import { OpenWhere } from '../DocumentView';
import { liftListItem, sinkListItem } from './prosemirrorPatches.js';
@@ -178,6 +179,83 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1))));
return true;
});
+ bind('Cmd-?', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ RTFMarkup.Instance.open();
+ return true;
+ });
+ bind('Cmd-e', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ if (!state.selection.empty) {
+ const mark = state.schema.marks.summarizeInclusive.create();
+ const tr = state.tr.addMark(state.selection.$from.pos, state.selection.$to.pos, mark);
+ const content = tr.selection.content();
+ tr.selection.replaceWith(tr, schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }));
+ dispatch(tr);
+ }
+ return true;
+ });
+ bind('Cmd-]', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const resolved = state.doc.resolve(state.selection.from) as any;
+ const tr = state.tr;
+ if (resolved?.parent.type.name === 'paragraph') {
+ tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'right' }, resolved.parent.marks);
+ } else {
+ const node = resolved.nodeAfter;
+ const sm = state.storedMarks || undefined;
+ if (node) {
+ tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+ }
+ }
+ dispatch(tr);
+ return true;
+ });
+ bind('Cmd-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const resolved = state.doc.resolve(state.selection.from) as any;
+ const tr = state.tr;
+ if (resolved?.parent.type.name === 'paragraph') {
+ tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'center' }, resolved.parent.marks);
+ } else {
+ const node = resolved.nodeAfter;
+ const sm = state.storedMarks || undefined;
+ if (node) {
+ tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+ }
+ }
+ dispatch(tr);
+ return true;
+ });
+ bind('Cmd-[', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const resolved = state.doc.resolve(state.selection.from) as any;
+ const tr = state.tr;
+ if (resolved?.parent.type.name === 'paragraph') {
+ tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'left' }, resolved.parent.marks);
+ } else {
+ const node = resolved.nodeAfter;
+ const sm = state.storedMarks || undefined;
+ if (node) {
+ tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+ }
+ }
+ dispatch(tr);
+ return true;
+ });
+
+ bind('Cmd-f', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const content = state.tr.selection.empty ? undefined : state.tr.selection.content().content.textBetween(0, state.tr.selection.content().size + 1);
+ const newNode = schema.nodes.footnote.create({}, content ? state.schema.text(content) : undefined);
+ const 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 || 0)
+ )
+ )
+ )
+ );
+ return true;
+ });
bind('Ctrl-a', (state: EditorState, dispatch: (tx: Transaction) => void) => {
dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1))));
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index e691869cc..cc19d12bd 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -5,7 +5,7 @@ import { Id } from '../../../../fields/FieldSymbols';
import { ComputedField } from '../../../../fields/ScriptField';
import { NumCast, StrCast } from '../../../../fields/Types';
import { normalizeEmail } from '../../../../fields/util';
-import { returnFalse, Utils } from '../../../../Utils';
+import { Utils } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
import { FormattedTextBox } from './FormattedTextBox';
@@ -28,7 +28,7 @@ export class RichTextRules {
emDash,
// > blockquote
- wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote),
+ wrappingInputRule(/%>$/, schema.nodes.blockquote),
// 1. create numerical ordered list
wrappingInputRule(
@@ -190,21 +190,6 @@ export class RichTextRules {
}
}),
- // %f create footnote
- new InputRule(new RegExp(/%f$/), (state, match, start, end) => {
- const newNode = schema.nodes.footnote.create({});
- const tr = state.tr;
- tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
- return 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 || 0)
- )
- )
- );
- }),
-
// activate a style by name using prefix '%<color name>'
new InputRule(new RegExp(/%[a-z]+$/), (state, match, start, end) => {
const color = match[0].substring(1, match[0].length);
@@ -229,6 +214,12 @@ export class RichTextRules {
}),
// stop using active style
+ new InputRule(new RegExp(/%alt$/), (state, match, start, end) => {
+ setTimeout(() => (this.Document[this.TextBox.props.fieldKey + '-usePath'] = this.Document[this.TextBox.props.fieldKey + '-usePath'] ? undefined : 'alternate'));
+ return state.tr.deleteRange(start, end);
+ }),
+
+ // stop using active style
new InputRule(new RegExp(/%%$/), (state, match, start, end) => {
const tr = state.tr.deleteRange(start, end);
const marks = state.tr.selection.$anchor.nodeBefore?.marks;
@@ -250,22 +241,26 @@ export class RichTextRules {
const fieldKey = match[1];
const docId = match[3]?.replace(':', '');
const value = match[2]?.substring(1);
+ const linkToDoc = (target: Doc) => {
+ const rstate = this.TextBox.EditorView?.state;
+ const selection = rstate?.selection.$from.pos;
+ if (rstate) {
+ this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
+ }
+
+ DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { linkRelationship: 'portal to:portal from' });
+
+ const fstate = this.TextBox.EditorView?.state;
+ if (fstate && selection) {
+ this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
+ }
+ };
if (!fieldKey) {
if (docId) {
- DocServer.GetRefField(docId).then(docx => {
- const rstate = this.TextBox.EditorView?.state;
- const selection = rstate?.selection.$from.pos;
- if (rstate) {
- this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
- }
- const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500 }, docId);
- DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { linkRelationship: 'portal to:portal from' });
-
- const fstate = this.TextBox.EditorView?.state;
- if (fstate && selection) {
- this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
- }
- });
+ const target = DocServer.QUERY_SERVER_CACHE(docId);
+ if (target) setTimeout(() => linkToDoc(target));
+ else DocServer.GetRefField(docId).then(docx => linkToDoc((docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId + '(auto)', _width: 500, _height: 500 }, docId)));
+
return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
}
return state.tr;
@@ -296,7 +291,7 @@ export class RichTextRules {
// create an inline equation node
// eq:<equation>>
- new InputRule(new RegExp(/:eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => {
+ new InputRule(new RegExp(/%eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => {
const fieldKey = 'math' + Utils.GenerateGuid();
this.TextBox.dataDoc[fieldKey] = match[1];
const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey }));
@@ -374,7 +369,7 @@ export class RichTextRules {
const content = selected.selection.content();
const replaced = node ? selected.replaceRangeWith(start, end, schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end))).setStoredMarks([...node.marks, ...sm]);
}),
new InputRule(new RegExp(/%\)/), (state, match, start, end) => {
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 3898490d3..5b47e8a70 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -349,7 +349,7 @@ export const marks: { [index: string]: MarkSpec } = {
group: 'inline',
toDOM(node: any) {
const uid = node.attrs.userid.replace('.', '').replace('@', '');
- const min = Math.round(node.attrs.modified / 12);
+ const min = Math.round(node.attrs.modified / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 60 / 24);
const remote = node.attrs.userid !== Doc.CurrentUserEmail ? ' UM-remote' : '';