aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMelissa Zhang <mzhang19096@gmail.com>2020-07-06 23:43:41 -0700
committerMelissa Zhang <mzhang19096@gmail.com>2020-07-06 23:43:41 -0700
commit4fc843a84a3f3001f749d5152bdc491d8efe7f94 (patch)
treeade4c015b65635febbb5eebdd5a48281df6aa1a0 /src
parentfbc7a328016af60052dd3f33c2d906e6c6447a5f (diff)
parent0438137cd435c47ce334b15a4ad00cbd70d80662 (diff)
merge with master
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/HypothesisAuthenticationManager.scss26
-rw-r--r--src/client/apis/HypothesisAuthenticationManager.tsx158
-rw-r--r--src/client/documents/Documents.ts41
-rw-r--r--src/client/util/CurrentUserUtils.ts1
-rw-r--r--src/client/views/GlobalKeyHandler.ts2
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/collections/CollectionSubView.tsx43
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx2
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx2
-rw-r--r--src/client/views/nodes/ComparisonBox.scss2
-rw-r--r--src/client/views/nodes/DocumentView.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.scss2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss23
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx38
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts10
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx57
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts6
-rw-r--r--src/server/ApiManagers/HypothesisManager.ts44
-rw-r--r--src/server/ApiManagers/UploadManager.ts3
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts3
-rw-r--r--src/server/index.ts3
21 files changed, 375 insertions, 95 deletions
diff --git a/src/client/apis/HypothesisAuthenticationManager.scss b/src/client/apis/HypothesisAuthenticationManager.scss
new file mode 100644
index 000000000..bd30dd94f
--- /dev/null
+++ b/src/client/apis/HypothesisAuthenticationManager.scss
@@ -0,0 +1,26 @@
+.authorize-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .paste-target {
+ padding: 5px;
+ width: 100%;
+ }
+
+ .avatar {
+ border-radius: 50%;
+ }
+
+ .welcome {
+ font-style: italic;
+ margin-top: 15px;
+ }
+
+ .disconnect {
+ font-size: 10px;
+ margin-top: 20px;
+ color: red;
+ cursor: grab;
+ }
+} \ No newline at end of file
diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx
new file mode 100644
index 000000000..cffb87227
--- /dev/null
+++ b/src/client/apis/HypothesisAuthenticationManager.tsx
@@ -0,0 +1,158 @@
+import { observable, action, reaction, runInAction, IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import MainViewModal from "../views/MainViewModal";
+import { Opt } from "../../fields/Doc";
+import { Networking } from "../Network";
+import "./HypothesisAuthenticationManager.scss";
+import { Scripting } from "../util/Scripting";
+
+const prompt = "Paste authorization code here...";
+
+@observer
+export default class HypothesisAuthenticationManager extends React.Component<{}> {
+ public static Instance: HypothesisAuthenticationManager;
+ private authenticationLink: Opt<string> = undefined;
+ @observable private openState = false;
+ @observable private authenticationCode: Opt<string> = undefined;
+ @observable private showPasteTargetState = false;
+ @observable private success: Opt<boolean> = undefined;
+ @observable private displayLauncher = true;
+ @observable private credentials: string;
+ private disposer: Opt<IReactionDisposer>;
+
+ private set isOpen(value: boolean) {
+ runInAction(() => this.openState = value);
+ }
+
+ private set shouldShowPasteTarget(value: boolean) {
+ runInAction(() => this.showPasteTargetState = value);
+ }
+
+ public cancel() {
+ this.openState && this.resetState(0, 0);
+ }
+
+ public fetchAccessToken = async (displayIfFound = false) => {
+ let response: any = await Networking.FetchFromServer("/readHypothesisAccessToken");
+ // if this is an authentication url, activate the UI to register the new access token
+ if (!response) { // new RegExp(AuthenticationUrl).test(response)) {
+ this.isOpen = true;
+ this.authenticationLink = response;
+ return new Promise<string>(async resolve => {
+ this.disposer?.();
+ this.disposer = reaction(
+ () => this.authenticationCode,
+ async authenticationCode => {
+ if (authenticationCode) {
+ this.disposer?.();
+ Networking.PostToServer("/writeHypothesisAccessToken", { authenticationCode });
+ runInAction(() => {
+ this.success = true;
+ this.credentials = response;
+ });
+ this.resetState();
+ resolve(authenticationCode);
+ }
+ }
+ );
+ });
+ }
+
+ if (displayIfFound) {
+ runInAction(() => {
+ this.success = true;
+ this.credentials = response;
+ });
+ this.resetState(-1, -1);
+ this.isOpen = true;
+ }
+ return response.access_token;
+ }
+
+ resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => {
+ if (!visibleForMS && !fadesOutInMS) {
+ runInAction(() => {
+ this.isOpen = false;
+ this.success = undefined;
+ this.displayLauncher = true;
+ this.credentials = "";
+ this.shouldShowPasteTarget = false;
+ this.authenticationCode = undefined;
+ });
+ return;
+ }
+ this.authenticationCode = undefined;
+ this.displayLauncher = false;
+ this.shouldShowPasteTarget = false;
+ if (visibleForMS > 0 && fadesOutInMS > 0) {
+ setTimeout(action(() => {
+ this.isOpen = false;
+ setTimeout(action(() => {
+ this.success = undefined;
+ this.displayLauncher = true;
+ this.credentials = "";
+ }), fadesOutInMS);
+ }), visibleForMS);
+ }
+ });
+
+ constructor(props: {}) {
+ super(props);
+ HypothesisAuthenticationManager.Instance = this;
+ }
+
+ private get renderPrompt() {
+ return (
+ <div className={'authorize-container'}>
+
+ {this.displayLauncher ? <button
+ className={"dispatch"}
+ onClick={() => {
+ this.shouldShowPasteTarget = true;
+ }}
+ style={{ marginBottom: this.showPasteTargetState ? 15 : 0 }}
+ >Authorize a Hypothesis account...</button> : (null)}
+ {this.showPasteTargetState ? <input
+ className={'paste-target'}
+ onChange={action(e => this.authenticationCode = e.currentTarget.value)}
+ placeholder={prompt}
+ /> : (null)}
+ {this.credentials ?
+ <>
+ <span
+ className={'welcome'}
+ >Welcome to Dash, {this.credentials}
+ </span>
+ <div
+ className={'disconnect'}
+ onClick={async () => {
+ await Networking.FetchFromServer("/revokeHypothesisAccessToken");
+ this.resetState(0, 0);
+ }}
+ >Disconnect Account</div>
+ </> : (null)}
+ </div>
+ );
+ }
+
+ private get dialogueBoxStyle() {
+ const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red";
+ return { borderColor, transition: "0.2s borderColor ease" };
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ isDisplayed={this.openState}
+ interactive={true}
+ contents={this.renderPrompt}
+ overlayDisplayedOpacity={0.9}
+ dialogueBoxStyle={this.dialogueBoxStyle}
+ />
+ );
+ }
+
+}
+
+Scripting.addGlobal("HypothesisAuthenticationManager", HypothesisAuthenticationManager); \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ae34e5909..27f61a128 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -830,6 +830,47 @@ export namespace Docs {
}
export namespace DocUtils {
+ export function FilterDocs(docs: Doc[], docFilters: string[], docRangeFilters: string[], viewSpecScript?: ScriptField) {
+ const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+
+ const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
+ for (let i = 0; i < docFilters.length; i += 3) {
+ const [key, value, modifiers] = docFilters.slice(i, i + 3);
+ if (!filterFacets[key]) {
+ filterFacets[key] = {};
+ }
+ filterFacets[key][value] = modifiers;
+ }
+
+ const filteredDocs = docFilters.length ? childDocs.filter(d => {
+ for (const facetKey of Object.keys(filterFacets)) {
+ const facet = filterFacets[facetKey];
+ const satisfiesFacet = Object.keys(facet).some(value => {
+ if (facet[value] === "match") {
+ return d[facetKey] === undefined || Field.toString(d[facetKey] as Field).includes(value);
+ }
+ return (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value);
+ });
+ if (!satisfiesFacet) {
+ return false;
+ }
+ }
+ return true;
+ }) : childDocs;
+ const rangeFilteredDocs = filteredDocs.filter(d => {
+ for (let i = 0; i < docRangeFilters.length; i += 3) {
+ const key = docRangeFilters[i];
+ const min = Number(docRangeFilters[i + 1]);
+ const max = Number(docRangeFilters[i + 2]);
+ const val = Cast(d[key], "number", null);
+ if (val !== undefined && (val < min || val > max)) {
+ return false;
+ }
+ }
+ return true;
+ });
+ return rangeFilteredDocs;
+ }
export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) {
targetID = targetID.replace(/^-/, "").replace(/\([0-9]*\)$/, "");
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 4276e04e4..9f04aab04 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -424,6 +424,7 @@ export class CurrentUserUtils {
{ title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc },
{ title: "Toggle a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
{ title: "Connect a Google Account", label: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
+ { title: "Connect a Hypothesis Account", label: "Hypothesis Account", icon: "houzz", click: 'HypothesisAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
];
}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 34f666f62..a3a023164 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -7,6 +7,7 @@ import { List } from "../../fields/List";
import { ScriptField } from "../../fields/ScriptField";
import { Cast, PromiseValue } from "../../fields/Types";
import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
+import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager";
import { DocServer } from "../DocServer";
import { DocumentType } from "../documents/DocumentTypes";
import { DictationManager } from "../util/DictationManager";
@@ -104,6 +105,7 @@ export default class KeyManager {
DictationManager.Controls.stop();
// RecommendationsBox.Instance.closeMenu();
GoogleAuthenticationManager.Instance.cancel();
+ HypothesisAuthenticationManager.Instance.cancel();
SharingManager.Instance.close();
break;
case "delete":
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 4c3add663..9221b27a5 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -61,6 +61,7 @@ import { LinkMenu } from './linking/LinkMenu';
import { LinkDocPreview } from './nodes/LinkDocPreview';
import { Fade } from '@material-ui/core';
import { LinkCreatedBox } from './nodes/LinkCreatedBox';
+import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager';
@observer
export class MainView extends React.Component {
@@ -605,6 +606,7 @@ export class MainView extends React.Component {
<SettingsManager />
<GroupManager />
<GoogleAuthenticationManager />
+ <HypothesisAuthenticationManager />
<DocumentDecorations />
<GestureOverlay>
<RichTextMenu key="rich" />
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index ed8535ecb..ce6872695 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -106,16 +106,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
[...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])];
}
@computed get childDocs() {
- const docFilters = this.docFilters();
- const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []);
- const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
- for (let i = 0; i < docFilters.length; i += 3) {
- const [key, value, modifiers] = docFilters.slice(i, i + 3);
- if (!filterFacets[key]) {
- filterFacets[key] = {};
- }
- filterFacets[key][value] = modifiers;
- }
let rawdocs: (Doc | Promise<Doc>)[] = [];
if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list;
@@ -128,38 +118,13 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const rootDoc = Cast(this.props.Document.rootDocument, Doc, null);
rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : [];
}
+
const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc);
+ const docFilters = this.docFilters();
const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
- const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+ const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []);
- const filteredDocs = docFilters.length && !this.props.dontRegisterView ? childDocs.filter(d => {
- for (const facetKey of Object.keys(filterFacets)) {
- const facet = filterFacets[facetKey];
- const satisfiesFacet = Object.keys(facet).some(value => {
- if (facet[value] === "match") {
- return d[facetKey] === undefined || Field.toString(d[facetKey] as Field).includes(value);
- }
- return (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value);
- });
- if (!satisfiesFacet) {
- return false;
- }
- }
- return true;
- }) : childDocs;
- const rangeFilteredDocs = filteredDocs.filter(d => {
- for (let i = 0; i < docRangeFilters.length; i += 3) {
- const key = docRangeFilters[i];
- const min = Number(docRangeFilters[i + 1]);
- const max = Number(docRangeFilters[i + 2]);
- const val = Cast(d[key], "number", null);
- if (val !== undefined && (val < min || val > max)) {
- return false;
- }
- }
- return true;
- });
- return rangeFilteredDocs;
+ return this.props.Document.dontRegisterView ? docs : DocUtils.FilterDocs(docs, docFilters, docRangeFilters, viewSpecScript);
}
@action
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 776266ce6..cd25c21b4 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -10,7 +10,7 @@ import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
import { CollectionSubView } from '../CollectionSubView';
-import "./collectionMulticolumnView.scss";
+import "./CollectionMulticolumnView.scss";
import ResizeBar from './MulticolumnResizer';
import WidthLabel from './MulticolumnWidthLabel';
import { List } from '../../../../fields/List';
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 1703ff4dc..51dcdcfe6 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -7,7 +7,7 @@ import { Doc } from '../../../../fields/Doc';
import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../fields/Types';
import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
import { Utils, returnZero, returnFalse, returnOne } from '../../../../Utils';
-import "./collectionMultirowView.scss";
+import "./CollectionMultirowView.scss";
import { computed, trace, observable, action } from 'mobx';
import { Transform } from '../../../util/Transform';
import HeightLabel from './MultirowHeightLabel';
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 810a824cf..acf6b1636 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -3,7 +3,7 @@
border-radius: inherit;
width: 100%;
height: 100%;
- position: absolute;
+ position: relative;
z-index: 0;
pointer-events: none;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 128a56c19..0e5b09f8b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1121,7 +1121,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return (this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots
this.layoutDoc.presBox || // presentationbox nodes
this.props.dontRegisterView ? (null) : // view that are not registered
- DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) =>
+ DocUtils.FilterDocs(DocListCast(this.Document.links), this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) =>
<DocumentView {...this.props} key={i + 1}
Document={d}
ContainingCollectionView={this.props.ContainingCollectionView}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 15148d01d..c1b95b308 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -2,7 +2,7 @@
border-radius: inherit;
width: 100%;
height: 100%;
- position: absolute;
+ position: relative;
transform-origin: top left;
.imageBox-fader {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 0a094ba6a..afdd8fea2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -303,27 +303,26 @@ footnote::after {
font-family: inherit;
}
ol {
- margin-left: 1em;
font-family: inherit;
}
- .bullet { p {display: inline-block; font-family: inherit} margin-left: 0; }
- .bullet1 { p {display: inline-block; font-family: inherit} }
- .bullet2,.bullet3,.bullet4,.bullet5,.bullet6 { p {display: inline-block; font-family: inherit} font-size: smaller; }
+ .bullet { p { font-family: inherit} margin-left: 0; }
+ .bullet1 { p { font-family: inherit} }
+ .bullet2,.bullet3,.bullet4,.bullet5,.bullet6 { p { font-family: inherit} font-size: smaller; }
.decimal1-ol { counter-reset: deci1; p {display: inline-block; font-family: inherit} margin-left: 0; }
- .decimal2-ol { counter-reset: deci2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 1em;}
- .decimal3-ol { counter-reset: deci3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;}
- .decimal4-ol { counter-reset: deci4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3em;}
+ .decimal2-ol { counter-reset: deci2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.1em;}
+ .decimal3-ol { counter-reset: deci3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.85em;}
+ .decimal4-ol { counter-reset: deci4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.85em;}
.decimal5-ol { counter-reset: deci5; p {display: inline-block; font-family: inherit} font-size: smaller; }
.decimal6-ol { counter-reset: deci6; p {display: inline-block; font-family: inherit} font-size: smaller; }
.decimal7-ol { counter-reset: deci7; p {display: inline-block; font-family: inherit} font-size: smaller; }
.multi1-ol { counter-reset: multi1; p {display: inline-block; font-family: inherit} margin-left: 0; padding-left: 1.2em }
- .multi2-ol { counter-reset: multi2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 1.4em;}
- .multi3-ol { counter-reset: multi3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;}
- .multi4-ol { counter-reset: multi4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.4em;}
+ .multi2-ol { counter-reset: multi2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;}
+ .multi3-ol { counter-reset: multi3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.85em;}
+ .multi4-ol { counter-reset: multi4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.85em;}
- .bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " }
+ //.bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " }
.decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content: counter(deci1) ". "; }
.decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; vertical-align: top; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; }
@@ -333,7 +332,7 @@ footnote::after {
.decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; vertical-align: top; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; }
.decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; vertical-align: top; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; }
- .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
+ .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; vertical-align: top; margin-left: -1.3em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
.multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; vertical-align: top; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; }
.multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; vertical-align: top; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; }
.multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; vertical-align: top; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 167ba782f..fc63dfbf5 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1032,6 +1032,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
_downX = 0;
_downY = 0;
_break = false;
+ _collapsed = false;
onPointerDown = (e: React.PointerEvent): void => {
if (this._recording && !e.ctrlKey && e.button === 0) {
this.stopDictation(true);
@@ -1115,7 +1116,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@action
onFocused = (e: React.FocusEvent): void => {
- console.log("FOUCSS");
FormattedTextBox.FocusedBox = this;
this.tryUpdateHeight();
@@ -1147,9 +1147,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
static _bulletStyleSheet: any = addStyleSheet();
static _userStyleSheet: any = addStyleSheet();
-
+ _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle
+ _forceDownNode: Node | undefined;
onClick = (e: React.MouseEvent): void => {
- if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
+ if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) {
+ this._forceDownNode = undefined;
+ return;
+ }
+ if (!this._forceUncollapse || (this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) {
@@ -1161,6 +1166,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document
this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size)));
}
+ } else if ([this._editorView!.state.schema.nodes.ordered_list, this._editorView!.state.schema.nodes.listItem].includes(node?.type) &&
+ node !== (this._editorView!.state.selection as NodeSelection)?.node && pcords) {
+ this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos!)));
}
}
if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
@@ -1168,12 +1176,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events
e.stopPropagation();
- this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
+ this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey);
}
+ this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed;
+ this._forceDownNode = (this._editorView!.state.selection as NodeSelection)?.node;
}
// this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
- hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean) {
+ hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, downNode: Node | undefined = undefined, selectOrderedList: boolean = false) {
+ this._forceUncollapse = false;
clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
const clickPos = this._editorView!.posAtCoords({ left: x, top: y });
let olistPos = clickPos?.pos;
@@ -1189,20 +1200,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
$olistPos = this._editorView?.state.doc.resolve(($olistPos as any).path[($olistPos as any).path.length - 4]);
}
}
+ const listPos = this._editorView?.state.doc.resolve(clickPos.pos);
const listNode = this._editorView?.state.doc.nodeAt(clickPos.pos);
- if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list) {
- if (!collapse) {
- if (!highlightOnly) {
- this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection($olistPos!)));
- }
- addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
- } else if (listNode && listNode.type === this._editorView.state.schema.nodes.list_item) {
- if (!highlightOnly) {
+ if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) {
+ if (!highlightOnly) {
+ if (selectOrderedList || (!collapse && listNode.attrs.visibility)) {
+ this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!)));
+ } else if (!listNode.attrs.visibility || downNode === listNode) {
this._editorView.dispatch(this._editorView.state.tr.setNodeMarkup(clickPos.pos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility }));
this._editorView.dispatch(this._editorView.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, clickPos.pos)));
}
- addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
}
+ addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
}
}
}
@@ -1247,7 +1256,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
public static HadSelection: boolean = false;
onBlur = (e: any) => {
- console.log("BLURRR");
FormattedTextBox.HadSelection = window.getSelection()?.toString() !== "";
//DictationManager.Controls.stop(false);
this.endUndoTypingBatch();
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 9d69f4be7..3f73ec436 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -16,13 +16,17 @@ const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) :
export type KeyMap = { [key: string]: any };
-export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string, from?: number, to?: number) => {
+export let updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: string, from?: number, to?: number) => {
+ let mapStyle = assignedMapStyle;
tx2.doc.descendants((node: any, offset: any, index: any) => {
if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) {
const path = (tx2.doc.resolve(offset) as any).path;
let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
- if (node.type === schema.nodes.ordered_list) depth++;
- tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle || node.attrs.mapStyle, bulletStyle: depth, }, node.marks);
+ if (node.type === schema.nodes.ordered_list) {
+ if (depth === 0 && !assignedMapStyle) mapStyle = node.attrs.mapStyle;
+ depth++;
+ }
+ tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle, bulletStyle: depth, }, node.marks);
}
});
return tx2;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 95d6c9fac..9075a6486 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -55,6 +55,7 @@ export default class RichTextMenu extends AntimodeMenu {
@observable private activeFontSize: string = "";
@observable private activeFontFamily: string = "";
@observable private activeListType: string = "";
+ @observable private activeAlignment: string = "left";
@observable private brushIsEmpty: boolean = true;
@observable private brushMarks: Set<Mark> = new Set();
@@ -91,7 +92,7 @@ export default class RichTextMenu extends AntimodeMenu {
{ mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize },
{ mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize },
{ mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
- { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ { mark: null, title: "", label: "...", command: unimplementedFunction, hidden: true },
{ mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option
];
@@ -110,7 +111,7 @@ export default class RichTextMenu extends AntimodeMenu {
this.listTypeOptions = [
{ node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType },
{ node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
- { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "A.1", command: this.changeListType },
//{ node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
];
@@ -178,11 +179,13 @@ export default class RichTextMenu extends AntimodeMenu {
// update active font family and size
const active = this.getActiveFontStylesOnSelection();
- const activeFamilies = active?.get("families");
- const activeSizes = active?.get("sizes");
+ const activeFamilies = active.activeFamilies;
+ const activeSizes = active.activeSizes;
- this.activeFontFamily = !activeFamilies?.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
- this.activeFontSize = !activeSizes?.length ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) : "various";
+ this.activeListType = this.getActiveListStyle();
+ this.activeAlignment = this.getActiveAlignment();
+ this.activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
+ this.activeFontSize = !activeSizes.length ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) : "...";
// update link in current selection
const targetTitle = await this.getTextLinkTargetTitle();
@@ -213,8 +216,34 @@ export default class RichTextMenu extends AntimodeMenu {
}
// finds font sizes and families in selection
+ getActiveAlignment() {
+ if (this.view) {
+ const path = (this.view.state.selection.$from as any).path;
+ for (let i = path.length - 3; i < path.length; i -= 3) {
+ if (path[i]?.type === this.view.state.schema.nodes.paragraph) {
+ return path[i].attrs.align || "left";
+ }
+ }
+ }
+ return "left";
+ }
+
+ // finds font sizes and families in selection
+ getActiveListStyle() {
+ if (this.view) {
+ const path = (this.view.state.selection.$from as any).path;
+ for (let i = 0; i < path.length; i += 3) {
+ if (path[i].type === this.view.state.schema.nodes.ordered_list) {
+ return path[i].attrs.mapStyle;
+ }
+ }
+ }
+ return "decimal";
+ }
+
+ // finds font sizes and families in selection
getActiveFontStylesOnSelection() {
- if (!this.view) return;
+ if (!this.view) return { activeFamilies: [], activeSizes: [] };
const activeFamilies: string[] = [];
const activeSizes: string[] = [];
@@ -228,10 +257,7 @@ export default class RichTextMenu extends AntimodeMenu {
});
}
- const styles = new Map<String, String[]>();
- styles.set("families", activeFamilies);
- styles.set("sizes", activeSizes);
- return styles;
+ return { activeFamilies, activeSizes };
}
getMarksInSelection(state: EditorState<any>) {
@@ -354,7 +380,8 @@ export default class RichTextMenu extends AntimodeMenu {
return <select onChange={onChange} key={key}>{items}</select>;
}
- createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element {
+ createNodesDropdown(activeMap: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element {
+ const activeOption = activeMap === "bullet" ? ":" : activeMap === "decimal" ? "1.1" : "A.1";
const items = options.map(({ title, label, hidden, style }) => {
if (hidden) {
return label === activeOption ?
@@ -871,9 +898,9 @@ export default class RichTextMenu extends AntimodeMenu {
this.createLinkButton(),
this.createBrushButton(),
<div className="richTextMenu-divider" />,
- 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("align-left", "Align Left", this.activeAlignment === "left", this.alignLeft),
+ this.createButton("align-center", "Align Center", this.activeAlignment === "center", this.alignCenter),
+ this.createButton("align-right", "Align Right", this.activeAlignment === "right", 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),
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index f83cff9b9..1af821738 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -310,9 +310,9 @@ export const nodes: { [index: string]: NodeSpec } = {
}],
toDOM(node: any) {
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 }, `${node.firstChild?.textContent}...`];
+ return ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, node.attrs.visibility ? 0 :
+ ["span", { style: `position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== "bullet" ? "inline-block" : "list-item"}; text-overflow: ellipsis; white-space: pre` },
+ `${node.firstChild?.textContent}...`]];
}
},
}; \ No newline at end of file
diff --git a/src/server/ApiManagers/HypothesisManager.ts b/src/server/ApiManagers/HypothesisManager.ts
new file mode 100644
index 000000000..33badbc42
--- /dev/null
+++ b/src/server/ApiManagers/HypothesisManager.ts
@@ -0,0 +1,44 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _permission_denied } from "../RouteManager";
+import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
+import { Database } from "../database";
+import { writeFile, readFile, readFileSync, existsSync } from "fs";
+import { serverPathToFile, Directory } from "./UploadManager";
+
+export default class HypothesisManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: "/readHypothesisAccessToken",
+ secureHandler: async ({ user, res }) => {
+ if (existsSync(serverPathToFile(Directory.hypothesis, user.id))) {
+ const read = readFileSync(serverPathToFile(Directory.hypothesis, user.id), "base64") || "";
+ console.log("READ = " + read);
+ res.send(read);
+ } else res.send("");
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: "/writeHypothesisAccessToken",
+ secureHandler: async ({ user, req, res }) => {
+ const write = req.body.authenticationCode;
+ console.log("WRITE = " + write);
+ res.send(await writeFile(serverPathToFile(Directory.hypothesis, user.id), write, "base64", () => { }));
+ }
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/revokeHypothesisAccessToken",
+ secureHandler: async ({ user, res }) => {
+ await Database.Auxiliary.GoogleAccessToken.Revoke("dash-hyp-" + user.id);
+ res.send();
+ }
+ });
+
+ }
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index fe39b84e6..55ceab9fb 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -24,7 +24,8 @@ export enum Directory {
pdfs = "pdfs",
text = "text",
pdf_thumbnails = "pdf_thumbnails",
- audio = "audio"
+ audio = "audio",
+ hypothesis = "hypothesis"
}
export function serverPathToFile(directory: Directory, filename: string) {
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 20f96f432..b0157a85f 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -39,7 +39,8 @@ export namespace GoogleApiServerUtils {
*/
export enum Service {
Documents = "Documents",
- Slides = "Slides"
+ Slides = "Slides",
+ Hypothesis = "Hypothesis"
}
/**
diff --git a/src/server/index.ts b/src/server/index.ts
index 083173bb5..9af4b00bc 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -18,10 +18,10 @@ import PDFManager from "./ApiManagers/PDFManager";
import UploadManager from "./ApiManagers/UploadManager";
import { log_execution } from "./ActionUtilities";
import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager";
+import HypothesisManager from "./ApiManagers/HypothesisManager";
import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
import { Logger } from "./ProcessFactory";
import { yellow } from "colors";
-import { DashSessionAgent } from "./DashSession/DashSessionAgent";
import SessionManager from "./ApiManagers/SessionManager";
import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent";
@@ -72,6 +72,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
new DeleteManager(),
new UtilManager(),
new GeneralGoogleManager(),
+ new HypothesisManager(),
new GooglePhotosManager(),
];