From de0b723551eb033f7ad1999dd23309b4f306f344 Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 8 Oct 2019 10:08:36 -0400 Subject: moved style sheet stuff into Utils. fixed menu/footnote interaction --- src/Utils.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'src/Utils.ts') diff --git a/src/Utils.ts b/src/Utils.ts index 4b892aa70..f6ab434a8 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -337,4 +337,17 @@ export default function smoothScroll(duration: number, element: HTMLElement, to: } }; animateScroll(); +} +export function addStyleSheet(styleType: string = "text/css") { + let style = document.createElement("style"); + style.type = styleType; + var sheets = document.head.appendChild(style); + return (sheets as any).sheet; +} +export function addStyleSheetRule(sheet: any, selector: any, css: any) { + var propText = typeof css === "string" ? css : Object.keys(css).map(p => p + ":" + (p === "content" ? "'" + css[p] + "'" : css[p])).join(";"); + return sheet.insertRule("." + selector + "{" + propText + "}", sheet.cssRules.length); +} +export function removeStyleSheetRule(sheet: any, rule: number) { + sheet.removeRule(rule); } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 84211163c9f085a521b3ca3af0dc9103640e3622 Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 8 Oct 2019 14:07:32 -0400 Subject: cleaned up tagging of text spans with data and timestamps --- src/Utils.ts | 13 ++++++- src/client/util/DocumentManager.ts | 2 +- src/client/util/RichTextRules.ts | 21 ++++++++++ src/client/util/RichTextSchema.tsx | 34 ++++++++++++----- src/client/views/nodes/FormattedTextBox.tsx | 59 +++++++++++++++++++++++++---- 5 files changed, 109 insertions(+), 20 deletions(-) (limited to 'src/Utils.ts') diff --git a/src/Utils.ts b/src/Utils.ts index f6ab434a8..489de3b50 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -349,5 +349,16 @@ export function addStyleSheetRule(sheet: any, selector: any, css: any) { return sheet.insertRule("." + selector + "{" + propText + "}", sheet.cssRules.length); } export function removeStyleSheetRule(sheet: any, rule: number) { - sheet.removeRule(rule); + if (sheet.rules.length) { + sheet.removeRule(rule); + return true; + } + return false; +} +export function clearStyleSheetRules(sheet: any) { + if (sheet.rules.length) { + numberRange(sheet.rules.length).map(n => sheet.removeRule(0)); + return true; + } + return false; } \ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 83a69465f..c45d3a75f 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -168,7 +168,7 @@ export class DocumentManager { retryDocView.props.focus(targetDoc, willZoom); // focus on the target if it now exists in the context } else { if (closeContextIfNotFound && targetDocContextView.props.removeDocument) targetDocContextView.props.removeDocument(targetDocContextView.props.Document); - (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); // otherwise create a new view of the target + targetDoc.layout && (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); // otherwise create a new view of the target } highlight(); }, 0); diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts index 2ff758fbc..346d5ab99 100644 --- a/src/client/util/RichTextRules.ts +++ b/src/client/util/RichTextRules.ts @@ -71,6 +71,27 @@ export const inpRules = { } return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) })); }), + new InputRule( + new RegExp(/\t/), + (state, match, start, end) => { + if (state.selection.to === state.selection.from) return null; + let node = (state.doc.resolve(start) as any).nodeAfter; + return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "todo", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + }), + new InputRule( + new RegExp(/\!/), + (state, match, start, end) => { + if (state.selection.to === state.selection.from) return null; + let node = (state.doc.resolve(start) as any).nodeAfter; + return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "important", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + }), + new InputRule( + new RegExp(/\x/), + (state, match, start, end) => { + if (state.selection.to === state.selection.from) return null; + let node = (state.doc.resolve(start) as any).nodeAfter; + return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "disagree", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + }), new InputRule( new RegExp(/^\^\^\s$/), (state, match, start, end) => { diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index bc1ad5070..80bcf25ad 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -439,20 +439,34 @@ export const marks: { [index: string]: MarkSpec } = { user_mark: { attrs: { userid: { default: "" }, - hide_users: { default: [] }, opened: { default: true }, - modified: { default: "when?" } + modified: { default: "when?" }, // 5 second intervals since 1970 }, group: "inline", toDOM(node: any) { - let hideUsers = node.attrs.hide_users; - let hidden = hideUsers.indexOf(node.attrs.userid) !== -1 || (hideUsers.length === 0 && node.attrs.userid !== Doc.CurrentUserEmail); - return hidden ? - (node.attrs.opened ? - ['span', { class: "userMarkOpen" }, 0] : - ['span', { class: "userMark" }, ['span', 0]] - ) : - ['span', 0]; + let uid = node.attrs.userid.replace(".", "").replace("@", ""); + let min = Math.round(node.attrs.modified / 12); + let hr = Math.round(min / 60); + let day = Math.round(hr / 60 / 24); + return node.attrs.opened ? + ['span', { class: "userMark-" + uid + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0] : + ['span', { class: "userMark-" + uid + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, ['span', 0]]; + } + }, + // the id of the user who entered the text + user_tag: { + attrs: { + userid: { default: "" }, + opened: { default: true }, + modified: { default: "when?" }, // 5 second intervals since 1970 + tag: { default: "" } + }, + group: "inline", + toDOM(node: any) { + let uid = node.attrs.userid.replace(".", "").replace("@", ""); + return node.attrs.opened ? + ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0] : + ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, ['span', 0]]; } }, diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 96b434708..d878f2b74 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -18,7 +18,7 @@ import { RichTextField } from "../../../new_fields/RichTextField"; import { RichTextUtils } from '../../../new_fields/RichTextUtils'; import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types"; -import { numberRange, timenow, Utils, addStyleSheet, addStyleSheetRule, removeStyleSheetRule } from '../../../Utils'; +import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from '../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from '../../documents/Documents'; @@ -43,6 +43,7 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './Format import React = require("react"); import { ContextMenuProps } from '../ContextMenuItem'; import { ContextMenu } from '../ContextMenu'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -298,9 +299,51 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + + static _highlights: string[] = []; + + updateHighlights = () => { + clearStyleSheetRules(FormattedTextBox._userStyleSheet); + if (FormattedTextBox._highlights.indexOf("My Text") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "yellow" }); + } + if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" }); + } + if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" }); + } + if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" }); + } + if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); + let min = Math.round(Date.now() / 1000 / 60); + numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); + setTimeout(() => this.updateHighlights()); + } + if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); + let hr = Math.round(Date.now() / 1000 / 60 / 60); + numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); + } + } + specificContextMenu = (e: React.MouseEvent): void => { let funcs: ContextMenuProps[] = []; funcs.push({ description: "Dictate", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" }); + ["My Text", "Todo Items", "Important Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => + funcs.push({ + description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { + e.stopPropagation(); + if (FormattedTextBox._highlights.indexOf(option) === -1) { + FormattedTextBox._highlights.push(option); + } else { + FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1); + } + this.updateHighlights(); + }, icon: "expand-arrows-alt" + })); ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" }); } @@ -718,7 +761,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } this._editorView!.focus(); // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. - this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })]; + this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })]; } getFont(font: string) { switch (font) { @@ -781,7 +824,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - static _sheet: any = addStyleSheet(); + static _bulletStyleSheet: any = addStyleSheet(); + static _userStyleSheet: any = addStyleSheet(); onClick = (e: React.MouseEvent): void => { if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } @@ -824,9 +868,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe // 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, offsetX: number, select: boolean = false) { - if (FormattedTextBox._sheet.rules.length) { - removeStyleSheetRule(FormattedTextBox._sheet, 0); - } + clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); if (this.props.isSelected() && offsetX < 40) { let pos = this._editorView!.posAtCoords({ left: x, top: y }); if (pos && pos.pos > 0) { @@ -840,7 +882,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined; if (ol && ol.type === schema.nodes.ordered_list && select) { this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2)))); - addStyleSheetRule(FormattedTextBox._sheet, hit.className + ":before", { background: "gray" }); + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, hit.className + ":before", { background: "gray" }); } else { this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility })); } @@ -898,7 +940,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (e.key === "Tab" || e.key === "Enter") { e.preventDefault(); } - this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }))); + let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) }); + this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); -- cgit v1.2.3-70-g09d2 From 29cdf0d66df3e38408f69ac8225b4d59397ee2e6 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Tue, 8 Oct 2019 19:02:35 -0400 Subject: fixes for text selection in pdfs to be less jumpy and to work through other annotations. --- src/Utils.ts | 2 +- src/client/util/TooltipTextMenu.tsx | 2 +- src/client/views/nodes/FormattedTextBox.tsx | 2 +- src/client/views/pdf/PDFMenu.tsx | 2 +- src/client/views/pdf/PDFViewer.scss | 4 ++++ src/client/views/pdf/PDFViewer.tsx | 15 ++++++++++----- 6 files changed, 18 insertions(+), 9 deletions(-) (limited to 'src/Utils.ts') diff --git a/src/Utils.ts b/src/Utils.ts index 489de3b50..9a2f01f80 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -320,7 +320,7 @@ const easeInOutQuad = (currentTime: number, start: number, change: number, durat return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start; }; -export default function smoothScroll(duration: number, element: HTMLElement, to: number) { +export function smoothScroll(duration: number, element: HTMLElement, to: number) { const start = element.scrollTop; const change = to - start; const startDate = new Date().getTime(); diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 5f2bc18b5..fe4c5ac9f 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -545,7 +545,7 @@ export class TooltipTextMenu { view.dispatch(tx2); })) { let tx2 = view.state.tr; - let tx3 = updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle); + let tx3 = nodeType ? updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle) : tx2; marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 86166b0b3..67d1bc42c 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -548,7 +548,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let editor = this._editorView; let ret = findLinkFrag(editor.state.doc.content, editor); - if (ret.frag.size > 2) { + if (ret.frag.size > 2 && ret.start >= 0) { let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 1e3320069..517a99a68 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -155,7 +155,7 @@ export default class PDFMenu extends React.Component { @action highlightClicked = (e: React.MouseEvent) => { - if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { + if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { // yellowish highlight color for a marker type highlight this.Highlighting = !this.Highlighting; } } diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index c77cee792..f6fedf3da 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -15,6 +15,10 @@ mix-blend-mode: multiply; opacity: 0.9; + span { + padding-right: 5px; + padding-bottom: 4px; + } } .textLayer ::selection { background: yellow; } // should match the backgroundColor in createAnnotation() .textLayer .highlight { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 4516e9904..b010d16c8 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -9,7 +9,7 @@ import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import smoothScroll, { Utils, emptyFunction, returnOne, intersectRect } from "../../../Utils"; +import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { CompiledScript, CompileScript } from "../../util/Scripting"; @@ -24,6 +24,7 @@ import { CollectionView } from "../collections/CollectionView"; import Annotation from "./Annotation"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { SelectionManager } from "../../util/SelectionManager"; +import { undoBatch } from "../../util/UndoManager"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); const pdfjsLib = require("pdfjs-dist"); @@ -62,6 +63,7 @@ interface IViewerProps { */ @observer export class PDFViewer extends React.Component { + static _annotationStyle: any = addStyleSheet(); @observable private _pageSizes: { width: number, height: number }[] = []; @observable private _annotations: Doc[] = []; @observable private _savedAnnotations: Dictionary = new Dictionary(); @@ -150,7 +152,7 @@ export class PDFViewer extends React.Component { copy = (e: ClipboardEvent) => { if (this.props.active() && e.clipboardData) { - let annoDoc = this.makeAnnotationDocument("#0390fc"); + let annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)"); // copied text markup color (blueish) if (annoDoc) { e.clipboardData.setData("text/plain", this._selectionText); e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); @@ -237,6 +239,7 @@ export class PDFViewer extends React.Component { this._pdfViewer.setDocument(this.props.pdf); } + @undoBatch @action makeAnnotationDocument = (color: string): Opt => { if (this._savedAnnotations.size() === 0) return undefined; @@ -388,6 +391,7 @@ export class PDFViewer extends React.Component { // if alt+left click, drag and annotate this._downX = e.clientX; this._downY = e.clientY; + addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); if (NumCast(this.props.Document.scale, 1) !== 1) return; if ((e.button !== 0 || e.altKey) && this.active()) { this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true); @@ -475,6 +479,7 @@ export class PDFViewer extends React.Component { @action onSelectEnd = (e: PointerEvent): void => { + clearStyleSheetRules(PDFViewer._annotationStyle); this._savedAnnotations.clear(); if (this._marqueeing) { if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { @@ -510,8 +515,8 @@ export class PDFViewer extends React.Component { } } - if (PDFMenu.Instance.Highlighting) { - this.highlight("rgba(245, 230, 95, 0.616)"); // when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up + if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up + this.highlight("rgba(245, 230, 95, 0.616)"); // yellowish highlight color for highlighted text (should match PDFMenu's highlight color) } else { PDFMenu.Instance.StartDrag = this.startDrag; @@ -538,7 +543,7 @@ export class PDFViewer extends React.Component { e.preventDefault(); e.stopPropagation(); let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title }); - const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); + const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection if (annotationDoc) { let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc); DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, { -- cgit v1.2.3-70-g09d2