diff options
author | bobzel <zzzman@gmail.com> | 2020-07-07 10:01:33 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-07 10:01:33 -0400 |
commit | 06ef30846b8014d6701caa86b264612ceabc27d1 (patch) | |
tree | 69e1699c41688ffa984c3acc267f5ef2e77d62e2 /src | |
parent | 0438137cd435c47ce334b15a4ad00cbd70d80662 (diff) | |
parent | 915f35e6d6c0b0f7bdb18c0c2a6aa88ee5df5eed (diff) |
Merge pull request #433 from browngraphicslab/anika_linking
Pull Request for Andy
Diffstat (limited to 'src')
24 files changed, 567 insertions, 100 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 3355c0091..ad62169ab 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -94,6 +94,7 @@ export interface DocumentOptions { label?: string; // short form of title for use as an icon label style?: string; page?: number; + description?: string; // added for links _viewScale?: number; isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents forceActive?: boolean; @@ -259,7 +260,7 @@ export namespace Docs { }], [DocumentType.LINK, { layout: { view: LinkBox, dataField: defaultDataKey }, - options: { _height: 150 } + options: { _height: 150, description: "" } }], [DocumentType.LINKDB, { data: new List<Doc>(), @@ -905,15 +906,15 @@ export namespace DocUtils { export let ActiveRecordings: Doc[] = []; export function MakeLinkToActiveAudio(doc: Doc) { - DocUtils.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: doc }, { doc: d }, "audio link", "audio timeline")); + DocUtils.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: doc }, { doc: d }, "audio link", "", "audio timeline")); } - export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", id?: string) { + export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string) { const sv = DocumentManager.Instance.getDocumentView(source.doc); if (sv && sv.props.ContainingCollectionDoc === target.doc) return; if (target.doc === Doc.UserDoc()) return undefined; - const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView" }, id); + const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView", description }, id); linkDoc.layout_linkView = Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null); Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('self.anchor1?.title +" (" + (self.linkRelationship||"to") +") " + self.anchor2?.title'); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 749fabfcc..6da581f35 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,4 +1,4 @@ -import { Doc, DocListCast } from "../../fields/Doc"; +import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; @@ -23,6 +23,10 @@ import { Scripting } from "./Scripting"; export class LinkManager { private static _instance: LinkManager; + + + public static currentLink: Opt<Doc>; + public static get Instance(): LinkManager { return this._instance || (this._instance = new this()); } diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 628db366f..25a87ab56 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -194,7 +194,11 @@ export class EditableView extends React.Component<EditableProps> { ref={this._ref} style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} onClick={this.onClick} placeholder={this.props.placeholder}> - <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> + <span style={{ + fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, + color: this.props.contents ? this.props.color ? this.props.color : "black" : "grey" + }}> + {this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> </div> ); } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 487467b2b..cdc468066 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -551,7 +551,7 @@ export default class GestureOverlay extends Touchable { else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) { // we don't want to create a link between ink strokes (doing so makes drawing a t very hard) if (this._d1.type !== "ink" && doc.type !== "ink") { - DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link"); + DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link", ""); actionPerformed = true; } } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index eba9bb344..584ed2f1c 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 { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager'; @observer @@ -610,6 +611,7 @@ export class MainView extends React.Component { </GestureOverlay> <PreviewCursor /> <LinkCreatedBox /> + {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.EditLink ? <LinkMenu location={DocumentLinksButton.EditLinkLoc} docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors} linkDoc={LinkDocPreview.LinkInfo.linkDoc} linkSrc={LinkDocPreview.LinkInfo.linkSrc} href={LinkDocPreview.LinkInfo.href} diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx index cdde32c21..196151e32 100644 --- a/src/client/views/RecommendationsBox.tsx +++ b/src/client/views/RecommendationsBox.tsx @@ -169,7 +169,7 @@ export class RecommendationsBox extends React.Component<FieldViewProps> { <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}> <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" /> </div> - <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "Recommender", undefined)}> + <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "Recommender", "", undefined)}> <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" /> </div> </div> diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index 5ada79a28..b8b72e756 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/CollectionLinearView.scss @@ -35,6 +35,18 @@ font-size: 12.5px; } + .bottomPopup-descriptions { + display: inline; + white-space: nowrap; + padding-left: 8px; + padding-right: 8px; + vertical-align: middle; + background-color: lightgrey; + border-radius: 5.5px; + color: black; + margin-right: 5px; + } + .bottomPopup-exit { display: inline; white-space: nowrap; diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 7cbe5c19d..c370415be 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -15,6 +15,7 @@ import { documentSchema } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { LinkDescriptionPopup } from '../nodes/LinkDescriptionPopup'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -89,6 +90,21 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { DocumentLinksButton.StartLink = undefined; } + @action + changeDescriptionSetting = () => { + if (LinkDescriptionPopup.showDescriptions) { + if (LinkDescriptionPopup.showDescriptions === "ON") { + LinkDescriptionPopup.showDescriptions = "OFF"; + LinkDescriptionPopup.descriptionPopup = false; + } else { + LinkDescriptionPopup.showDescriptions = "ON"; + } + } else { + LinkDescriptionPopup.showDescriptions = "OFF"; + LinkDescriptionPopup.descriptionPopup = false; + } + } + render() { const guid = Utils.GenerateGuid(); const flexDir: any = StrCast(this.Document.flexDirection); @@ -155,6 +171,9 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { onPointerDown={e => e.stopPropagation()} > <span className="bottomPopup-text" > Creating link from: {DocumentLinksButton.StartLink.title} </span> + <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting} + > Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : "ON"} + </span> <span className="bottomPopup-exit" onClick={this.exitLongLinks} >Exit</span> diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 620b977fa..26c41f524 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -207,7 +207,7 @@ class TreeView extends React.Component<TreeViewProps> { if (complete.linkDragData) { const sourceDoc = complete.linkDragData.linkSourceDocument; const destDoc = this.doc; - DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link"); + DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link", ""); e.stopPropagation(); } const docDragData = complete.docDragData; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 26abd2529..df21d6a28 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -147,7 +147,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null) }); Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); - const pushpinLink = 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); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index b81e400b3..9bf425db2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -246,7 +246,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } else { const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); this.props.addDocument(source); - linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation"); // TODODO this is where in text links get passed + linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation", ""); // TODODO this is where in text links get passed e.stopPropagation(); return true; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 97ed74c10..b47236bea 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -493,7 +493,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque summary._backgroundColor = "#e2ad32"; portal.layoutKey = "layout_portal"; portal.title = "document collection"; - DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing"); + DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing", ""); this.props.addLiveTextDocument(summary); MarqueeOptionsMenu.Instance.fadeOut(true); diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss index b47c8976e..406a38c26 100644 --- a/src/client/views/linking/LinkEditor.scss +++ b/src/client/views/linking/LinkEditor.scss @@ -7,34 +7,112 @@ user-select: none; } -.linkEditor-back { +.linkEditor-button-back { margin-bottom: 6px; + border-radius: 10px; + width: 18px; + height: 18px; + padding: 0; } .linkEditor-info { - border-bottom: 0.5px solid $light-color-secondary; - padding-bottom: 6px; - margin-bottom: 6px; + //border-bottom: 0.5px solid $light-color-secondary; + padding-bottom: 4px; + padding-top: 5px; + padding-left: 5px; + //margin-bottom: 6px; display: flex; justify-content: space-between; .linkEditor-linkedTo { width: calc(100% - 26px); + padding-left: 5px; + padding-right: 5px } } -.linkEditor-button, .linkEditor-addbutton { +.linkEditor-description { + padding-left: 6.5px; + padding-right: 6.5px; + padding-bottom: 3.5px; + + .linkEditor-description-text { + text-decoration-color: grey; + } + + .linkEditor-description-input { + border: 1px solid grey; + border-radius: 4px; + background-color: rgb(236, 236, 236); + padding-left: 2px; + padding-right: 2px; + color: grey; + text-decoration-color: grey; + } +} + +.linkEditor-followingDropdown { + padding-left: 6.5px; + padding-right: 6.5px; + padding-bottom: 3.5px; + + .linkEditor-followingDropdown-dropdown { + + .linkEditor-followingDropdown-header { + + border: 1px solid grey; + border-radius: 4px; + background-color: rgb(236, 236, 236); + padding-left: 2px; + padding-right: 2px; + color: grey; + text-decoration-color: grey; + + .linkEditor-followingDropdown-icon { + float: right; + } + } + + .linkEditor-followingDropdown-optionsList { + padding-left: 3px; + padding-right: 3px; + + .linkEditor-followingDropdown-option { + border: 0.25px dotted grey; + background-color: rgb(236, 236, 236); + padding-left: 2px; + padding-right: 2px; + color: grey; + text-decoration-color: grey; + font-size: 9px; + + &:hover { + background-color: rgb(211, 210, 210); + } + } + + } + } + + +} + + +.linkEditor-button, +.linkEditor-addbutton { width: 18px; height: 18px; padding: 0; // font-size: 12px; border-radius: 10px; + &:disabled { background-color: gray; } } -.linkEditor-addbutton{ + +.linkEditor-addbutton { margin-left: 0px; } @@ -44,7 +122,7 @@ } .linkEditor-group { - background-color: $light-color-secondary; + background-color: $light-color-secondary; padding: 6px; margin: 3px 0; border-radius: 3px; @@ -56,7 +134,7 @@ .linkEditor-group-row-label { margin-right: 6px; - display:inline-block; + display: inline-block; } .linkEditor-metadata-row { diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index 13b9a2459..014d57ed0 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -1,14 +1,18 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faArrowLeft, faCog, faEllipsisV, faExchangeAlt, faPlus, faTable, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable } from "mobx"; +import { action, observable, computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../fields/Doc"; +import { Doc, Opt } from "../../../fields/Doc"; import { StrCast } from "../../../fields/Types"; import { Utils } from "../../../Utils"; import { LinkManager } from "../../util/LinkManager"; import './LinkEditor.scss'; import React = require("react"); +import { DocumentView } from "../nodes/DocumentView"; +import { DocumentLinksButton } from "../nodes/DocumentLinksButton"; +import { EditableView } from "../EditableView"; +import { RefObject } from "react"; library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus); @@ -281,27 +285,113 @@ interface LinkEditorProps { @observer export class LinkEditor extends React.Component<LinkEditorProps> { + + @observable description = StrCast(LinkManager.currentLink?.description); + @observable openDropdown: boolean = false; + + @observable followBehavior = this.props.linkDoc.follow ? this.props.linkDoc.follow : "Default"; + + + //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION"; + @action deleteLink = (): void => { LinkManager.Instance.deleteLink(this.props.linkDoc); this.props.showLinks(); } + @action + setDescripValue = (value: string) => { + if (LinkManager.currentLink) { + LinkManager.currentLink.description = value; + return true; + } + } + + @computed + get editDescription() { + return <div className="linkEditor-description"> + <div className="linkEditor-description-label"> + Link Description:</div> + <div className="linkEditor-description-input"> + <EditableView + GetValue={() => StrCast(LinkManager.currentLink?.description)} + SetValue={value => { this.setDescripValue(value); return false; }} + contents={LinkManager.currentLink?.description} + placeholder={"(optional) enter link description"} + color={"rgb(88, 88, 88)"} + ></EditableView></div></div>; + } + + @action + changeDropdown = () => { + this.openDropdown = !this.openDropdown; + } + + @action + changeFollowBehavior = (follow: string) => { + this.openDropdown = false; + this.followBehavior = follow; + this.props.linkDoc.follow = follow; + } + + @computed + get followingDropdown() { + return <div className="linkEditor-followingDropdown"> + <div className="linkEditor-followingDropdown-label"> + Follow Behavior:</div> + <div className="linkEditor-followingDropdown-dropdown"> + <div className="linkEditor-followingDropdown-header" + onPointerDown={this.changeDropdown}> + {this.followBehavior} + <FontAwesomeIcon className="linkEditor-followingDropdown-icon" + icon={this.openDropdown ? "chevron-up" : "chevron-down"} + size={"lg"} onPointerDown={this.changeDropdown} /> + </div> + <div className="linkEditor-followingDropdown-optionsList" + style={{ display: this.openDropdown ? "" : "none" }}> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("Default")}> + Default + </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("Always open in right tab")}> + Always open in right tab + </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("Always open in new tab")}> + Always open in new tab + </div> + </div> + </div> + </div>; + } + render() { const destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); const groups = [this.props.linkDoc].map(groupDoc => { - return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.linkRelationship)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />; + return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.linkRelationship)} linkDoc={this.props.linkDoc} + sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />; }); return !destination ? (null) : ( <div className="linkEditor"> - {this.props.hideback ? (null) : <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>} <div className="linkEditor-info"> - <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto?.title ?? destination.title ?? "untitled"}</b></p> - <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button> + <button className="linkEditor-button-back" + style={{ display: this.props.hideback ? "none" : "" }} + onClick={this.props.showLinks}> + <FontAwesomeIcon icon="arrow-left" size="sm" /> </button> + <p className="linkEditor-linkedTo">editing link to: <b>{ + destination.proto?.title ?? destination.title ?? "untitled"}</b></p> + <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"> + <FontAwesomeIcon icon="trash" size="sm" /></button> </div> - {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>} + + <div>{this.editDescription}</div> + <div>{this.followingDropdown}</div> + + {/* {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>} */} </div> ); diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 6468ccd3d..4b1a3f425 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -3,20 +3,32 @@ .linkMenu { width: 100%; height: auto; + //border: 1px solid black; + + &:hover { + width: calc(auto + 26px); + } } .linkMenu-list { + border: 1px solid black; max-height: 200px; overflow-y: scroll; position: absolute; z-index: 10; - background: $link-color; - min-width: 150px + background: white; + min-width: 150px; + border-radius: 5px; + padding-top: 6.5px; + padding-bottom: 6.5px; + padding-left: 6.5px; + padding-right: 2px; + //width: calc(auto + 50px); } .linkMenu-group { - border-bottom: 0.5px solid lightgray; - padding: 5px 0; + //border-bottom: 0.5px solid lightgray; + //@extend: 5px 0; &:last-child { @@ -26,13 +38,15 @@ .linkMenu-group-name { display: flex; + &:hover { p { background-color: lightgray; + } p.expand-one { - width: calc(100% - 26px); + width: calc(100% + 26px); } .linkEditor-tableButton { diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index c672511ac..8a7b12f48 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -11,6 +11,7 @@ import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { library } from "@fortawesome/fontawesome-svg-core"; import { DocumentLinksButton } from "../nodes/DocumentLinksButton"; import { LinkDocPreview } from "../nodes/LinkDocPreview"; +import { isUndefined } from "util"; library.add(faTrash); @@ -25,19 +26,20 @@ interface Props { export class LinkMenu extends React.Component<Props> { @observable private _editingLink?: Doc; - @observable private _linkMenuRef: Opt<HTMLDivElement | null>; + @observable private _linkMenuRef = React.createRef<HTMLDivElement>(); + private _editorRef = React.createRef<HTMLDivElement>(); @action onClick = (e: PointerEvent) => { LinkDocPreview.LinkInfo = undefined; - if (this._linkMenuRef?.contains(e.target as any)) { - DocumentLinksButton.EditLink = undefined; - } - if (this._linkMenuRef && !this._linkMenuRef.contains(e.target as any)) { - DocumentLinksButton.EditLink = undefined; + if (this._linkMenuRef && !!!this._linkMenuRef.current?.contains(e.target as any)) { + if (this._editorRef && !!!this._editorRef.current?.contains(e.target as any)) { + console.log("outside click"); + DocumentLinksButton.EditLink = undefined; + } } } @action @@ -78,12 +80,14 @@ export class LinkMenu extends React.Component<Props> { render() { const sourceDoc = this.props.docView.props.Document; const groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc); - return <div className="linkMenu-list" - ref={(r) => this._linkMenuRef = r} style={{ left: this.props.location[0], top: this.props.location[1] }}> - {!this._editingLink ? - this.renderAllGroups(groups) : - <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)} /> - } - </div>; + return <div className="linkMenu" ref={this._linkMenuRef} > + <div className="linkMenu-list" + style={{ left: this.props.location[0], top: this.props.location[1] }}> + {!this._editingLink ? + this.renderAllGroups(groups) : + <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} + showLinks={action(() => this._editingLink = undefined)} /> + } + </div> </div>; } }
\ No newline at end of file diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index 7892d381b..ec17776e3 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -26,6 +26,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { private _drag = React.createRef<HTMLDivElement>(); private _table = React.createRef<HTMLDivElement>(); + private _menuRef = React.createRef<HTMLDivElement>(); onLinkButtonDown = (e: React.PointerEvent): void => { e.stopPropagation(); @@ -74,12 +75,13 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} - showEditor={this.props.showEditor} />; + showEditor={this.props.showEditor} + menuRef={this._menuRef} />; } }); return ( - <div className="linkMenu-group"> + <div className="linkMenu-group" ref={this._menuRef}> {/* <div className="linkMenu-group-name"> <p ref={this._drag} onPointerDown={this.onLinkButtonDown} className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p> diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss index e3ce69cd7..67bf71fb9 100644 --- a/src/client/views/linking/LinkMenuItem.scss +++ b/src/client/views/linking/LinkMenuItem.scss @@ -4,19 +4,39 @@ // border-top: 0.5px solid $main-accent; position: relative; display: flex; - font-size: 12px; .linkMenu-name { position: relative; - p { - padding: 4px 6px; - line-height: 12px; - border-radius: 5px; - overflow-wrap: break-word; - user-select: none; + .linkMenu-text { + + padding: 4px 2px; + //display: inline; + + .linkMenu-destination-title { + text-decoration: none; + color: rgb(85, 120, 196); + font-size: 14px; + padding-bottom: 2px; + } + + .linkMenu-description { + text-decoration: none; + font-style: italic; + color: rgb(95, 97, 102); + font-size: 10px; + } + + p { + //padding: 4px 2px; + line-height: 12px; + border-radius: 5px; + overflow-wrap: break-word; + user-select: none; + } } + } .linkMenu-item-content { @@ -32,25 +52,40 @@ } &:hover { + + .linkMenu-item-buttons { display: flex; } .linkMenu-item-content { + + .linkMenu-destination-title { + text-decoration: underline; + color: rgb(60, 90, 156); + //display: inline; + text-overflow: break; + } + &.expand-two p { width: calc(100% - 52px); - background-color: lightgray; + //text-decoration: underline; + //color: rgb(15, 57, 148); + //background-color: lightgray; } &.expand-three p { width: calc(100% - 84px); - background-color: lightgray; + //text-decoration: underline; + //color: rgb(15, 57, 148); + //background-color: lightgray; } } } } .linkMenu-item-buttons { + //@extend: right; position: absolute; top: 50%; right: 0; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 04cd83ee0..6af474513 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -26,6 +26,7 @@ interface LinkMenuItemProps { destinationDoc: Doc; showEditor: (linkDoc: Doc) => void; addDocTab: (document: Doc, where: string) => boolean; + menuRef: React.Ref<HTMLDivElement>; } // drag links and drop link targets (aliasing them if needed) @@ -77,6 +78,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { @action toggleShowMore(e: React.PointerEvent) { e.stopPropagation(); this._showMore = !this._showMore; } onEdit = (e: React.PointerEvent): void => { + LinkManager.currentLink = this.props.linkDoc; setupMoveUpEvents(this, e, this.editMoved, emptyFunction, () => this.props.showEditor(this.props.linkDoc)); } @@ -110,7 +112,8 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { document.removeEventListener("pointerup", this.onLinkButtonUp); document.addEventListener("pointerup", this.onLinkButtonUp); - if (this._buttonRef && this._buttonRef.current?.contains(e.target as any)) { + if (this._buttonRef && !!!this._buttonRef.current?.contains(e.target as any)) { + console.log("outside click"); LinkDocPreview.LinkInfo = undefined; } } @@ -147,7 +150,18 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { console.log("FOLLOWWW"); DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; - DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false); + + if (this.props.linkDoc.follow) { + if (this.props.linkDoc.follow === "Default") { + DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false); + } else if (this.props.linkDoc.follow === "Always open in right tab") { + this.props.addDocTab(this.props.destinationDoc, "onRight"); + } else if (this.props.linkDoc.follow === "Always open in new tab") { + this.props.addDocTab(this.props.destinationDoc, "inTab"); + } + } else { + DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false); + } } @action @@ -174,18 +188,24 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { Location: [e.clientX, e.clientY + 20] }))} onPointerDown={this.onLinkButtonDown}> - <p >{StrCast(this.props.destinationDoc.title)}</p> + + <div className="linkMenu-text"> + <p className="linkMenu-destination-title" + onPointerDown={this.followDefault}> + {StrCast(this.props.destinationDoc.title)}</p> + {this.props.linkDoc.description !== "" ? <p className="linkMenu-description"> + {StrCast(this.props.linkDoc.description)}</p> : null} </div> + <div className="linkMenu-item-buttons" ref={this._buttonRef} > {canExpand ? <div title="Show more" className="button" onPointerDown={e => this.toggleShowMore(e)}> <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>} - {/* <div title="Edit link" className="button" ref={this._editRef} onPointerDown={this.onEdit}> - <FontAwesomeIcon className="fa-icon" icon="pencil-alt" size="sm" /></div> */} + <div title="Edit link" className="button" ref={this._editRef} onPointerDown={this.onEdit}> + <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div> <div title="Delete link" className="button" onPointerDown={this.deleteLink}> <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /></div> - <div title="Follow link" className="button" onPointerDown={this.followDefault} onContextMenu={this.onContextMenu}> - <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /> - </div> + {/* <div title="Follow link" className="button" onPointerDown={this.followDefault} onContextMenu={this.onContextMenu}> + <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> */} </div> </div> {this._showMore ? this.renderMetadata() : <></>} diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index bfd860f65..7fb447cab 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -11,6 +11,8 @@ import { DocUtils } from "../../documents/Documents"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { LinkDocPreview } from "./LinkDocPreview"; import { LinkCreatedBox } from "./LinkCreatedBox"; +import { LinkDescriptionPopup } from "./LinkDescriptionPopup"; +import { LinkManager } from "../../util/LinkManager"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -54,6 +56,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp return false; } + onLinkButtonDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { if (doubleTap && this.props.InMenu) { @@ -87,15 +90,22 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp // Doc.UnBrushDoc(this.props.View.Document); // }); } else { - DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View && - DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); - - runInAction(() => { - LinkCreatedBox.popupX = e.screenX; - LinkCreatedBox.popupY = e.screenY - 120; - LinkCreatedBox.linkCreated = true; - setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); - }); + + if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) { + const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); + LinkManager.currentLink = linkDoc; + runInAction(() => { + LinkCreatedBox.popupX = e.screenX; + LinkCreatedBox.popupY = e.screenY - 133; + LinkCreatedBox.linkCreated = true; + + LinkDescriptionPopup.popupX = e.screenX; + LinkDescriptionPopup.popupY = e.screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; + + setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + }); + } } } })); @@ -109,15 +119,23 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp // Doc.UnBrushDoc(this.props.View.Document); // }); } else { - DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View && - DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); - - runInAction(() => { - LinkCreatedBox.popupX = e.screenX; - LinkCreatedBox.popupY = e.screenY - 120; - LinkCreatedBox.linkCreated = true; - setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); - }); + if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) { + const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); + LinkManager.currentLink = linkDoc; + runInAction(() => { + LinkCreatedBox.popupX = e.screenX; + LinkCreatedBox.popupY = e.screenY - 133; + LinkCreatedBox.linkCreated = true; + + if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { + LinkDescriptionPopup.popupX = e.screenX; + LinkDescriptionPopup.popupY = e.screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; + } + + setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + }); + } } } @@ -128,10 +146,14 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @computed get linkButton() { const links = DocListCast(this.props.View.props.Document.links); + + const title = this.props.InMenu ? "Drag or tap to create links" : "Tap to view links"; + return (!links.length || links[0].hidden) && !this.props.AlwaysOn ? (null) : - <div title="Drag(create link) Tap(view links)" ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}> + <div title={title} ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}> <div className={"documentLinksButton"} style={{ - backgroundColor: DocumentLinksButton.StartLink ? "transparent" : "", + backgroundColor: DocumentLinksButton.StartLink ? "transparent" : this.props.InMenu ? "black" : "", + color: this.props.InMenu ? "white" : "black", width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", fontWeight: "bold" }} onPointerDown={this.onLinkButtonDown} onClick={this.onLinkClick} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3311bfa33..97e3bc01c 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -43,6 +43,8 @@ import React = require("react"); import { DocumentLinksButton } from './DocumentLinksButton'; import { MobileInterface } from '../../../mobile/MobileInterface'; import { LinkCreatedBox } from './LinkCreatedBox'; +import { LinkDescriptionPopup } from './LinkDescriptionPopup'; +import { LinkManager } from '../../util/LinkManager'; library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, @@ -642,30 +644,46 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.stopPropagation(); de.complete.annoDragData.linkedToDoc = true; + const linkDoc = DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link"); + LinkManager.currentLink = linkDoc; + runInAction(() => { LinkCreatedBox.popupX = de.x; - LinkCreatedBox.popupY = de.y; + LinkCreatedBox.popupY = de.y - 33; LinkCreatedBox.linkCreated = true; + + LinkDescriptionPopup.popupX = de.x; + LinkDescriptionPopup.popupY = de.y; + LinkDescriptionPopup.descriptionPopup = true; + setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); }); - - DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link"); } if (de.complete.linkDragData) { e.stopPropagation(); // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); - runInAction(() => { - LinkCreatedBox.popupX = de.x; - LinkCreatedBox.popupY = de.y; - LinkCreatedBox.linkCreated = true; - setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); - }); + if (de.complete.linkDragData.linkSourceDocument !== this.props.Document) { + const linkDoc = DocUtils.MakeLink({ doc: de.complete.linkDragData.linkSourceDocument }, + { doc: this.props.Document }, `link`); + LinkManager.currentLink = linkDoc; + + de.complete.linkDragData.linkSourceDocument !== this.props.Document && + (de.complete.linkDragData.linkDocument = linkDoc); // TODODO this is where in text links get passed + runInAction(() => { + LinkCreatedBox.popupX = de.x; + LinkCreatedBox.popupY = de.y - 33; + LinkCreatedBox.linkCreated = true; + + LinkDescriptionPopup.popupX = de.x; + LinkDescriptionPopup.popupY = de.y; + LinkDescriptionPopup.descriptionPopup = true; + + setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + }); + } - de.complete.linkDragData.linkSourceDocument !== this.props.Document && - (de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: de.complete.linkDragData.linkSourceDocument }, - { doc: this.props.Document }, `link`)); // TODODO this is where in text links get passed } } diff --git a/src/client/views/nodes/LinkCreatedBox.tsx b/src/client/views/nodes/LinkCreatedBox.tsx index d157d3fca..648ae23c8 100644 --- a/src/client/views/nodes/LinkCreatedBox.tsx +++ b/src/client/views/nodes/LinkCreatedBox.tsx @@ -11,8 +11,8 @@ import { Fade } from "@material-ui/core"; export class LinkCreatedBox extends React.Component<{}> { @observable public static linkCreated: boolean = false; - @observable public static popupX: number = 600; - @observable public static popupY: number = 250; + @observable public static popupX: number = 500; + @observable public static popupY: number = 150; @action public static changeLinkCreated = () => { @@ -23,8 +23,8 @@ export class LinkCreatedBox extends React.Component<{}> { return <Fade in={LinkCreatedBox.linkCreated}> <div className="linkCreatedBox-fade" style={{ - left: LinkCreatedBox.popupX ? LinkCreatedBox.popupX : 600, - top: LinkCreatedBox.popupY ? LinkCreatedBox.popupY : 250, + left: LinkCreatedBox.popupX ? LinkCreatedBox.popupX : 500, + top: LinkCreatedBox.popupY ? LinkCreatedBox.popupY : 150, }}>Link Created</div> </Fade>; } diff --git a/src/client/views/nodes/LinkDescriptionPopup.scss b/src/client/views/nodes/LinkDescriptionPopup.scss new file mode 100644 index 000000000..54002fd1b --- /dev/null +++ b/src/client/views/nodes/LinkDescriptionPopup.scss @@ -0,0 +1,69 @@ +.linkDescriptionPopup { + + display: flex; + + border: 1px solid rgb(170, 26, 26); + + width: auto; + position: absolute; + + height: auto; + z-index: 10000; + border-radius: 10px; + font-size: 12px; + //white-space: nowrap; + + background-color: rgba(250, 250, 250, 0.95); + padding-top: 9px; + padding-bottom: 9px; + padding-left: 9px; + padding-right: 9px; + + .linkDescriptionPopup-input { + float: left; + background-color: rgba(250, 250, 250, 0.95); + color: rgb(100, 100, 100); + border: none; + min-width: 160px; + } + + .linkDescriptionPopup-btn { + + float: right; + + justify-content: center; + vertical-align: middle; + + + .linkDescriptionPopup-btn-dismiss { + background-color: white; + color: black; + display: inline; + right: 0; + border-radius: 10px; + border: 1px solid black; + padding: 3px; + font-size: 9px; + text-align: center; + position: relative; + margin-right: 4px; + justify-content: center; + } + + .linkDescriptionPopup-btn-add { + background-color: black; + color: white; + display: inline; + right: 0; + border-radius: 10px; + border: 1px solid black; + padding: 3px; + font-size: 9px; + text-align: center; + position: relative; + justify-content: center; + } + } + + +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx new file mode 100644 index 000000000..3bb52d9fb --- /dev/null +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -0,0 +1,73 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import "./LinkDescriptionPopup.scss"; +import { observable, action } from "mobx"; +import { EditableView } from "../EditableView"; +import { LinkManager } from "../../util/LinkManager"; +import { LinkCreatedBox } from "./LinkCreatedBox"; + + +@observer +export class LinkDescriptionPopup extends React.Component<{}> { + + @observable public static descriptionPopup: boolean = false; + @observable public static showDescriptions: string = "ON"; + @observable public static popupX: number = 700; + @observable public static popupY: number = 350; + @observable description: string = ""; + @observable popupRef = React.createRef<HTMLDivElement>(); + + @action + descriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this.description = e.currentTarget.value; + } + + @action + setDescription = () => { + if (LinkManager.currentLink) { + LinkManager.currentLink.description = this.description; + } + LinkDescriptionPopup.descriptionPopup = false; + } + + @action + onDismiss = () => { + LinkDescriptionPopup.descriptionPopup = false; + } + + @action + onClick = (e: PointerEvent) => { + if (this.popupRef && !!!this.popupRef.current?.contains(e.target as any)) { + LinkDescriptionPopup.descriptionPopup = false; + LinkCreatedBox.linkCreated = false; + } + } + + @action + componentDidMount() { + document.addEventListener("pointerdown", this.onClick); + } + + componentWillUnmount() { + document.removeEventListener("pointerdown", this.onClick); + } + + render() { + return <div className="linkDescriptionPopup" ref={this.popupRef} + style={{ + left: LinkDescriptionPopup.popupX ? LinkDescriptionPopup.popupX : 700, + top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350, + }}> + <input className="linkDescriptionPopup-input" + placeholder={"(optional) enter link label..."} + onChange={(e) => this.descriptionChanged(e)}> + </input> + <div className="linkDescriptionPopup-btn"> + <div className="linkDescriptionPopup-btn-dismiss" + onPointerDown={this.onDismiss}> Dismiss </div> + <div className="linkDescriptionPopup-btn-add" + onPointerDown={this.setDescription}> Add </div> + </div> + </div>; + } +}
\ No newline at end of file |