diff options
Diffstat (limited to 'src')
67 files changed, 1038 insertions, 1001 deletions
diff --git a/src/JSZipUtils.js b/src/JSZipUtils.js new file mode 100644 index 000000000..5ce1bd471 --- /dev/null +++ b/src/JSZipUtils.js @@ -0,0 +1,142 @@ +var JSZipUtils = {}; +// just use the responseText with xhr1, response with xhr2. +// The transformation doesn't throw away high-order byte (with responseText) +// because JSZip handles that case. If not used with JSZip, you may need to +// do it, see https://developer.mozilla.org/En/Using_XMLHttpRequest#Handling_binary_data +JSZipUtils._getBinaryFromXHR = function (xhr) { + // for xhr.responseText, the 0xFF mask is applied by JSZip + return xhr.response || xhr.responseText; +}; + +// taken from jQuery +function createStandardXHR() { + try { + return new window.XMLHttpRequest(); + } catch (e) {} +} + +function createActiveXHR() { + try { + return new window.ActiveXObject('Microsoft.XMLHTTP'); + } catch (e) {} +} + +// Create the request object +var createXHR = + typeof window !== 'undefined' && window.ActiveXObject + ? /* Microsoft failed to properly + * implement the XMLHttpRequest in IE7 (can't request local files), + * so we use the ActiveXObject when it is available + * Additionally XMLHttpRequest can be disabled in IE7/IE8 so + * we need a fallback. + */ + function () { + return createStandardXHR() || createActiveXHR(); + } + : // For all other browsers, use the standard XMLHttpRequest object + createStandardXHR; + +/** + * @param {string} path The path to the resource to GET. + * @param {function|{callback: function, progress: function}} options + * @return {Promise|undefined} If no callback is passed then a promise is returned + */ +JSZipUtils.getBinaryContent = function (path, options) { + var promise, resolve, reject; + var callback; + + if (!options) { + options = {}; + } + + // backward compatible callback + if (typeof options === 'function') { + callback = options; + options = {}; + } else if (typeof options.callback === 'function') { + // callback inside options object + callback = options.callback; + } + + if (!callback && typeof Promise !== 'undefined') { + promise = new Promise(function (_resolve, _reject) { + resolve = _resolve; + reject = _reject; + }); + } else { + resolve = function (data) { + callback(null, data); + }; + reject = function (err) { + callback(err, null); + }; + } + + /* + * Here is the tricky part : getting the data. + * In firefox/chrome/opera/... setting the mimeType to 'text/plain; charset=x-user-defined' + * is enough, the result is in the standard xhr.responseText. + * cf https://developer.mozilla.org/En/XMLHttpRequest/Using_XMLHttpRequest#Receiving_binary_data_in_older_browsers + * In IE <= 9, we must use (the IE only) attribute responseBody + * (for binary data, its content is different from responseText). + * In IE 10, the 'charset=x-user-defined' trick doesn't work, only the + * responseType will work : + * http://msdn.microsoft.com/en-us/library/ie/hh673569%28v=vs.85%29.aspx#Binary_Object_upload_and_download + * + * I'd like to use jQuery to avoid this XHR madness, but it doesn't support + * the responseType attribute : http://bugs.jquery.com/ticket/11461 + */ + try { + var xhr = createXHR(); + + xhr.open('GET', path, true); + + // recent browsers + if ('responseType' in xhr) { + xhr.responseType = 'arraybuffer'; + } + + // older browser + if (xhr.overrideMimeType) { + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + } + + xhr.onreadystatechange = function (event) { + // use `xhr` and not `this`... thanks IE + if (xhr.readyState === 4) { + if (xhr.status === 200 || xhr.status === 0) { + try { + resolve(JSZipUtils._getBinaryFromXHR(xhr)); + } catch (err) { + reject(new Error(err)); + } + } else { + reject(new Error('Ajax error for ' + path + ' : ' + this.status + ' ' + this.statusText)); + } + } + }; + + if (options.progress) { + xhr.onprogress = function (e) { + options.progress({ + path: path, + originalEvent: e, + percent: (e.loaded / e.total) * 100, + loaded: e.loaded, + total: e.total, + }); + }; + } + + xhr.send(); + } catch (e) { + reject(new Error(e), null); + } + + // returns a promise or undefined depending on whether a callback was + // provided + return promise; +}; + +// export +module.exports = JSZipUtils; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 422ee7711..0b6d4380d 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -200,6 +200,8 @@ export class DocumentOptions { 'icon-nativeWidth'?: NUMt = new NumInfo('native width of icon view'); 'icon-nativeHeight'?: NUMt = new NumInfo('native height of icon view'); 'dragFactory-count'?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)'); + openFactoryLocation?: string; // an OpenWhere value to place the factory created document + openFactoryAsDelegate?: boolean; // lat?: number; lng?: number; infoWindowOpen?: boolean; @@ -220,6 +222,8 @@ export class DocumentOptions { autoPlayAnchors?: boolean; // whether to play audio/video when an anchor is clicked in a stackedTimeline. dontPlayLinkOnSelect?: boolean; // whether an audio/video should start playing when a link is followed to it. toolTip?: string; // tooltip to display on hover + toolType?: string; // type of pen tool + expertMode?: boolean; // something available only in expert (not novice) mode contextMenuFilters?: List<ScriptField>; contextMenuScripts?: List<ScriptField>; contextMenuLabels?: List<string>; @@ -241,6 +245,7 @@ export class DocumentOptions { childContextMenuIcons?: List<string>; followLinkZoom?: boolean; // whether to zoom to the target of a link hideLinkButton?: boolean; // whether the blue link counter button should be hidden + disableDocBrushing?: boolean; // whether to suppress border highlighting hideDecorationTitle?: boolean; hideOpenButton?: boolean; hideResizeHandles?: boolean; @@ -579,8 +584,8 @@ export namespace Docs { [ DocumentType.FONTICON, { - layout: { view: FontIconBox, dataField: defaultDataKey }, - options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%' }, + layout: { view: FontIconBox, dataField: 'icon' }, + options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40 }, }, ], [ @@ -757,7 +762,6 @@ export namespace Docs { ...(template.options || {}), layout: layout.view?.LayoutString(layout.dataField), data: template.data, - layout_keyValue: KeyValueBox.LayoutString(''), }; Object.entries(options).map(pair => { if (typeof pair[1] === 'string' && pair[1].startsWith('@')) { @@ -1016,9 +1020,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), { lat, lng, infoWindowOpen, ...options }, id); } - export function KVPDocument(document: Doc, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options }); - } + // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView) + // export function KVPDocument(document: Doc, options: DocumentOptions = {}) { + // return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options }); + // } export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _viewType: CollectionViewType.Freeform }, id); @@ -1353,6 +1358,8 @@ export namespace DocUtils { const script = scripts[key]; if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) { doc[key] = ScriptField.MakeScript(script, { + self: Doc.name, + this: Doc.name, dragData: DragManager.DocumentDragData.name, value: 'any', _readOnly_: 'boolean', @@ -1679,6 +1686,7 @@ export namespace DocUtils { x: Cast(doc.x, 'number', null), y: Cast(doc.y, 'number', null), backgroundColor: '#ACCEF7', + hideAllLinks: true, _width: 15, _height: 15, _xPadding: 0, @@ -1862,11 +1870,8 @@ export namespace DocUtils { } ScriptingGlobals.add('Docs', Docs); -ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc) { - return DocUtils.copyDragFactory(dragFactory); -}); -ScriptingGlobals.add(function delegateDragFactory(dragFactory: Doc) { - return DocUtils.delegateDragFactory(dragFactory); +ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc, asDelegate?: boolean) { + return dragFactory instanceof Doc ? (asDelegate ? DocUtils.delegateDragFactory(dragFactory) : DocUtils.copyDragFactory(dragFactory)) : dragFactory; }); ScriptingGlobals.add(function makeDelegate(proto: any) { const d = Docs.Create.DelegateDocument(proto, { title: 'child of ' + proto.title }); diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts index 575c984f5..5e2baf924 100644 --- a/src/client/documents/Gitlike.ts +++ b/src/client/documents/Gitlike.ts @@ -1,118 +1,118 @@ -import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc"; -import { List } from "../../fields/List"; -import { Cast, DateCast } from "../../fields/Types"; -import { DateField } from "../../fields/DateField"; -import { Id } from "../../fields/FieldSymbols"; +// import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc"; +// import { List } from "../../fields/List"; +// import { Cast, DateCast } from "../../fields/Types"; +// import { DateField } from "../../fields/DateField"; +// import { Id } from "../../fields/FieldSymbols"; -// synchs matching documents on the two branches that are being merged/pulled -// currently this just synchs the main 'fieldKey' component of the data since -// we don't have individual timestamps for all fields -- this is a problematic design issue. -function GitlikeSynchDocs(bd: Doc, md: Doc) { - const fieldKey = Doc.LayoutFieldKey(md); - const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date; - const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date; - const bdproto = bd && Doc.GetProto(bd); - if (bdate !== mdate && bdate <= mdate) { - if (bdproto && md) { - bdproto[fieldKey] = Field.Copy(md[fieldKey]); - bdproto[`${fieldKey}-lastModified`] = new DateField(); - } - } - const bldate = DateCast(bd._lastModified)?.date; - const mldate = DateCast(md._lastModified)?.date; - if (bldate === mldate || bldate > mldate) return; - if (bdproto && md) { - bd.x = Field.Copy(md.x); - bd.y = Field.Copy(md.y); - bd.width = Field.Copy(md.width); - bd.height = Field.Copy(md.height); - bdproto._lastModified = new DateField(); - } -} +// // synchs matching documents on the two branches that are being merged/pulled +// // currently this just synchs the main 'fieldKey' component of the data since +// // we don't have individual timestamps for all fields -- this is a problematic design issue. +// function GitlikeSynchDocs(bd: Doc, md: Doc) { +// const fieldKey = Doc.LayoutFieldKey(md); +// const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date; +// const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date; +// const bdproto = bd && Doc.GetProto(bd); +// if (bdate !== mdate && bdate <= mdate) { +// if (bdproto && md) { +// bdproto[fieldKey] = Field.Copy(md[fieldKey]); +// bdproto[`${fieldKey}-lastModified`] = new DateField(); +// } +// } +// const bldate = DateCast(bd._lastModified)?.date; +// const mldate = DateCast(md._lastModified)?.date; +// if (bldate === mldate || bldate > mldate) return; +// if (bdproto && md) { +// bd.x = Field.Copy(md.x); +// bd.y = Field.Copy(md.y); +// bd.width = Field.Copy(md.width); +// bd.height = Field.Copy(md.height); +// bdproto._lastModified = new DateField(); +// } +// } -// pulls documents onto a branch from the branch's master -// if a document exists on master but not on the branch, it is branched and added -// NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp. -async function GitlikePullFromMaster(branch: Doc, suffix = "") { - const masterMain = Cast(branch.branchOf, Doc, null); - // get the set of documents on both the branch and master - const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]); - const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); - // get the master documents that correspond to the branch documents - const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd); - const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc)); - // get documents on master that don't have a corresponding master doc (form a branch doc), and ... - const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md))); - const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md))); - oldDocsFromMaster?.forEach(md => { - const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md); - bd && GitlikeSynchDocs(bd, md); - }); - const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch); - // make branch clones of them, then add them to the branch - const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []); - newlyBranchedDocs.forEach(nd => { - Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd); - nd.context = branch; - }); - // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted. - const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context); - // so then remove all the deleted main docs from this branch. - remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd)); -} +// // pulls documents onto a branch from the branch's master +// // if a document exists on master but not on the branch, it is branched and added +// // NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp. +// async function GitlikePullFromMaster(branch: Doc, suffix = "") { +// const masterMain = Cast(branch.branchOf, Doc, null); +// // get the set of documents on both the branch and master +// const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]); +// const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); +// // get the master documents that correspond to the branch documents +// const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd); +// const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc)); +// // get documents on master that don't have a corresponding master doc (form a branch doc), and ... +// const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md))); +// const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md))); +// oldDocsFromMaster?.forEach(md => { +// const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md); +// bd && GitlikeSynchDocs(bd, md); +// }); +// const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch); +// // make branch clones of them, then add them to the branch +// const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []); +// newlyBranchedDocs.forEach(nd => { +// Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd); +// nd.context = branch; +// }); +// // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted. +// const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context); +// // so then remove all the deleted main docs from this branch. +// remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd)); +// } -// merges all branches from the master branch by first merging the top-level collection of documents, -// and then merging all the annotations on those documents. -// TODO: need to add an incrementing timestamp whenever anything merges. don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp. -async function GitlikeMergeWithMaster(master: Doc, suffix = "") { - const branches = await DocListCastAsync(master.branches); - branches?.map(async branch => { - const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); - branchChildren && await Promise.all(branchChildren.map(async bd => { - const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch); - // see if the branch's child exists on master. - const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone; - // if the branch's child didn't exist on master, we make a branch clone of the child to add to master. - // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields - // on the branch child and master clone. - if (masterChild.branchOf) { - const branchDocProto = Doc.GetProto(bd); - const masterChildProto = Doc.GetProto(masterChild); - const branchTitle = bd.title; - branchDocProto.title = masterChildProto.title; - masterChildProto.title = branchTitle; - masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf' - masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child - Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list. - branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child - } - Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op) - masterChild.context = master; - GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd); - })); - const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]); - masterChildren?.forEach(mc => { // see if any master children - if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch. - Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it. - mc.context = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp - } - }); - }); -} +// // merges all branches from the master branch by first merging the top-level collection of documents, +// // and then merging all the annotations on those documents. +// // TODO: need to add an incrementing timestamp whenever anything merges. don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp. +// async function GitlikeMergeWithMaster(master: Doc, suffix = "") { +// const branches = await DocListCastAsync(master.branches); +// branches?.map(async branch => { +// const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); +// branchChildren && await Promise.all(branchChildren.map(async bd => { +// const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch); +// // see if the branch's child exists on master. +// const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone; +// // if the branch's child didn't exist on master, we make a branch clone of the child to add to master. +// // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields +// // on the branch child and master clone. +// if (masterChild.branchOf) { +// const branchDocProto = Doc.GetProto(bd); +// const masterChildProto = Doc.GetProto(masterChild); +// const branchTitle = bd.title; +// branchDocProto.title = masterChildProto.title; +// masterChildProto.title = branchTitle; +// masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf' +// masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child +// Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list. +// branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child +// } +// Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op) +// masterChild.context = master; +// GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd); +// })); +// const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]); +// masterChildren?.forEach(mc => { // see if any master children +// if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch. +// Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it. +// mc.context = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp +// } +// }); +// }); +// } -// performs a "git"-like task: pull or merge -// if pull, then target is a specific branch document that will be updated from its associated master -// if merge, then target is the master doc that will merge in all branches associated with it. -// TODO: parameterize 'merge' to specify which branch(es) should be merged. -// extend 'merge' to allow a specific branch to be merge target (not just master); -// make pull/merge be recursive (ie, this func currently just operates on the main doc and its children) -export async function BranchTask(target: Doc, action: "pull" | "merge") { - const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster; - await func(target, ""); - await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations")); - await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar")); -} +// // performs a "git"-like task: pull or merge +// // if pull, then target is a specific branch document that will be updated from its associated master +// // if merge, then target is the master doc that will merge in all branches associated with it. +// // TODO: parameterize 'merge' to specify which branch(es) should be merged. +// // extend 'merge' to allow a specific branch to be merge target (not just master); +// // make pull/merge be recursive (ie, this func currently just operates on the main doc and its children) +// export async function BranchTask(target: Doc, action: "pull" | "merge") { +// const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster; +// await func(target, ""); +// await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations")); +// await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar")); +// } -export async function BranchCreate(target: Doc) { - return (await Doc.MakeClone(target, false, true)).clone; -}
\ No newline at end of file +// export async function BranchCreate(target: Doc) { +// return (await Doc.MakeClone(target, false, true)).clone; +// } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index c4fb4788c..968c7a79b 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -21,6 +21,7 @@ import { DashboardView } from "../views/DashboardView"; import { Colors } from "../views/global/globalEnums"; import { MainView } from "../views/MainView"; import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox"; +import { OpenWhere } from "../views/nodes/DocumentView"; import { OverlayView } from "../views/OverlayView"; import { DragManager } from "./DragManager"; import { MakeTemplate } from "./DropConverter"; @@ -40,6 +41,8 @@ interface Button { numBtnMax?: number; switchToggle?: boolean; width?: number; + toolType?: string; // type of pen tool + expertMode?: boolean;// available only in expert mode btnList?: List<string>; ignoreClick?: boolean; buttonText?: string; @@ -78,7 +81,7 @@ export class CurrentUserUtils { ]; const requiredTypes = requiredTypeNameFields.map(({ btnOpts, template, templateOpts }) => { const tempBtn = DocListCast(tempDocs?.data)?.find(doc => doc.title === btnOpts.title); - const reqdScripts = { onDragStart: '{ return copyDragFactory(this.dragFactory); }' }; + const reqdScripts = { onDragStart: '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }' }; const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => { if (templateBtn) { DocUtils.AssignOpts(templateBtn,btnOpts); @@ -105,7 +108,7 @@ export class CurrentUserUtils { const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, system: true}; const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self])))"}, - { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: "openOnRight(self.doubleClickView)"}]; + { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: `openDoc(self.doubleClickView.${OpenWhere.addRight})`}]; const reqdClickList = reqdTempOpts.map(opts => { const allOpts = {...reqdClickOpts, ...opts.opts}; const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined; @@ -121,11 +124,11 @@ export class CurrentUserUtils { const tempClicks = DocCast(doc[field]); const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, system: true}; const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ - { opts: { title: "onClick"}, script: "console.log( 'click')"}, - { opts: { title: "onDoubleClick"}, script: "console.log( 'double click')"}, - { opts: { title: "onChildClick"}, script: "console.log( 'child click')"}, - { opts: { title: "onChildDoubleClick"}, script: "console.log( 'child double click')"}, - { opts: { title: "onCheckedClick"}, script: "console.log( heading, checked, containingTreeView)"}, + { opts: { title: "onClick"}, script: "console.log('click')"}, + { opts: { title: "onDoubleClick"}, script: "console.log('click')"}, + { opts: { title: "onChildClick"}, script: "console.log('click')"}, + { opts: { title: "onChildDoubleClick"}, script: "console.log('click')"}, + { opts: { title: "onCheckedClick"}, script: "console.log(heading, checked, containingTreeView)"}, ]; const reqdClickList = reqdTempOpts.map(opts => { const allOpts = {...reqdClickOpts, ...opts.opts}; @@ -145,7 +148,7 @@ export class CurrentUserUtils { { noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" }, { noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }]; const reqdNoteList = reqdTempOpts.map(opts => { - const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true, system: true}; + const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true}; const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined; return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note"); }); @@ -183,7 +186,7 @@ export class CurrentUserUtils { const allopts = {system: true, ...opts}; return DocUtils.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ? DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))), - {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView"}); + {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)"}); }; const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({ textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts @@ -211,7 +214,7 @@ export class CurrentUserUtils { /// initalizes the set of "empty<DocType>" versions of each document type with default fields. e.g.,. emptyNote, emptyTrail static creatorBtnDescriptors(doc: Doc): { title: string, toolTip: string, icon: string, ignoreClick?: boolean, dragFactory?: Doc, - backgroundColor?: string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string}, + backgroundColor?: string, openFactoryAsDelegate?:boolean, openFactoryLocation?:string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string}, }[] { const standardOps = (key:string) => ({ title : "Untitled "+ key, _fitWidth: false, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) }); const json = { @@ -221,11 +224,11 @@ export class CurrentUserUtils { { type: "paragraph", attrs: {}, content: [{ type: "dashField", - attrs: { fieldKey: "author", docid: "", hideKey: false }, + attrs: { fieldKey: "author", docId: "", hideKey: false }, marks: [{ type: "strong" }] }, { type: "dashField", - attrs: { fieldKey: "creationDate", docid: "", hideKey: false }, + attrs: { fieldKey: "creationDate", docId: "", hideKey: false }, marks: [{ type: "strong" }] }] }] @@ -257,13 +260,13 @@ export class CurrentUserUtils { creator:(opts:DocumentOptions)=> any // how to create the empty thing if it doesn't exist }[] = [ {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _autoHeight: true }}, - {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200 }}, + {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _fitWidth: true}}, {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _fitWidth: true }}, {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _backgroundGridShow: true, }}, {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }}, {key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }}, {key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }}, - {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _showSidebar: true, }}, + {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _fitWidth: true, _showSidebar: true, }}, {key: "Screengrab", creator: Docs.Create.ScreenshotDocument, opts: { _width: 400, _height: 200 }}, {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }}, {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, _isLinkButton: true }}, @@ -282,30 +285,34 @@ export class CurrentUserUtils { emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, undefined, thing.funcs)); return [ - { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, }, - { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, }, - { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab), scripts: { onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, }, - { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, }, - { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, }, - { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, }, - { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, }, - { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, }, - { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'} }, - { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'}}, - { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, funcs: { hidden: 'IsNoviceMode()'} }, - { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, funcs: { hidden: 'IsNoviceMode()'}}, - { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, }, - { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, funcs: { hidden: 'IsNoviceMode()'}}, - { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc,scripts: { onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'}, }, - { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", scripts: { onClick: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' } }, - ].map(tuple => ({scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, ...tuple, })) + { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, clickFactory: DocCast(doc.emptyNote)}, + { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)}, + { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc,clickFactory: DocCast(doc.emptyTab)}, + { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, clickFactory: DocCast(doc.emptyEquation)}, + { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)}, + { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc,clickFactory: DocCast(doc.emptyComparison)}, + { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, clickFactory: DocCast(doc.emptyAudio), openFactoryLocation: OpenWhere.overlay}, + { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)}, + { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc,clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay}, + { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, clickFactory: DocCast(doc.emptyWebCam), openFactoryLocation: OpenWhere.overlay}, + { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)}, + { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript)}, + { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, + { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay}, + { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true }, + { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", clickFactory: "repl" as any, openFactoryLocation: OpenWhere.overlay}, + ].map(tuple => ( + { openFactoryLocation: OpenWhere.addRight, + scripts: { onClick: 'openDoc(copyDragFactory(this.clickFactory,this.openFactoryAsDelegate), this.openFactoryLocation)', + onDragStart: '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }'}, + ...tuple, })) } /// Initalizes the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools static setupCreatorButtons(doc: Doc, dragCreatorDoc?:Doc):Doc { const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => { const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined; - const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit, + const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit, _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true, _removeDropProperties: new List<string>(["_stayInCollection"]), @@ -601,55 +608,59 @@ export class CurrentUserUtils { return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); } + static zTools(): Button[] { + return [ + { title: "Bottom", icon: "arrows-down-to-line", toolTip: "Make doc topmost",btnType: ButtonType.ClickButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform + { title: "Top", icon: "arrows-up-to-line", toolTip: "Make doc bottommost",btnType: ButtonType.ClickButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform + { title: "Z order", icon: "z", toolTip: "Bring Forward on Drag",btnType: ButtonType.ToggleButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {}, scripts: { onClick: 'toggleRaiseOnDrag(_readOnly_)'}}, // Only when floating document is selected in freeform + ] + } static textTools():Button[] { return [ - { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, scripts: {script: 'setFont(value, _readOnly_)'}, + { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) }, - { title: "Size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setFontSize(value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions }, - { title: "Color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, scripts: {script: '{ return setFontColor(value, _readOnly_);}'}}, - { title: "Highlight",toolTip:"Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", ignoreClick: true, scripts: {script: '{ return setFontHighlight(value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} }, - { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", scripts: {onClick: '{ return toggleBold(_readOnly_); }'} }, - { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", scripts: {onClick: '{ return toggleItalic(_readOnly_);}'} }, - { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", scripts: {onClick: '{ return toggleUnderline(_readOnly_);}'} }, - { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", scripts: {onClick: '{ return setBulletList("bullet", _readOnly_);}'} }, - { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", scripts: {onClick: '{ return setBulletList("decimal", _readOnly_);}'} }, - + { title: "Size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions }, + { title: "Color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}}, + { title: "Highlight",toolTip:"Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} }, + { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }}, + { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}}, + { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}}, // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}}, // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}}, // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}}, - { title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }}, - { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} }, - { title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} }, - { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}}, - { title: "Dictate",toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}}, - ]; + ]; } static inkTools():Button[] { return [ - { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", scripts: {onClick:'{ return setActiveTool("pen", false, _readOnly_);}' }}, - { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", scripts: {onClick:'{ return setActiveTool("write", false, _readOnly_);}'} }, - { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", scripts: {onClick:'{ return setActiveTool("eraser", false, _readOnly_);}' }}, - // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} }, - { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", true, _readOnly_);}`} }, - { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", true, _readOnly_);}`} }, - { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", true, _readOnly_);}`} }, - { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle", scripts: {onClick:'{ return setIsInkMask(_readOnly_);}'} }, - { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip",ignoreClick: true, scripts: {script: '{ return setFillColor(value, _readOnly_);}'} }, - { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1}, - { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, scripts: {script: '{ return setStrokeColor(value, _readOnly_);}'} }, + { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }}, + { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}'} }, + { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", toolType: "eraser", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }}, + { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType:GestureUtils.Gestures.Circle, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, + { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType:GestureUtils.Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, + { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType:GestureUtils.Gestures.Line, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, + { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(self.toolType, value, _readOnly_);}'} }, + { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip", toolType: "fillColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'} }, + { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1}, + { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'} }, ]; } static schemaTools():Button[] { - return [{ title: "Show preview", toolTip: "Show preview of selected document", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{return toggleSchemaPreview(_readOnly_);}'}, }]; + return [{ title: "Show preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'}, }]; } static webTools() { return [ { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", scripts: { onClick: '{ return webBack(_readOnly_); }' }}, { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", scripts: { onClick: '{ return webForward(_readOnly_); }'}}, - //{ title: "Reload", toolTip: "Reload webpage", btnType: ButtonType.ClickButton, icon: "redo-alt", click: 'webReload()' }, { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} }, ]; } @@ -661,18 +672,19 @@ export class CurrentUserUtils { CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel, CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, CollectionViewType.Grid, CollectionViewType.NoteTaking]), - title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, - { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}}, - { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, - { title: "Num",icon: "",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, - { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, - { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected - { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}}, - { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform", true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform - { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.RTF}")`} }, // Always available - { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.INK}")`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available - { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), funcs: {hidden: `!SelectionManager_selectedDocType("${DocumentType.WEB}")`, linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.WEB}")`, } }, // Only when Web is selected - { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), funcs: {hidden: `!SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`, linearViewIsExpanded: `SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected + title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, + { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, toolType:"tab", funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}}, + { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, toolType:"tab", ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected + { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: false, toolType:"tab", ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}}, + { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform + { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, + { title: "Num", icon:"",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, + { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, + { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available + { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available + { title: "Z", icon: "z", toolTip: "Z order functions", subMenu: CurrentUserUtils.zTools(), expertMode: false, toolType:"tab", funcs: { linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available + { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected + { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected ]; } @@ -684,6 +696,7 @@ export class CurrentUserUtils { color: Colors.WHITE, system: true, dontUndo: true, _nativeWidth: params.width ?? 30, _width: params.width ?? 30, _height: 30, _nativeHeight: 30, + toolType: params.toolType, expertMode: params.expertMode, _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true, _removeDropProperties: new List<string>([ "_stayInCollection"]), }; diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 203d4ad62..e116ae14b 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -324,16 +324,6 @@ export namespace DictationManager { ], [ - 'open fields', - { - action: (target: DocumentView) => { - const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 }); - target.props.addDocTab(kvp, OpenWhere.addRight); - }, - }, - ], - - [ 'new outline', { action: (target: DocumentView) => { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index f2c554866..947613801 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -265,6 +265,7 @@ export class DocumentManager { let rootContextView = await new Promise<DocumentView>(res => { const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc)); if (viewIndex !== -1) return res(this.getDocumentView(docContextPath[viewIndex])!); + options.didMove = true; docContextPath.some(doc => TabDocView.Activate(doc)) || MainView.addDocTabFunc(docContextPath[0], options.openLocation as OpenWhere); this.AddViewRenderedCb(docContextPath[0], dv => res(dv)); }); @@ -299,7 +300,7 @@ export class DocumentManager { PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500); Doc.linkFollowHighlight(docView.rootDoc, undefined, options.effect); if (options.playAudio) DocumentManager.playAudioAnno(docView.rootDoc); - if (options.toggleTarget) docView.rootDoc.hidden = !docView.rootDoc.hidden; + if (options.toggleTarget && (!options.didMove || docView.rootDoc.hidden)) docView.rootDoc.hidden = !docView.rootDoc.hidden; if (options.effect) docView.rootDoc[AnimationSym] = options.effect; if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && viewSpec.textHtml) { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 1b413c73b..c35941ee5 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -10,7 +10,9 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../fields/Types import { emptyFunction, Utils } from '../../Utils'; import { Docs, DocUtils } from '../documents/Documents'; import * as globalCssVariables from '../views/global/globalCssVariables.scss'; +import { Colors } from '../views/global/globalEnums'; import { DocumentView } from '../views/nodes/DocumentView'; +import { ScriptingGlobals } from './ScriptingGlobals'; import { SnappingManager } from './SnappingManager'; import { UndoManager } from './UndoManager'; @@ -594,3 +596,8 @@ export namespace DragManager { endDrag?.(); } } + +ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) { + if (readOnly) return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE : 'transparent'; + DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged()); +}); diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index eacbcc0e3..785018990 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -27,7 +27,7 @@ export class LinkFollower { // follows a link - if the target is on screen, it highlights/pans to it. // if the target isn't onscreen, then it will open up the target in the lightbox, or in place // depending on the followLinkLocation property of the source (or the link itself as a fallback); - public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean) => { + public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => { const batch = UndoManager.StartBatch('follow link click'); runInAction(() => (DocumentDecorations.Instance.overrideBounds = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value LinkFollower.traverseLink( diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 6dcdcb71b..f17a98616 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -7,7 +7,9 @@ import * as typescriptlib from '!!raw-loader!./type_decls.d'; import * as ts from 'typescript'; import { Doc, Field } from '../../fields/Doc'; +import { ToScriptString } from '../../fields/FieldSymbols'; import { ObjectField } from '../../fields/ObjectField'; +import { RefField } from '../../fields/RefField'; import { ScriptField } from '../../fields/ScriptField'; import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals'; export { ts }; @@ -180,7 +182,11 @@ function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, inde export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { const captured = options.capturedVariables ?? {}; - const signature = Object.keys(captured).reduce((p, v) => p + `${v}=${captured[v] instanceof ObjectField ? 'XXX' : captured[v].toString()}`, ''); + const signature = Object.keys(captured).reduce((p, v) => { + const formatCapture = (obj: any) => `${v}=${obj instanceof RefField ? 'XXX' : obj.toString()}`; + if (captured[v] instanceof Array) return p + (captured[v] as any).map(formatCapture); + return p + formatCapture(captured[v]); + }, ''); const found = ScriptField.GetScriptFieldCache(script + ':' + signature); if (found) return found as CompiledScript; const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; @@ -248,8 +254,10 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp ScriptingGlobals.resetScriptingGlobals(); } !signature.includes('XXX') && ScriptField._scriptFieldCache.set(script + ':' + signature, result as CompiledScript); - //console.log('COMPILED: ' + script + ':' + signature); return result; } ScriptingGlobals.add(CompileScript); +ScriptingGlobals.add(function runScript(self: Doc, script: ScriptField) { + return script?.script.run({ this: self, self: self }).result; +}); diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index c0fc25376..0f4f77588 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -104,10 +104,11 @@ export namespace SelectionManager { return Array.from(manager.SelectedViews.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking); } } -ScriptingGlobals.add(function SelectionManager_selectedDocType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) { - if (colType === ('tab' as any)) { +ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, expertMode: boolean, checkContext?: boolean) { + if (Doc.noviceMode && expertMode) return false; + if (type === 'tab') { return SelectionManager.Views().lastElement()?.props.renderDepth === 0; } let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement()); - return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true; + return selected?.type === type || selected?.viewType === type || !type; }); diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 2d1f61cfb..76037a7e9 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -44,12 +44,8 @@ export namespace SerializationHelper { } if (!obj.__type) { - if (true || ClientUtils.RELEASE) { - console.warn("No property 'type' found in JSON."); - return undefined; - } else { - throw Error("No property 'type' found in JSON."); - } + console.warn("No property 'type' found in JSON."); + return undefined; } if (!(obj.__type in serializationTypes)) { @@ -58,9 +54,8 @@ export namespace SerializationHelper { const type = serializationTypes[obj.__type]; const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result))); - if (type.afterDeserialize) { - type.afterDeserialize(value); - } + type.afterDeserialize?.(value); + return value; } } @@ -68,75 +63,20 @@ export namespace SerializationHelper { const serializationTypes: { [name: string]: { ctor: { new (): any }; afterDeserialize?: (obj: any) => void | Promise<any> } } = {}; const reverseMap: { [ctor: string]: string } = {}; -export interface DeserializableOpts { - (constructor: { new (...args: any[]): any }): void; - withFields(fields: string[]): Function; -} - -export function Deserializable(name: string, afterDeserialize?: (obj: any) => void | Promise<any>): DeserializableOpts; -export function Deserializable(constructor: { new (...args: any[]): any }): void; -export function Deserializable(constructor: { new (...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void { - function addToMap(name: string, ctor: { new (...args: any[]): any }) { +export function Deserializable(className: string, afterDeserialize?: (obj: any) => void | Promise<any>, constructorArgs?: [string]): (constructor: { new (...args: any[]): any }) => void { + function addToMap(className: string, ctor: { new (...args: any[]): any }) { const schema = getDefaultModelSchema(ctor) as any; - if (schema.targetClass !== ctor) { - const newSchema = { ...schema, factory: () => new ctor() }; - setDefaultModelSchema(ctor, newSchema); + if (schema.targetClass !== ctor || constructorArgs) { + setDefaultModelSchema(ctor, { ...schema, factory: (context: any) => new ctor(...(constructorArgs ?? [])?.map(arg => context.json[arg])) }); } - if (!(name in serializationTypes)) { - serializationTypes[name] = { ctor, afterDeserialize }; - reverseMap[ctor.name] = name; + if (!(className in serializationTypes)) { + serializationTypes[className] = { ctor, afterDeserialize }; + reverseMap[ctor.name] = className; } else { - throw new Error(`Name ${name} has already been registered as deserializable`); + throw new Error(`Name ${className} has already been registered as deserializable`); } } - if (typeof constructor === 'string') { - return Object.assign( - (ctor: { new (...args: any[]): any }) => { - addToMap(constructor, ctor); - }, - { withFields: (fields: string[]) => Deserializable.withFields(fields, constructor, afterDeserialize) } - ); - } - addToMap(constructor.name, constructor); -} - -export namespace Deserializable { - export function withFields(fields: string[], name?: string, afterDeserialize?: (obj: any) => void | Promise<any>) { - return function (constructor: { new (...fields: any[]): any }) { - Deserializable(name || constructor.name, afterDeserialize)(constructor); - let schema = getDefaultModelSchema(constructor); - if (schema) { - schema.factory = context => { - const args = fields.map(key => context.json[key]); - return new constructor(...args); - }; - // TODO A modified version of this would let us not reassign fields that we're passing into the constructor later on in deserializing - // fields.forEach(field => { - // if (field in schema.props) { - // let propSchema = schema.props[field]; - // if (propSchema === false) { - // return; - // } else if (propSchema === true) { - // propSchema = primitive(); - // } - // schema.props[field] = custom(propSchema.serializer, - // () => { - // return SKIP; - // }); - // } - // }); - } else { - schema = { - props: {}, - factory: context => { - const args = fields.map(key => context.json[key]); - return new constructor(...args); - }, - }; - setDefaultModelSchema(constructor, schema); - } - }; - } + return (ctor: { new (...args: any[]): any }) => addToMap(className, ctor); } export function autoObject(): PropSchema { diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 6c823e80a..396d754b6 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -184,10 +184,6 @@ export class SettingsManager extends React.Component<{}> { <div className="preferences-check">Show full toolbar</div> </div> <div> - <input type="checkbox" onChange={e => DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged())} checked={DragManager.GetRaiseWhenDragged()} /> - <div className="preferences-check">Raise on drag</div> - </div> - <div> <input type="checkbox" onChange={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} checked={FontIconBox.GetShowLabels()} /> <div className="preferences-check">Show button labels</div> </div> diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 7ebe8d0e3..2b586b0e2 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -233,7 +233,6 @@ export class DashboardView extends React.Component { /// this also sets the readonly state of the dashboard based on the current mode of dash (from its url) public static openDashboard = (doc: Doc | undefined, fromHistory = false) => { if (!doc) return false; - Doc.MainDocId = doc[Id]; Doc.AddDocToList(Doc.MyDashboards, 'data', doc); // this has the side-effect of setting the main container since we're assigning the active/guest dashboard diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 7c81d92d4..0b92fd864 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -16,6 +16,7 @@ import { Touchable } from './Touchable'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) export interface DocComponentProps { Document: Doc; + fieldKey?: string; LayoutTemplate?: () => Opt<Doc>; LayoutTemplateString?: string; } @@ -37,6 +38,10 @@ export function DocComponent<P extends DocComponentProps>() { @computed get dataDoc() { return this.props.Document[DataSym] as Doc; } + // key where data is stored + @computed get fieldKey() { + return this.props.fieldKey; + } protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index af868ba9c..9389fed01 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -518,7 +518,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV ) }> <div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onTemplateButton}> - {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} + <FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" /> </div> </Flyout> </div> diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 6058eaaf9..0feccb742 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -94,7 +94,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { } static setupThumbButtons(doc: Doc) { - const docProtoData: { title: string; icon: string; drag?: string; ignoreClick?: boolean; pointerDown?: string; pointerUp?: string; clipboard?: Doc; backgroundColor?: string; dragFactory?: Doc }[] = [ + const docProtoData: { title: string; icon: string; drag?: string; toolType?: string; ignoreClick?: boolean; pointerDown?: string; pointerUp?: string; clipboard?: Doc; backgroundColor?: string; dragFactory?: Doc }[] = [ { title: 'use pen', icon: 'pen-nib', pointerUp: 'resetPen()', pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: 'blue' }, { title: 'use highlighter', icon: 'highlighter', pointerUp: 'resetPen()', pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: 'yellow' }, { @@ -105,8 +105,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300, system: true }), backgroundColor: 'orange', }, - { title: 'interpret text', icon: 'font', pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: 'orange' }, - { title: 'ignore gestures', icon: 'signature', pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: 'green' }, + { title: 'interpret text', icon: 'font', toolType: 'inktotext', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'orange' }, + { title: 'ignore gestures', icon: 'signature', toolType: 'ignoregesture', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'green' }, ]; return docProtoData.map(data => Docs.Create.FontIconDocument({ @@ -116,6 +116,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { _height: 10, title: data.title, icon: data.icon, + toolType: data.toolType, _dropAction: data.pointerDown ? 'copy' : undefined, ignoreClick: data.ignoreClick, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 3fbf6e445..0f8b46dbe 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -372,7 +372,7 @@ export class KeyManager { list.push(doc); } if (count === docids.length) { - const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d)).clone : d))); + const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d, true)).clone : d))); if (added.length) { added.map(doc => (doc.context = targetDataDoc)); undoBatch(() => { diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 4f08a8e22..3861331b5 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -418,7 +418,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { inkScaleX, inkScaleY, '', - this.props.pointerEvents?.() ?? (this.rootDoc._lockedPosition ? 'none' : 'visiblepainted'), + this.props.pointerEvents?.() ?? 'visiblepainted', 0.0, false, downHdlr, diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 945cd61db..e755f204b 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -10,7 +10,7 @@ import 'normalize.css'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { ScriptField } from '../../fields/ScriptField'; -import { DocCast, StrCast } from '../../fields/Types'; +import { StrCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; @@ -57,7 +57,6 @@ import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import { LinkDocPreview } from './nodes/LinkDocPreview'; import { RadialMenu } from './nodes/RadialMenu'; import { TaskCompletionBox } from './nodes/TaskCompletedBox'; -import { PresBox } from './nodes/trails'; import { OverlayView } from './OverlayView'; import { AnchorMenu } from './pdf/AnchorMenu'; import { PreviewCursor } from './PreviewCursor'; @@ -230,8 +229,6 @@ export class MainView extends React.Component { if (window.location.pathname !== '/home') { const pathname = window.location.pathname.substr(1).split('/'); if (pathname.length > 1 && pathname[0] === 'doc') { - Doc.MainDocId = pathname[1]; - //!this.userDoc && DocServer.GetRefField(pathname[1]).then( action(field => { if (field instanceof Doc && field._viewType !== CollectionViewType.Docking) { @@ -480,9 +477,12 @@ export class MainView extends React.Component { fa.faHighlighter, fa.faRemoveFormat, fa.faHandPointUp, + fa.faXRay, + fa.faZ, + fa.faArrowsUpToLine, + fa.faArrowsDownToLine, ] ); - this.initAuthenticationRouters(); } globalPointerDown = action((e: PointerEvent) => { @@ -521,23 +521,6 @@ export class MainView extends React.Component { document.oncontextmenu = () => false; }; - initAuthenticationRouters = async () => { - const received = Doc.MainDocId; - if (received && !this.userDoc) { - reaction( - () => Doc.GuestTarget, - target => target && DashboardView.createNewDashboard(), - { fireImmediately: true } - ); - } - // else { - // PromiseValue(this.userDoc.activeDashboard).then(dash => { - // if (dash instanceof Doc) DashboardView.openDashboard(dash); - // else Doc.createNewDashboard(); - // }); - // } - }; - @action createNewPresentation = () => { const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true); diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 08285ff0c..34e8cd6dd 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -255,4 +255,5 @@ export class OverlayView extends React.Component { // bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error ScriptingGlobals.add(function addOverlayWindow(type: string, options: OverlayElementOptions) { OverlayView.Instance.addWindow(<ScriptingRepl />, options); + addOverlayWindow('ScriptingRepl', { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); }); diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 3712fff58..95ae65d7a 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -81,14 +81,8 @@ export class PreviewCursor extends React.Component<{}> { const batch = UndoManager.StartBatch('cloning'); { - const docs = await Promise.all( - docids - .filter((did, i) => i) - .map(async did => { - const doc = Cast(await DocServer.GetRefField(did), Doc, null); - return clone ? (await Doc.MakeClone(doc)).clone : doc; - }) - ); + const toCopy = await Promise.all(docids.slice(1).map(async did => Cast(await DocServer.GetRefField(did), Doc, null))); + const docs = clone ? (await Promise.all(Doc.MakeClones(toCopy, false))).map(res => res.clone) : toCopy; const firstx = docs.length ? NumCast(docs[0].x) + ptx - newPoint[0] : 0; const firsty = docs.length ? NumCast(docs[0].y) + pty - newPoint[1] : 0; docs.map(doc => { diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index ebbe20077..11e9dd9c9 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -133,7 +133,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { const containerDoc = dv.rootDoc; //containerDoc.followAllLinks = // containerDoc.noShadow = - // containerDoc.noHighlighting = + // containerDoc.disableDocBrushing = // containerDoc._forceActive = containerDoc._fitContentsToBox = containerDoc._isLightbox = !containerDoc._isLightbox; containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 03b4100a7..f3a5a5393 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -1678,8 +1678,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-input inline"> <p>Center Target (no zoom)</p> <button - style={{ background: this.sourceAnchor?.followLinkZoomScale !== 0 ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomScale', this.sourceAnchor, 0, 0.5, val => !val && this.sourceAnchor && (this.sourceAnchor.followLinkZoom = true))} + style={{ background: this.sourceAnchor?.followLinkZoom ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)} onClick={e => e.stopPropagation()} className="propertiesButton"> <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> @@ -1687,7 +1687,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}> <p>Zoom %</p> - <div className="ribbon-property" style={{ display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'inline-flex' }}> + <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}> <input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} /> <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}> <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}> @@ -1706,11 +1706,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> </button> </div> - {!targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)} + {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)} <div className={'slider-headers'} style={{ - display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'grid', + display: !targZoom ? 'none' : 'grid', justifyContent: 'space-between', width: `calc(100% - ${indent * 2}px)`, marginLeft: indent, diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 7519cbb05..02dd15960 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -84,7 +84,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { Doc.GetProto(target)[key] = val; return { type: 'dashField', - attrs: { fieldKey: key, docid: '', hideKey: false, editable: true }, + attrs: { fieldKey: key, docId: '', hideKey: false, editable: true }, marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } }], }; }); @@ -111,7 +111,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { { type: 'pFontSize', attrs: { fontSize: '8px' } }, { type: 'em' }, ], - attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false }, + attrs: { fieldKey: 'text', docId: anchor[Id], hideKey: true, editable: false }, }, ], }, diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index ce764c7bf..d1e85a65b 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -5,7 +5,7 @@ import { action, runInAction } from 'mobx'; import { extname } from 'path'; import { Doc, Opt } from '../../fields/Doc'; import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../fields/Types'; -import { DashColor, emptyFunction, lightOrDark } from '../../Utils'; +import { DashColor, lightOrDark } from '../../Utils'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { DocFocusOrOpen } from '../util/DocumentManager'; import { LinkManager } from '../util/LinkManager'; @@ -21,6 +21,7 @@ import { FieldViewProps } from './nodes/FieldView'; import { SliderBox } from './nodes/SliderBox'; import './StyleProvider.scss'; import React = require('react'); +import { KeyValueBox } from './nodes/KeyValueBox'; export enum StyleProp { TreeViewIcon = 'treeViewIcon', @@ -89,7 +90,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps const isAnnotated = property.includes(':annotated'); const isOpen = property.includes(':open'); const fieldKey = props?.fieldKey ? props.fieldKey + '-' : isCaption ? 'caption-' : ''; - const isBackground = () => doc && BoolCast(doc._lockedPosition); + const lockedPosition = () => doc && BoolCast(doc._lockedPosition); const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor); const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); const showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); @@ -112,16 +113,15 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' }; return allSorts; case StyleProp.Highlighting: - if (doc && !doc.noHighlighting) { - const highlightIndex = Doc.isBrushedHighlightedDegree(doc); - const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; + if (doc && !doc.disableDocBrushing && !props?.disableDocBrushing) { + const selected = SelectionManager.Views().some(dv => dv.rootDoc === doc); + const highlightIndex = Doc.isBrushedHighlightedDegree(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0); + const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? 'black' : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex]; - const excludeTypes = [DocumentType.FONTICON]; - let highlighting = !props?.disableDocBrushing && highlightIndex && !excludeTypes.includes(doc.type as any) && doc._viewType !== CollectionViewType.Linear; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way - if (highlighting && props?.focus !== emptyFunction && StrCast(doc.title) !== '[pres element template]') { + if (highlightIndex) { return { highlightStyle, - highlightColor: highlightIndex !== Doc.DocBrushStatus.highlighted && SelectionManager.Views().some(dv => dv.rootDoc === doc) ? 'black' : highlightColor, + highlightColor, highlightIndex, highlightStroke: doc.type === DocumentType.INK, }; @@ -132,10 +132,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps return undefined; case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey'; - // return doc = dragManager.dragDocument ? props.dragEffects.opacity??CastofOpacityonline94 : Cast()) - // same idea for background Color case StyleProp.Opacity: - return Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null)); + return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null)); case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && doc?.hideLinkButton); case StyleProp.FontSize: @@ -218,7 +216,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps docColor = docColor || 'transparent'; break; case DocumentType.LABEL: - docColor = docColor || (doc.annotationOn !== undefined ? 'rgba(128, 128, 128, 0.18)' : undefined) || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); + docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); @@ -269,7 +267,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps doc?.boxShadow, doc?._viewType === CollectionViewType.Pile ? '4px 4px 10px 2px' - : isBackground() || doc?._isGroup || docProps?.LayoutTemplateString + : lockedPosition() || doc?._isGroup || docProps?.LayoutTemplateString ? undefined // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide) : `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.boxShadow, '0.2vw 0.2vw 0.8vw')}` ); @@ -280,27 +278,28 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps return doc.z ? `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow : props?.ContainingCollectionDoc?._useClusters - ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent + ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : NumCast(doc.group, -1) !== -1 - ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent - : isBackground() + ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent + : lockedPosition() ? undefined // if it's a background & has a cluster color, make the shadow spread really big : StrCast(doc.boxShadow, ''); } } case StyleProp.PointerEvents: - if (MainView.Instance._exploreMode) return 'all'; + const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name); + if (MainView.Instance._exploreMode) return isInk ? 'visiblePainted' : 'all'; if (doc?.pointerEvents) return StrCast(doc.pointerEvents); + if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents); if (props?.pointerEvents?.() === 'none') return 'none'; - const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name); - if (opacity() === 0 || (isInk && !docProps?.treeViewDoc) || doc?.isInkMask) return 'none'; - if (!isInk) return props?.isDocumentActive?.() ? 'all' : undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active - return undefined; + if (opacity() === 0 || doc?.isInkMask) return 'none'; + if (props?.isDocumentActive?.()) return isInk ? 'visiblePainted' : 'all'; + return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active case StyleProp.Decorations: if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) { - return doc && doc.pointerEvents === 'none' && isBackground() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? ( + return doc && doc.pointerEvents === 'none' && lockedPosition() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? ( <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}> - <FontAwesomeIcon icon={isBackground() ? 'lock' : 'unlock'} style={{ color: isBackground() ? 'red' : undefined }} size="lg" /> + <FontAwesomeIcon icon={lockedPosition() ? 'lock' : 'unlock'} style={{ color: lockedPosition() ? 'red' : undefined }} size="lg" /> </div> ) : null; } diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 863829a51..681ff66e0 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -1,7 +1,6 @@ import { action, computed, observable, ObservableSet, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast } from '../../fields/Doc'; -import { List } from '../../fields/List'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx index 21a5af83f..3465a5283 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -345,14 +345,6 @@ export class Keyframe extends React.Component<IProps> { }), TimelineMenu.Instance.addItem( 'button', - 'Show Data', - action(() => { - const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 }); - CollectionDockingView.AddSplit(kvp, OpenWhereMod.right); - }) - ), - TimelineMenu.Instance.addItem( - 'button', 'Delete', action(() => { (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 1ead80bd0..e38905cbc 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -26,12 +26,14 @@ import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; import React = require('react'); import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; +import { OverlayView } from '../OverlayView'; +import { ScriptingRepl } from '../ScriptingRepl'; const _global = (window /* browser */ || global) /* node */ as any; @observer export class CollectionDockingView extends CollectionSubView() { @observable public static Instance: CollectionDockingView | undefined; - public static makeDocumentConfig(document: Doc, panelName?: string, width?: number) { + public static makeDocumentConfig(document: Doc, panelName?: string, width?: number, keyValue?: boolean) { return { type: 'react-component', component: 'DocumentFrameRenderer', @@ -39,6 +41,7 @@ export class CollectionDockingView extends CollectionSubView() { width: width, props: { documentId: document[Id], + keyValue, panelName, // name of tab that can be used to close or replace its contents }, }; @@ -143,10 +146,10 @@ export class CollectionDockingView extends CollectionSubView() { @undoBatch @action - public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean): boolean { + public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean, keyValue?: boolean): boolean { const instance = CollectionDockingView.Instance; if (!instance) return false; - const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName); + const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue); if (!panelName && stack) { const activeContentItemIndex = stack.contentItems.findIndex((item: any) => item.config === stack._activeContentItem.config); const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout); @@ -168,10 +171,10 @@ export class CollectionDockingView extends CollectionSubView() { } @undoBatch - public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string) { + public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) { return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1 ? CollectionDockingView.CloseSplit(doc) - : CollectionDockingView.AddSplit(doc, location, stack, panelName); + : CollectionDockingView.AddSplit(doc, location, stack, panelName, keyValue); } // @@ -179,7 +182,7 @@ export class CollectionDockingView extends CollectionSubView() { // @undoBatch @action - public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string) { + public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) { if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document); if (!CollectionDockingView.Instance) return false; const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document); @@ -190,7 +193,7 @@ export class CollectionDockingView extends CollectionSubView() { const instance = CollectionDockingView.Instance; const glayRoot = instance._goldenLayout.root; if (!instance) return false; - const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName); + const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue); if (!pullSide && stack) { stack.addChild(docContentConfig, undefined); @@ -407,11 +410,10 @@ export class CollectionDockingView extends CollectionSubView() { const _width = Number(getComputedStyle(content).width.replace('px', '')); const _height = Number(getComputedStyle(content).height.replace('px', '')); return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => { - const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: this.rootDoc.title + '-icon', _width, _height, _nativeWidth, _nativeHeight }); const proto = this.dataDoc; // Cast(img.proto, Doc, null)!; proto['thumb-nativeWidth'] = _width; proto['thumb-nativeHeight'] = _height; - this.dataDoc.thumb = new ImageField(iconFile); + proto.thumb = new ImageField(iconFile); }); } } @@ -580,19 +582,25 @@ ScriptingGlobals.add( '(doc: any)' ); ScriptingGlobals.add( - function openOnRight(doc: any) { - return CollectionDockingView.AddSplit(doc, OpenWhereMod.right); + function openDoc(doc: any, where: OpenWhere) { + switch (where) { + case OpenWhere.addRight: + return CollectionDockingView.AddSplit(doc, OpenWhereMod.right); + case OpenWhere.overlay: + if (doc === 'repl') OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); + else Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + } }, - 'opens up document in tab on right side of the screen', + 'opens up document in location specified', '(doc: any)' ); ScriptingGlobals.add( - function openInOverlay(doc: any) { - return Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + function openRepl() { + return 'openRepl'; }, 'opens up document in screen overlay layer', '(doc: any)' ); -ScriptingGlobals.add(function useRightSplit(doc: any, shiftKey?: boolean) { - CollectionDockingView.ReplaceTab(doc, OpenWhereMod.right, undefined, shiftKey); +ScriptingGlobals.add(function useRightSplit(doc: any, addToRightSplit?: boolean) { + CollectionDockingView.ReplaceTab(doc, OpenWhereMod.right, undefined, addToRightSplit); }); diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index 5a107d2ca..a55b70e22 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -97,7 +97,7 @@ top: 2.5%; height: 95%; border-radius: 4px; - background: $light-gray; + //background: $light-gray; &:hover { opacity: 1; } diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 5bdff347c..4941bc722 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -9,7 +9,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { Cast, NumCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils'; +import { emptyFunction, formatTime, OmitKeys, returnFalse, returnNone, returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; @@ -180,12 +180,15 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack if ( // need to include range inputs because after dragging video time slider it becomes target element !(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) && - this.props.isSelected(true) + this.props.isContentActive() ) { // if shift pressed scrub 1 second otherwise 1/10th const jump = e.shiftKey ? 1 : 0.1; switch (e.key) { case ' ': + this.props.playing() ? this.props.Pause() : this.props.Play(); + break; + case '^': if (!CollectionStackedTimeline.SelectingRegion) { this._markerStart = this._markerEnd = this.currentTime; CollectionStackedTimeline.SelectingRegion = this; @@ -399,8 +402,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack title: ComputedField.MakeFunction(`self["${endTag}"] ? "#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"]) : "#" + formatToTime(self["${startTag}"])`) as any, _minFontSize: 12, _maxFontSize: 24, - _singleLine: false, + _singleLine: true, _stayInCollection: true, + backgroundColor: 'rgba(128, 128, 128, 0.5)', useLinkSmallAnchor: true, hideLinkButton: true, _isLinkButton: true, @@ -422,7 +426,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack @action playOnClick = (anchorDoc: Doc, clientX: number) => { - const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; + const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05; const endTime = this.anchorEnd(anchorDoc); if (this.layoutDoc.autoPlayAnchors) { if (this.props.playing()) this.props.Pause(); @@ -448,9 +452,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack @action clickAnchor = (anchorDoc: Doc, clientX: number) => { if (anchorDoc.isLinkButton) { - LinkFollower.FollowLink(undefined, anchorDoc, this.props, false); + LinkFollower.FollowLink(undefined, anchorDoc, false); } - const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; + const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05; const endTime = this.anchorEnd(anchorDoc); if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) { if (this.props.playing()) this.props.Pause(); @@ -564,7 +568,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack anchor, })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; - return ( + return this.clipDuration === 0 ? null : ( <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}> <div className="collectionStackedTimeline-timelineContainer" @@ -597,10 +601,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack top, width: `${width}px`, height: `${height}px`, - }} - onClick={e => { - this.props.playFrom(start, this.anchorEnd(d.anchor)); - e.stopPropagation(); + pointerEvents: 'none', }}> <StackedTimelineAnchor {...this.props} @@ -753,7 +754,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> time < NumCast(this.props.mark[this.props.endTag]) && this._lastTimecode < NumCast(this.props.mark[this.props.startTag]) - 1e-5 ) { - LinkFollower.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false); + LinkFollower.FollowLink(undefined, this.props.mark, false); } this._lastTimecode = time; } @@ -764,7 +765,9 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> this._disposer?.(); } + @observable noEvents = false; // starting the drag event for anchor resizing + @action onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => { //this.props._timeline?.setPointerCapture(e.pointerId); const newTime = (e: PointerEvent) => { @@ -782,8 +785,8 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> } return false; }; + this.noEvents = true; var undo: UndoManager.Batch | undefined; - setupMoveUpEvents( this, e, @@ -792,11 +795,11 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> this.props.setTime(newTime(e)); return changeAnchor(anchor, left, newTime(e)); }, - e => { + action(e => { this.props.setTime(newTime(e)); - // this.props._timeline?.releasePointerCapture(e.pointerId); undo?.end(); - }, + this.noEvents = false; + }), emptyFunction ); }; @@ -814,10 +817,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> // renders anchor LabelBox renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) { const anchor = observable({ view: undefined as any }); - const focusFunc = (doc: Doc, options: DocFocusOptions) => { - this.props.playLink(mark); - this.props.focus(doc, options); - }; + const focusFunc = (doc: Doc, options: DocFocusOptions) => this.props.playLink(mark); return { anchor, view: ( @@ -827,6 +827,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> ref={action((r: DocumentView | null) => (anchor.view = r))} Document={mark} DataDoc={undefined} + pointerEvents={this.noEvents ? returnNone : undefined} styleProvider={this.props.styleProvider} renderDepth={this.props.renderDepth + 1} LayoutTemplate={undefined} @@ -857,15 +858,15 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> render() { const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.anchorScreenToLocalXf, this.width, this.height); return ( - <> + <div style={{ pointerEvents: this.noEvents ? 'none' : undefined }}> {inner.view} {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? null : ( <> - <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} /> - <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} /> + <div key="left" className="collectionStackedTimeline-left-resizer" style={{ pointerEvents: this.noEvents ? 'none' : undefined }} onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} /> + <div key="right" className="collectionStackedTimeline-resizer" style={{ pointerEvents: this.noEvents ? 'none' : undefined }} onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} /> </> )} - </> + </div> ); } } diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 4805a748b..33d468950 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -201,6 +201,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection componentDidMount() { super.componentDidMount?.(); + this.props.setContentView?.(this); // reset section headers when a new filter is inputted this._pivotFieldDisposer = reaction( @@ -226,6 +227,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection this._autoHeightDisposer?.(); } + isAnyChildContentActive = () => this.props.isAnyChildContentActive(); + @action moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => { return this.props.removeDocument?.(doc) && addDocument?.(doc) ? true : false; @@ -651,8 +654,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection <DocumentView Document={menuDoc} DataDoc={menuDoc} - isContentActive={this.props.isContentActive} - isDocumentActive={returnTrue} + isContentActive={this.isContentActive} + isDocumentActive={this.isContentActive} addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} addDocTab={this.props.addDocTab} @@ -716,14 +719,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection className={this.isStackingView ? 'collectionStackingView' : 'collectionMasonryView'} ref={this.createRef} style={{ - overflowY: this.props.isContentActive() ? 'auto' : 'hidden', + overflowY: this.isContentActive() ? 'auto' : 'hidden', background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), - pointerEvents: this.backgroundEvents ? 'all' : undefined, + pointerEvents: (this.props.pointerEvents?.() as any) ?? (this.backgroundEvents ? 'all' : undefined), }} onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} - onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}> + onWheel={e => this.isContentActive() && e.stopPropagation()}> {this.renderedSections} {!this.showAddAGroup ? null : ( <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? '100%' : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index e46220f02..bd74c9399 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -269,10 +269,10 @@ export function CollectionSubView<X>(moreProps?: X) { if (FormattedTextBox.IsFragment(html)) { const href = FormattedTextBox.GetHref(html); if (href) { - const docid = FormattedTextBox.GetDocFromUrl(href); - if (docid) { + const docId = FormattedTextBox.GetDocFromUrl(href); + if (docId) { // prosemirror text containing link to dash document - DocServer.GetRefField(docid).then(f => { + DocServer.GetRefField(docId).then(f => { if (f instanceof Doc) { if (options.x || options.y) { f.x = options.x as number; @@ -311,8 +311,8 @@ export function CollectionSubView<X>(moreProps?: X) { } else { const path = window.location.origin + '/doc/'; if (text.startsWith(path)) { - const docid = text.replace(Doc.globalServerPath(), '').split('?')[0]; - DocServer.GetRefField(docid).then(f => { + const docId = text.replace(Doc.globalServerPath(), '').split('?')[0]; + DocServer.GetRefField(docId).then(f => { if (f instanceof Doc) { if (options.x || options.y) { f.x = options.x as number; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 48e5748a0..eafa50d27 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -2,7 +2,6 @@ import { computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; -import { Id } from '../../../fields/FieldSymbols'; import { ObjectField } from '../../../fields/ObjectField'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; @@ -10,13 +9,12 @@ import { TraceMobx } from '../../../fields/util'; import { returnEmptyString } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { CollectionViewType } from '../../documents/DocumentTypes'; -import { BranchCreate, BranchTask } from '../../documents/Gitlike'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; -import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; +import { OpenWhere } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { CollectionCarousel3DView } from './CollectionCarousel3DView'; import { CollectionCarouselView } from './CollectionCarouselView'; @@ -169,7 +167,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab onContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; - if (cm && !e.isPropagationStopped() && this.rootDoc[Id] !== Doc.MainDocId) { + if (cm && !e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 this.setupViewTypes( 'UI Controls...', @@ -193,23 +191,23 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab } !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => (this.rootDoc._isLightbox = !this.rootDoc._isLightbox), icon: 'project-diagram' }); - if (!Doc.noviceMode && false) { - optionItems.push({ - description: 'Create Branch', - event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight), - icon: 'project-diagram', - }); - optionItems.push({ - description: 'Pull Master', - event: () => BranchTask(this.rootDoc, 'pull'), - icon: 'project-diagram', - }); - optionItems.push({ - description: 'Merge Branches', - event: () => BranchTask(this.rootDoc, 'merge'), - icon: 'project-diagram', - }); - } + // if (!Doc.noviceMode && false) { + // optionItems.push({ + // description: 'Create Branch', + // event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight), + // icon: 'project-diagram', + // }); + // optionItems.push({ + // description: 'Pull Master', + // event: () => BranchTask(this.rootDoc, 'pull'), + // icon: 'project-diagram', + // }); + // optionItems.push({ + // description: 'Merge Branches', + // event: () => BranchTask(this.rootDoc, 'merge'), + // icon: 'project-diagram', + // }); + // } !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'hand-point-right' }); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 9459aaf1e..631b9add5 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -35,10 +35,12 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV import { CollectionView } from './CollectionView'; import './TabDocView.scss'; import React = require('react'); +import { KeyValueBox } from '../nodes/KeyValueBox'; const _global = (window /* browser */ || global) /* node */ as any; interface TabDocViewProps { documentId: FieldId; + keyValue?: boolean; glContainer: any; } @observer @@ -57,7 +59,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { } @computed get tabBorderColor() { const highlight = DefaultStyleProvider(this._document, undefined, StyleProp.Highlighting); - if (highlight?.highlightIndex >= Doc.DocBrushStatus.highlighted) return highlight.highlightColor; + if (highlight?.highlightIndex === Doc.DocBrushStatus.highlighted) return highlight.highlightColor; return 'transparent'; } @computed get tabColor() { @@ -348,7 +350,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { addDocTab = (doc: Doc, location: OpenWhere) => { SelectionManager.DeselectAll(); const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); - const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none; + const keyValue = whereFields[1]?.includes('KeyValue'); + const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none; if (doc.dockingConfig) return DashboardView.openDashboard(doc); // prettier-ignore switch (whereFields[0]) { @@ -365,9 +368,9 @@ export class TabDocView extends React.Component<TabDocViewProps> { case OpenWhere.dashboard: return DashboardView.openDashboard(doc); case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc); case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods); - case OpenWhere.replace: return CollectionDockingView.ReplaceTab(doc, whereMods, this.stack); - case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack); - case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, this.stack); + case OpenWhere.replace: return CollectionDockingView.ReplaceTab(doc, whereMods, this.stack, undefined, keyValue); + case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack, undefined, keyValue); + case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, this.stack, undefined, keyValue); } }; remDocTab = (doc: Doc | Doc[]) => { @@ -419,6 +422,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { this._lastView = this._view; })} renderDepth={0} + LayoutTemplateString={this.props.keyValue ? KeyValueBox.LayoutString('data') : undefined} Document={this._document} DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined} ContainingCollectionView={undefined} diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 2bdcf472f..af2d148e0 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; @@ -172,11 +172,7 @@ export class TreeView extends React.Component<TreeViewProps> { childDocList(field: string) { const layout = Cast(Doc.LayoutField(this.doc), Doc, null); - return ( - (this.props.dataDoc ? DocListCastOrNull(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field - (layout ? DocListCastOrNull(layout[field]) : undefined) || // else if there's a layout doc, display it's fields - DocListCastOrNull(this.doc[field]) - ); // otherwise use the document's data field + return DocListCast(this.props.dataDoc?.[field], DocListCast(layout?.[field], DocListCast(this.doc[field]))); } moving: boolean = false; @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { @@ -759,7 +755,7 @@ export class TreeView extends React.Component<TreeViewProps> { const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'New Folder' }; const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'Delete' }; const folderOp = this.childDocs?.length ? [makeFolder] : []; - const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: 'copy', label: 'Open Alias' }; + const openAlias = { script: ScriptField.MakeFunction(`openDoc(getAlias(self), ${OpenWhere.addRight})`)!, icon: 'copy', label: 'Open Alias' }; const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Focus or Open' }; return [ ...(this.props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)), diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 15d8144fc..aea7a3ad9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -120,7 +120,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection constructor(props: any) { super(props); - this.props.setContentView?.(this); } @computed get views() { @@ -293,6 +292,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime); } + groupFocus = (anchor: Doc, options: DocFocusOptions) => { + options.docTransform = new Transform(-NumCast(this.rootDoc.panX) + NumCast(anchor.x), -NumCast(this.rootDoc.panY) + NumCast(anchor.y), 1); + const res = this.props.focus(this.rootDoc, options); + options.docTransform = undefined; + return res; + }; + focus = (anchor: Doc, options: DocFocusOptions) => { const xfToCollection = options?.docTransform ?? Transform.Identity(); const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined }; @@ -301,6 +307,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // focus on the document in the collection const didMove = !cantTransform && !anchor.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale); + if (didMove) options.didMove = true; // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... if (didMove) { const focusTime = options?.instant ? 0 : options.zoomTime ?? 500; @@ -1068,7 +1075,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.Document._isGroup) return; // group style collections neither pan nor zoom + if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom PresBox.Instance?.pauseAutoPres(); if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; e.stopPropagation(); @@ -1320,7 +1327,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection searchFilterDocs={this.searchFilterDocs} isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} isContentActive={emptyFunction} - focus={this.isAnnotationOverlay ? this.props.focus : this.focus} + focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus} addDocTab={this.addDocTab} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} @@ -1568,6 +1575,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action componentDidMount() { + this.props.setContentView?.(this); super.componentDidMount?.(); this.props.setBrushViewer?.(this.brushView); setTimeout( @@ -1796,11 +1804,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); const mores = ContextMenu.Instance.findByDescription('More...'); const moreItems = mores && 'subitems' in mores ? mores.subitems : []; - if (!Doc.noviceMode) { - e.persist(); - moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); - moreItems.push({ description: 'Import exported collection', icon: 'upload', event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) }); - } + moreItems.push({ description: 'Import exported collection', icon: 'upload', event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) }); !mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' }); }; @@ -2250,8 +2254,9 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY zoomTime: browseTransitionTime, }) === undefined ) { - const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; - const parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; + let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; const ffview = selfFfview && selfFfview.rootDoc[selfFfview.props.scaleField || '_viewScale'] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime); Doc.linkFollowHighlight(dv?.props.Document, false); @@ -2275,3 +2280,9 @@ ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: bool TabDocView.PinDoc(view.rootDoc, { currentFrame: Cast(view.rootDoc.currentFrame, 'number', null), pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) }) ); }); +ScriptingGlobals.add(function bringToFront() { + SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc)); +}); +ScriptingGlobals.add(function sendToBack(doc: Doc) { + SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc, true)); +}); diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 4741fc6f2..29e7cd3ad 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -140,7 +140,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { : undefined; if (focusDoc) this.props.docView.props.focus(focusDoc, { instant: true }); - LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false); + LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, false); } } ); diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 5e4315833..384975b45 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -446,7 +446,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)); - timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight); + timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight); setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._currentTimecode = time); @@ -635,15 +635,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp step="0.1" min="1" max="5" - value={this.timeline?._zoomFactor} + value={this.timeline?._zoomFactor ?? 1} className="toolbar-slider" id="zoom-slider" - onPointerDown={(e: React.PointerEvent) => { - e.stopPropagation(); - }} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => { - this.zoom(Number(e.target.value)); - }} + onPointerDown={(e: React.PointerEvent) => e.stopPropagation()} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.zoom(Number(e.target.value))} /> </div> )} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 901415d1e..9bdb2cee7 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -7,7 +7,6 @@ import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { numberRange } from '../../../Utils'; -import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 105f7e151..c21bc9754 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,4 +1,4 @@ -import { computed, trace } from 'mobx'; +import { computed } from 'mobx'; import { observer } from 'mobx-react'; import { AclPrivate, Doc, Opt } from '../../../fields/Doc'; import { ScriptField } from '../../../fields/ScriptField'; @@ -9,6 +9,7 @@ import { DirectoryImportBox } from '../../util/Import & Export/DirectoryImportBo import { CollectionDockingView } from '../collections/CollectionDockingView'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView'; +import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox'; import { CollectionView } from '../collections/CollectionView'; import { InkingStroke } from '../InkingStroke'; import { PresElementBox } from '../nodes/trails/PresElementBox'; @@ -31,6 +32,7 @@ import { KeyValueBox } from './KeyValueBox'; import { LabelBox } from './LabelBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { LinkBox } from './LinkBox'; +import { LoadingBox } from './LoadingBox'; import { MapBox } from './MapBox/MapBox'; import { PDFBox } from './PDFBox'; import { RecordingBox } from './RecordingBox'; @@ -42,8 +44,6 @@ import { VideoBox } from './VideoBox'; import { WebBox } from './WebBox'; import React = require('react'); import XRegExp = require('xregexp'); -import { LoadingBox } from './LoadingBox'; -import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index fcfcaaeda..f34ac2b44 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -54,6 +54,7 @@ import { ScriptingBox } from './ScriptingBox'; import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); +import { KeyValueBox } from './KeyValueBox'; const { Howl } = require('howler'); interface Window { @@ -80,6 +81,7 @@ export enum OpenWhere { replaceLeft = 'replace:left', inParent = 'inParent', inParentFromScreen = 'inParentFromScreen', + overlay = 'overlay', } export enum OpenWhereMod { none = '', @@ -87,6 +89,7 @@ export enum OpenWhereMod { right = 'right', top = 'top', bottom = 'bottom', + rightKeyValue = 'rightKeyValue', } export interface DocFocusOptions { @@ -94,6 +97,7 @@ export interface DocFocusOptions { willZoomCentered?: boolean; // determines whether to zoom in on target document zoomScale?: number; // percent of containing frame to zoom into document zoomTime?: number; + didMove?: boolean; // whether a document was changed during the showDocument process docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom) preview?: boolean; // whether changes should be previewed by the componentView or written to the document @@ -160,6 +164,7 @@ export interface DocumentViewSharedProps { childHideResizeHandles?: () => boolean; dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt styleProvider: Opt<StyleProviderFunc>; + setTitleFocus?: () => void; focus: DocFocusFunc; fitWidth?: (doc: Doc) => boolean | undefined; docFilters: () => string[]; @@ -313,8 +318,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } - @computed get pointerEvents() { - return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : '')); + @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined { + return this.onClickHandler ? 'all' : this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : '')); } @computed get finalLayoutKey() { return StrCast(this.Document.layoutKey, 'layout'); @@ -326,7 +331,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this.props.NativeHeight(); } @computed get onClickHandler() { - return this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); + const noClick = this.rootSelected() || this.props.isSelected() || this._componentView?.isAnyChildContentActive?.(); + return noClick ? undefined : this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick; @@ -538,18 +544,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps .ScreenToLocalTransform() .scale(this.NativeDimScaling) .transformDirection(x - left, y - top); - // dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); // bcz: this was breaking dragging rotated objects since the offset may be out of bounds with regard to the unrotated document - // dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]); dragData.dropAction = dropAction; dragData.treeViewDoc = this.props.treeViewDoc; dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument; dragData.canEmbed = this.props.canEmbedOnDrag; - //dragData.dimSource : - // dragEffects field, set dim - // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping - // add a dragEffects prop to DocumentView as a function that sets up. Each view has its own prop, when you start dragging: - // in Draganager, figure out which doc(s) you're dragging and change what opacity function returns const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, () => @@ -559,22 +558,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } - onKeyDown = (e: React.KeyboardEvent) => { - if (e.altKey) { - e.stopPropagation(); - e.preventDefault(); - if (e.key === '†' || e.key === 't') { - if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title'; - if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true)); - else if (!this._titleRef.current.setIsFocused(true)) { - // if focus didn't change, focus on interior text... - this._titleRef.current?.setIsFocused(false); - this._componentView?.setFocus?.(); - } - } - } - }; - defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => { const targetMatch = Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document @@ -584,6 +567,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined; }; + // switches text input focus to the title bar of the document (and displays the title bar if it hadn't been) + setTitleFocus = () => { + if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title'; + setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined + }; + onClick = action((e: React.MouseEvent | React.PointerEvent) => { if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { let stopPropagate = true; @@ -613,6 +602,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps altKey, shiftKey, ctrlKey, + value: undefined, }, console.log ); @@ -627,7 +617,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // bcz: hack? don't execute script if you're clicking on a scripting box itself const { clientX, clientY, shiftKey, altKey, metaKey } = e; const func = () => - this.onClickHandler.script.run( + this.onClickHandler?.script.run( { this: this.layoutDoc, self: this.rootDoc, @@ -655,7 +645,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } else clickFunc(); } else if (!this._longPress && this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { SelectionManager.DeselectAll(); - this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey); + this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, e.altKey); } else { if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part @@ -990,7 +980,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const more = cm.findByDescription('More...'); const moreItems = more && 'subitems' in more ? more.subitems : []; if (!Doc.IsSystem(this.rootDoc)) { - (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); if (!Doc.noviceMode) { moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: 'concierge-bell' }); moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => (this.Document._chromeHidden = !this.Document._chromeHidden), icon: 'project-diagram' }); @@ -1002,6 +991,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' }); } + moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); + + (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); } if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) { @@ -1012,7 +1004,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } const help = cm.findByDescription('Help...'); const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; - helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' }); + helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' }); !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' }); !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' }); !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' }); @@ -1078,7 +1070,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; panelHeight = () => this.props.PanelHeight() - this.headerMargin; screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); - onClickFunc = () => this.onClickHandler; + onClickFunc = () => this.onClickHandler as any as ScriptField; // bcz: typing HACK. check and fix. setHeight = (height: number) => (this.layoutDoc._height = height); setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); isContentActive = (outsideReaction?: boolean) => { @@ -1129,22 +1121,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </Tooltip> ); } + contentPointerEvents = () => (this.onClickHandler ? 'none' : this.pointerEvents); @computed get contents() { TraceMobx(); return ( <div className="documentView-contentsView" style={{ - pointerEvents: - this.opacity === 0 - ? 'none' - : (this.props.pointerEvents?.() as any) ?? this.rootDoc.layoutKey === 'layout_icon' - ? 'none' - : (this.props.contentPointerEvents as any) - ? (this.props.contentPointerEvents as any) - : this.rootDoc.type !== DocumentType.INK && this.isContentActive() - ? 'all' - : 'none', + pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> {!this._retryThumb || !this.thumbShown() ? null : ( @@ -1165,7 +1149,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps <DocumentContentsView key={1} {...this.props} - pointerEvents={this.opacity === 0 ? returnNone : this.props.pointerEvents} + pointerEvents={this.contentPointerEvents} docViewPath={this.props.viewPath} thumbShown={this.thumbShown} isHovering={this.props.isHovering} @@ -1177,7 +1161,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ScreenToLocalTransform={this.screenToLocal} rootSelected={this.rootSelected} onClick={this.onClickFunc} - focus={emptyFunction} + focus={this.props.focus} + setTitleFocus={this.setTitleFocus} layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints} @@ -1495,7 +1480,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps className={`${DocumentView.ROOT_DIV} docView-hack`} ref={this._mainCont} onContextMenu={this.onContextMenu} - onKeyDown={this.onKeyDown} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)} @@ -1504,7 +1488,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps style={{ display: this.hidden ? 'inline' : undefined, borderRadius: this.borderRounding, - pointerEvents: this.pointerEvents, + pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, }}> {!borderPath.path ? ( animRenderDoc @@ -1581,8 +1565,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { } public static showBackLinks(linkSource: Doc) { - const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish'; - DocServer.GetRefField(docid).then(docx => { + const docId = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish'; + DocServer.GetRefField(docId).then(docx => { const rootAlias = () => { const rootAlias = Doc.MakeAlias(linkSource); rootAlias.x = rootAlias.y = 0; @@ -1595,7 +1579,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { /*rootAlias()*/ ], { title: linkSource.title + '-pivot', _width: 500, _height: 500 }, - docid + docId ); linkCollection.linkSource = linkSource; if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript('updateLinkCollection(self)'); @@ -1857,7 +1841,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, width: isButton ? '100%' : xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, height: - isButton || this.props.forceAutoHeight + isButton || this.props.LayoutTemplateString?.includes(KeyValueBox.name) || this.props.forceAutoHeight ? undefined : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), }}> diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 60417430f..9f9d93a82 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -45,7 +45,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { return NumCast(this.props.Document.schemaSplitPercentage, 50); } get fieldDocToLayout() { - return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document; + return this.props.fieldKey ? DocCast(this.props.Document[this.props.fieldKey], DocCast(this.props.Document)) : this.props.Document; } @action diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index e74ef4a39..c4adc7f1a 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -48,7 +48,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { if (value instanceof Doc) { e.stopPropagation(); e.preventDefault(); - ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' }); + ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(value, ((OpenWhere.addRight as string) + 'KeyValue') as OpenWhere), icon: 'layer-group' }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } }; diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 33fab6873..6e0b4be37 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -79,7 +79,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp @observable _mouseOver = false; @computed get hoverColor() { - return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : 'unset'; + return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); } fitTextToBox = (r: any): any => { diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 1b37dc7ab..e12548f18 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -39,7 +39,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { this.onPointerMove, emptyFunction, (e, doubleTap) => { - if (doubleTap) LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false); + if (doubleTap) LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, false); else this.props.select(false); }, false diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 16b352e4f..fcc5b6975 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -171,7 +171,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { followLink = () => { LinkDocPreview.Clear(); if (this._linkDoc && this._linkSrc) { - LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); + LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false); } else if (this.props.hrefs?.length) { const webDoc = Array.from(SearchBox.staticSearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ?? diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index fa2021642..a6e2c3e90 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -180,13 +180,15 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable const params: ScriptParam = {}; this.compileParams.forEach(p => (params[p.split(':')[0].trim()] = p.split(':')[1].trim())); - const result = CompileScript(this.rawText, { - editable: true, - transformer: DocumentIconContainer.getTransformer(), - params, - typecheck: false, - }); - Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : new ScriptField(undefined, undefined, this.rawText), true); + const result = !this.rawText.trim() + ? ({ compiled: false, errors: undefined } as any) + : CompileScript(this.rawText, { + editable: true, + transformer: DocumentIconContainer.getTransformer(), + params, + typecheck: false, + }); + Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : undefined, true); this.onError(result.compiled ? undefined : result.errors); return result.compiled; }; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index b1acef44a..f7425b26a 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -145,6 +145,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const nativeWidth = NumCast(this.layoutDoc.nativeWidth); const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth(); if ( + !this.props.isSelected(true) && + !Doc.IsBrushedDegree(this.rootDoc) && + !this.isAnyChildContentActive() && !this.rootDoc.thumbLockout && !this.props.dontRegisterView && this._iframe && @@ -181,6 +184,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }); } }; + _thumbTimer: any; async componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) @@ -197,6 +201,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document), async selected => { if (selected) { + this._thumbTimer && clearTimeout(this._thumbTimer); this._webPageHasBeenRendered = true; } else if ( (!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail) @@ -204,7 +209,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps LightboxView.LightboxDoc !== this.rootDoc ) { // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty. - this.updateThumb(); + this._thumbTimer && clearTimeout(this._thumbTimer); + this._thumbTimer = setTimeout(this.updateThumb, 2000); } }, { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) } diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts deleted file mode 100644 index b4a382faf..000000000 --- a/src/client/views/nodes/button/ButtonScripts.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ScriptingGlobals } from "../../../util/ScriptingGlobals"; -import { SelectionManager } from "../../../util/SelectionManager"; -import { Colors } from "../../global/globalEnums"; - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function changeView(view: string) { - const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; - selected ? selected.Document._viewType = view : console.log("[FontIconBox.tsx] changeView failed"); -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function toggleOverlay(readOnly?: boolean) { - const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; - if (readOnly) return selected?.Document.z ? Colors.MEDIUM_BLUE : "transparent"; - selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("failed"); -});
\ No newline at end of file diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss index a1ca777b3..7fe1436c7 100644 --- a/src/client/views/nodes/button/FontIconBox.scss +++ b/src/client/views/nodes/button/FontIconBox.scss @@ -43,10 +43,6 @@ cursor: pointer; flex-direction: column; - &:hover { - background-color: rgba(0, 0, 0, 0.3) !important; - } - svg { width: 50% !important; height: 50%; @@ -68,10 +64,6 @@ justify-content: center; align-items: center; justify-items: center; - - &:hover { - filter: brightness(0.85) !important; - } } &.tglBtn, @@ -220,10 +212,6 @@ box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); border-radius: 3px; } - - &:hover { - background-color: rgba(0, 0, 0, 0.3) !important; - } } &.colorBtnLabel { @@ -248,10 +236,6 @@ align-content: center; align-items: center; - &:hover { - background-color: rgba(0, 0, 0, 0.3) !important; - } - .menuButton-dropdownList { position: absolute; width: 150px; @@ -283,10 +267,6 @@ cursor: pointer; background: transparent; - &:hover { - background-color: rgba(0, 0, 0, 0.3) !important; - } - &.slider { color: $white; cursor: pointer; diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 0468d1c47..cb962cad3 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -11,7 +11,7 @@ import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { WebField } from '../../../../fields/URLField'; import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; -import { aggregateBounds, StopEvent, Utils } from '../../../../Utils'; +import { aggregateBounds, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { LinkManager } from '../../../util/LinkManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; @@ -100,7 +100,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); } Icon = (color: string) => { - const icon = StrCast(this.dataDoc.icon, 'user') as any; + const icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any; const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />; return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; }; @@ -134,10 +134,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { @computed get numberButton() { const numBtnType: string = StrCast(this.rootDoc.numBtnType); const numScript = ScriptCast(this.rootDoc.script); - const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ value, _readOnly_: false }), 'set num value'); + const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value'); // Script for checking the outcome of the toggle - const checkResult = Number(numScript?.script.run({ value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); + const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>; @@ -150,7 +150,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { min={NumCast(this.rootDoc.numBtnMin, 0)} max={NumCast(this.rootDoc.numBtnMax, 100)} value={checkResult} - className={'menu-slider'} + className="menu-slider" onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))} onPointerUp={() => this._batch?.end()} onChange={e => { @@ -284,27 +284,23 @@ export class FontIconBox extends DocComponent<ButtonProps>() { text = 'User Default'; } noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; - } else if (script?.script.originalScript.startsWith('setFont')) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); - noviceList = ['Roboto', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text']; - } + } else text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); } catch (e) { console.log(e); } // Get items to place into the list const list = this.buttonList - .filter(value => !Doc.noviceMode || noviceList.includes(value)) + .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value)) .map(value => ( <div className="list-item" key={`${value}`} style={{ - fontFamily: script.script.originalScript.startsWith('setFont') ? value : undefined, + fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined, backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined, }} - onClick={undoBatch(() => script.script.run({ value }))}> + onClick={undoBatch(() => script.script.run({ self: this.rootDoc, value }))}> {value[0].toUpperCase() + value.slice(1)} </div> )); @@ -357,7 +353,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { ev.preventDefault(); ev.stopPropagation(); const s = this.colorScript; - s && undoBatch(() => s.script.run({ value: Utils.colorString(value), _readOnly_: false }).result)(); + s && undoBatch(() => s.script.run({ self: this.rootDoc, value: Utils.colorString(value), _readOnly_: false }).result)(); }; const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']; return <SketchPicker onChange={change as any /* SketchPicker passes the mouse event to the callback, but the type system doesn't know that */} color={curColor} presetColors={presets} />; @@ -368,7 +364,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { @computed get colorButton() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const curColor = this.colorScript?.script.run({ value: undefined, _readOnly_: true }).result ?? 'transparent'; + const curColor = this.colorScript?.script.run({ self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent'; const label = !this.label || !FontIconBox.GetShowLabels() ? null : ( @@ -377,13 +373,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() { </div> ); - // dropdown caret seems superfluous since clicking the color button does the same thing - // const dropdownCaret = <div - // className="menuButton-dropDown" - // style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> - // <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" /> - // </div>; - //setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView return ( <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`} @@ -598,121 +587,70 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); }); -/** TEXT - * setFont - * setFontSize - * toggleBold - * toggleUnderline - * toggleItalic - * setAlignment - * toggleBold - * toggleItalic - * toggleUnderline - **/ - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setFont(font: string, checkResult?: boolean) { - if (checkResult) return RichTextMenu.Instance?.fontFamily; - font && RichTextMenu.Instance.setFontFamily(font); -}); - -ScriptingGlobals.add(function getActiveTextInfo(info: 'family' | 'size' | 'color' | 'highlight') { - const editorView = RichTextMenu.Instance.TextView?.EditorView; - const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection(); - // prettier-ignore - switch (info) { - case 'family': return style?.activeFamilies[0]; - case 'size': return style?.activeSizes[0]; - case 'color': return style?.activeColors[0]; - case 'highlight': return style?.activeHighlights[0]; - } -}); - -ScriptingGlobals.add(function setAlignment(align: 'left' | 'right' | 'center', checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - return (editorView ? RichTextMenu.Instance.textAlign : Doc.UserDoc().textAlign) === align ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (editorView?.state) RichTextMenu.Instance.align(editorView, editorView.dispatch, align); - else Doc.UserDoc().textAlign = align; -}); - -ScriptingGlobals.add(function setBulletList(mapStyle: 'bullet' | 'decimal', checkResult?: boolean) { +ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: any, checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle(); - if (active === mapStyle) return Colors.MEDIUM_BLUE; - return 'transparent'; - } - editorView?.state && RichTextMenu.Instance.changeListType(mapStyle); -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setFontColor(color?: string, checkResult?: boolean) { - if (checkResult) return RichTextMenu.Instance.fontColor; - color && RichTextMenu.Instance.setColor(color); -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); - const editorView = RichTextMenu.Instance.TextView?.EditorView; + // prettier-ignore + const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => any; setDoc: () => void; setMode?: () => void }> = new Map([ + ['font', { + checkResult: () => RichTextMenu.Instance?.fontFamily, + setDoc: () => value && RichTextMenu.Instance.setFontFamily(value), + setMode: () => Doc.UserDoc().textAlign = value, + }], + ['highlight', { + checkResult: () =>(selected ?? Doc.UserDoc())._fontHighlight, + setDoc: () => value && RichTextMenu.Instance.setHighlight(value), + }], + ['fontColor', { + checkResult: () => RichTextMenu.Instance?.fontColor, + setDoc: () => value && RichTextMenu.Instance.setColor(value), + }], + ['fontSize', { + checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''), + setDoc: () => { + if (typeof value === 'number') value = value.toString(); + if (value && Number(value).toString() === value) value += 'px'; + RichTextMenu.Instance.setFontSize(value); + }, + }], + ]); if (checkResult) { - return (selected ?? Doc.UserDoc())._fontHighlight; + return map.get(attr)?.checkResult(); } - color && RichTextMenu.Instance.setHighlight(color); + if (editorView?.state) map.get(attr)?.setDoc(); + else map.get(attr)?.setMode?.(); }); -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) { - if (checkResult) { - return RichTextMenu.Instance?.fontSize.replace('px', ''); - } - if (typeof size === 'number') size = size.toString(); - if (size && Number(size).toString() === size) size += 'px'; - RichTextMenu.Instance.setFontSize(size); -}); -ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - return (editorView ? RichTextMenu.Instance.noAutoLink : false) ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor(); -}); -ScriptingGlobals.add(function toggleDictation(checkResult?: boolean) { +type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'bullet' | 'decimal'; +type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }]; +ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) { const textView = RichTextMenu.Instance?.TextView; - if (checkResult) { - return textView?._recording ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (textView) runInAction(() => (textView._recording = !textView._recording)); -}); - -ScriptingGlobals.add(function toggleBold(checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - return (editorView ? RichTextMenu.Instance.bold : Doc.UserDoc().fontWeight === 'bold') ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (editorView) RichTextMenu.Instance?.toggleBold(); - else Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold'; -}); - -ScriptingGlobals.add(function toggleUnderline(checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - return (editorView ? RichTextMenu.Instance.underline : Doc.UserDoc().textDecoration === 'underline') ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (editorView) RichTextMenu.Instance?.toggleUnderline(); - else Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline'; -}); - -ScriptingGlobals.add(function toggleItalic(checkResult?: boolean) { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - if (checkResult) { - return (editorView ? RichTextMenu.Instance.italics : Doc.UserDoc().fontStyle === 'italics') ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (editorView) RichTextMenu.Instance?.toggleItalics(); - else Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics'; + const editorView = textView?.EditorView; + // prettier-ignore + const alignments:attrfuncs[] = (['left','right','center'] as ("left"|"center"|"right")[]).map((where) => + [ where, { checkResult: () =>(editorView ? (RichTextMenu.Instance.textAlign ===where): (Doc.UserDoc().textAlign ===where) ? true:false), + toggle: () => (editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, where):(Doc.UserDoc().textAlign = where))}]); + // prettier-ignore + const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list => + [ list, { checkResult: () => (editorView ? RichTextMenu.Instance.getActiveListStyle() === list:false), + toggle: () => editorView?.state && RichTextMenu.Instance.changeListType(list) }]); + // prettier-ignore + const attrs:attrfuncs[] = [ + ['dictation', { checkResult: () => textView?._recording ? true:false, + toggle: () => textView && runInAction(() => (textView._recording = !textView._recording)) }], + ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false), + toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}], + ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false), + toggle: editorView ? RichTextMenu.Instance.toggleBold : () => (Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold')}], + ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance.italics : (Doc.UserDoc().fontStyle === 'italics') ? true:false), + toggle: editorView ? RichTextMenu.Instance.toggleItalics : () => (Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics')}], + ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance.underline : (Doc.UserDoc().textDecoration === 'underline') ? true:false), + toggle: editorView ? RichTextMenu.Instance.toggleUnderline : () => (Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline') }]] + + const map = new Map(attrs.concat(alignments).concat(listings)); + if (checkResult) return map.get(charStyle)?.checkResult() ? Colors.MEDIUM_BLUE : 'transparent'; + map.get(charStyle)?.toggle(); }); export function checkInksToGroup() { @@ -832,62 +770,39 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode'); // toggle: Set overlay status of selected document -ScriptingGlobals.add(function setIsInkMask(checkResult?: boolean) { - const selected = SelectionManager.Docs().lastElement(); - if (checkResult) { - if (selected?.type === DocumentType.INK) { - return BoolCast(selected.isInkMask) ? Colors.MEDIUM_BLUE : 'transparent'; - } - return ActiveIsInkMask() ? Colors.MEDIUM_BLUE : 'transparent'; - } - SetActiveIsInkMask(!ActiveIsInkMask()); - SelectionManager.Docs() - .filter(doc => doc.type === DocumentType.INK) - .map(doc => (doc.isInkMask = !doc.isInkMask)); -}); - -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setFillColor(color?: string, checkResult?: boolean) { +ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); - if (checkResult) { - if (selected?.type === DocumentType.INK) { - return StrCast(selected.fillColor); - } - return ActiveFillColor(); - } - SetActiveFillColor(StrCast(color)); - SelectionManager.Docs() - .filter(doc => doc.type === DocumentType.INK) - .map(doc => (doc.fillColor = color)); -}); - -ScriptingGlobals.add(function setStrokeWidth(width: number, checkResult?: boolean) { - if (checkResult) { - const selected = SelectionManager.Docs().lastElement(); - if (selected?.type === DocumentType.INK) { - return NumCast(selected.strokeWidth); - } - return ActiveInkWidth(); - } - SetActiveInkWidth(width.toString()); - SelectionManager.Docs() - .filter(doc => doc.type === DocumentType.INK) - .map(doc => (doc.strokeWidth = Number(width))); -}); + // prettier-ignore + const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([ + ['inkMask', { + checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.isInkMask) : ActiveIsInkMask()) ? Colors.MEDIUM_BLUE : 'transparent'), + setInk: (doc: Doc) => (doc.isInkMask = !doc.isInkMask), + setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()), + }], + ['fillColor', { + checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ? Colors.MEDIUM_BLUE : 'transparent'), + setInk: (doc: Doc) => (doc.fillColor = StrCast(value)), + setMode: () => SetActiveFillColor(StrCast(value)), + }], + [ 'strokeWidth', { + checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.strokeWidth) : ActiveInkWidth()), + setInk: (doc: Doc) => (doc.strokeWidth = NumCast(value)), + setMode: () => SetActiveInkWidth(value.toString()), + }], + ['strokeColor', { + checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.color) : ActiveInkColor()), + setInk: (doc: Doc) => (doc.color = String(value)), + setMode: () => SetActiveInkColor(StrCast(value)), + }], + ]); -// toggle: Set overlay status of selected document -ScriptingGlobals.add(function setStrokeColor(color?: string, checkResult?: boolean) { if (checkResult) { - const selected = SelectionManager.Docs().lastElement(); - if (selected?.type === DocumentType.INK) { - return StrCast(selected.color); - } - return ActiveInkColor(); + return map.get(option)?.checkResult(); } - SetActiveInkColor(StrCast(color)); + map.get(option)?.setMode(); SelectionManager.Docs() .filter(doc => doc.type === DocumentType.INK) - .map(doc => (doc.color = String(color))); + .map(doc => map.get(option)?.setInk(doc)); }); /** WEB diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index fcd6e0c55..aa269d8d6 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -32,7 +32,7 @@ export class DashDocCommentView { }; this.root = ReactDOM.createRoot(this.dom); - this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />); + this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />); (this as any).dom = this.dom; } @@ -48,7 +48,7 @@ export class DashDocCommentView { } interface IDashDocCommentViewInternal { - docid: string; + docId: string; view: any; getPos: any; } @@ -63,13 +63,13 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV } onPointerLeaveCollapsed(e: any) { - DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); + DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); e.preventDefault(); e.stopPropagation(); } onPointerEnterCollapsed(e: any) { - DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); + DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); e.preventDefault(); e.stopPropagation(); } @@ -82,7 +82,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true }); this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs setTimeout(() => { - expand && DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); + expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1)))); } catch (e) {} @@ -100,12 +100,12 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV const state = this.props.view.state; for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) { const m = state.doc.nodeAt(i); - if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docid === this.props.docid) { + if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docId === this.props.docId) { return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any; pos: number; hidden: boolean }; } } - const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: this.props.docid, float: 'right' }); + const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: this.props.docId, float: 'right' }); this.props.view.dispatch(state.tr.insert(this.props.getPos() + 1, dashDoc)); setTimeout(() => { try { @@ -119,7 +119,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV return ( <span className="formattedTextBox-inlineComment" - id={'DashDocCommentView-' + this.props.docid} + id={'DashDocCommentView-' + this.props.docId} onPointerLeave={this.onPointerLeaveCollapsed} onPointerEnter={this.onPointerEnterCollapsed} onPointerUp={this.onPointerUpCollapsed} diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 19ee1fc7b..61345f891 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -42,7 +42,7 @@ export class DashDocView { this.root = ReactDOM.createRoot(this.dom); this.root.render( - <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} /> + <DashDocViewInternal docId={node.attrs.docId} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} /> ); } destroy() { @@ -53,7 +53,7 @@ export class DashDocView { } interface IDashDocViewInternal { - docid: string; + docId: string; alias: string; tbox: FormattedTextBox; width: string; @@ -77,7 +77,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { updateDoc = action((dashDoc: Doc) => { this._dashDoc = dashDoc; - this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey); + this._finalLayout = this.props.docId ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey); if (this._finalLayout) { if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) { @@ -107,12 +107,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { super(props); this._textBox = this.props.tbox; - DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => { + DocServer.GetRefField(this.props.docId + this.props.alias).then(async dashDoc => { if (!(dashDoc instanceof Doc)) { this.props.alias && - DocServer.GetRefField(this.props.docid).then(async dashDocBase => { + DocServer.GetRefField(this.props.docId).then(async dashDocBase => { if (dashDocBase instanceof Doc) { - const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docid + this.props.alias); + const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docId + this.props.alias); aliasedDoc.layoutKey = 'layout'; this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined); this.updateDoc(aliasedDoc); @@ -161,12 +161,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { }; onPointerLeave = () => { - const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement; + const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement; ele && (ele.style.backgroundColor = ''); }; onPointerEnter = () => { - const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement; + const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement; ele && (ele.style.backgroundColor = 'orange'); }; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index a89e8b4ed..21ccf3bc7 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -54,7 +54,7 @@ export class DashFieldView { unclickable={this.unclickable} getPos={getPos} fieldKey={node.attrs.fieldKey} - docid={node.attrs.docid} + docId={node.attrs.docId} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} @@ -76,7 +76,7 @@ export class DashFieldView { interface IDashFieldViewInternal { fieldKey: string; - docid: string; + docId: string; hideKey: boolean; tbox: FormattedTextBox; width: number; @@ -100,8 +100,8 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna this._fieldKey = this.props.fieldKey; this._textBoxDoc = this.props.tbox.props.Document; - if (this.props.docid) { - DocServer.GetRefField(this.props.docid).then( + if (this.props.docId) { + DocServer.GetRefField(this.props.docId).then( action(async dashDoc => { dashDoc instanceof Doc && (this._dashDoc = dashDoc); }) diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index a76f9623a..ddec86606 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -186,7 +186,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return ''; } public static GetDocFromUrl(url: string) { - return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docid + return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId } constructor(props: any) { @@ -545,7 +545,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps width: target[WidthSym](), height: target[HeightSym](), title: 'dashDoc', - docid: target[Id], + docId: target[Id], float: 'unset', }); if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) { @@ -1260,7 +1260,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => { if (pdfAnchor instanceof Doc) { const dashField = view.state.schema.nodes.paragraph.create({}, [ - view.state.schema.nodes.dashField.create({ fieldKey: 'text', docid: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [ + view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [ view.state.schema.marks.linkAnchor.create({ allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }], location: 'add:right', @@ -1485,7 +1485,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerUp = (e: React.PointerEvent): void => { const editor = this._editorView!; const state = editor?.state; - if (!state || !editor) return; + if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return; if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); else if (this.props.isContentActive(true)) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); @@ -1669,8 +1669,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; onKeyDown = (e: React.KeyboardEvent) => { - if (e.altKey) { + if ((e.altKey || e.ctrlKey) && e.key === 't') { e.preventDefault(); + e.stopPropagation(); + this.props.setTitleFocus?.(); return; } const state = this._editorView!.state; @@ -1721,6 +1723,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._ignoreScroll = true; this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop; this._ignoreScroll = false; + e.stopPropagation(); + e.preventDefault(); } } }; @@ -1871,7 +1875,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox-cont" - onWheel={e => this.props.isContentActive() && e.stopPropagation()} + ref={r => + r?.addEventListener( + 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) + (e: WheelEvent) => { + if (this.isContentActive()) { + if (!NumCast(this.layoutDoc._scrollTop) && e.deltaY <= 0) e.preventDefault(); + e.stopPropagation(); + } + }, + { passive: false } + ) + } style={{ ...(this.props.dontScale ? {} diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 5675776fb..9e9b61db3 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -89,8 +89,8 @@ export class RichTextRules { textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text textDoc[inlineFieldKey] = ''; // set a default value for the annotation const node = (state.doc.resolve(start) as any).nodeAfter; - const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] }); - const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: textDocInline[Id], float: 'right' }); + const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] }); + const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' }); const sm = state.storedMarks || undefined; const replaced = node ? state.tr @@ -248,17 +248,17 @@ export class RichTextRules { // [[fieldKey:Doc]] => show field of doc new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => { const fieldKey = match[1]; - const docid = match[3]?.replace(':', ''); + const docId = match[3]?.replace(':', ''); const value = match[2]?.substring(1); if (!fieldKey) { - if (docid) { - DocServer.GetRefField(docid).then(docx => { + if (docId) { + DocServer.GetRefField(docId).then(docx => { const rstate = this.TextBox.EditorView?.state; const selection = rstate?.selection.$from.pos; if (rstate) { this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); } - const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid); + const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500 }, docId); DocUtils.MakeLink({ doc: this.TextBox.getAnchor(true) }, { doc: target }, 'portal to:portal from', undefined); const fstate = this.TextBox.EditorView?.state; @@ -274,7 +274,7 @@ export class RichTextRules { const num = value.match(/^[0-9.]$/); this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: false }); + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId, hideKey: false }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }), @@ -311,16 +311,16 @@ export class RichTextRules { const fieldKey = match[1] || ''; const fieldParam = match[2]?.replace('…', '...') || ''; const rawdocid = match[3]?.substring(1); - const docid = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined; - if (!fieldKey && !docid) return state.tr; - docid && - DocServer.GetRefField(docid).then(docx => { + const docId = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined; + if (!fieldKey && !docId) return state.tr; + docId && + DocServer.GetRefField(docId).then(docx => { if (!(docx instanceof Doc && docx)) { - Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docid); + Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docId); } }); const node = (state.doc.resolve(start) as any).nodeAfter; - const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docid, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() }); + const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docId, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() }); const sm = state.storedMarks || undefined; return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; }), diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index aa2475dca..6c9d5d31a 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -176,7 +176,7 @@ export const nodes: { [index: string]: NodeSpec } = { dashComment: { attrs: { - docid: { default: '' }, + docId: { default: '' }, }, inline: true, group: 'inline', @@ -213,7 +213,7 @@ export const nodes: { [index: string]: NodeSpec } = { title: { default: null }, float: { default: 'left' }, location: { default: 'add:right' }, - docid: { default: '' }, + docId: { default: '' }, }, group: 'inline', draggable: true, @@ -246,7 +246,7 @@ export const nodes: { [index: string]: NodeSpec } = { float: { default: 'right' }, hidden: { default: false }, // whether dashComment node has toggle the dashDoc's display off fieldKey: { default: '' }, - docid: { default: '' }, + docId: { default: '' }, alias: { default: '' }, }, group: 'inline', @@ -261,7 +261,7 @@ export const nodes: { [index: string]: NodeSpec } = { inline: true, attrs: { fieldKey: { default: '' }, - docid: { default: '' }, + docId: { default: '' }, hideKey: { default: false }, editable: { default: true }, }, diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index be40b3592..e79e7472a 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -570,9 +570,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } else { if (bestTarget._panX !== activeItem.presPanX || bestTarget._panY !== activeItem.presPanY || bestTarget._viewScale !== activeItem.presViewScale) { - bestTarget._panX = activeItem.presPanX; - bestTarget._panY = activeItem.presPanY; - bestTarget._viewScale = activeItem.presViewScale; + bestTarget._panX = activeItem.presPanX ?? bestTarget._panX; + bestTarget._panY = activeItem.presPanY ?? bestTarget._panY; + bestTarget._viewScale = activeItem.presViewScale ?? bestTarget._viewScale; changed = true; } } @@ -717,6 +717,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime), effect: activeItem, noSelect: true, + openLocation: OpenWhere.addLeft, anchorDoc: activeItem, easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any, zoomTextSelections: BoolCast(activeItem.presZoomText), diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 3b101a0c6..db6b1f011 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -1,5 +1,5 @@ import React = require('react'); -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; @@ -7,10 +7,10 @@ import { List } from '../../../fields/List'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { LinkFollower } from '../../util/LinkFollower'; import { undoBatch } from '../../util/UndoManager'; +import { OpenWhere } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { AnchorMenu } from './AnchorMenu'; import './Annotation.scss'; -import { OpenWhere } from '../nodes/DocumentView'; interface IAnnotationProps extends FieldViewProps { anno: Doc; @@ -82,7 +82,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { e.stopPropagation(); } else if (e.button === 0) { e.stopPropagation(); - LinkFollower.FollowLink(undefined, this.annoTextRegion, this.props, false); + LinkFollower.FollowLink(undefined, this.annoTextRegion, false); } }; diff --git a/src/decycler/decycler.d.ts b/src/decycler/decycler.d.ts new file mode 100644 index 000000000..84620f79c --- /dev/null +++ b/src/decycler/decycler.d.ts @@ -0,0 +1,2 @@ +export declare const decycle: Function; +export declare const retrocycle: Function; diff --git a/src/decycler/decycler.js b/src/decycler/decycler.js new file mode 100644 index 000000000..7fb8a45c7 --- /dev/null +++ b/src/decycler/decycler.js @@ -0,0 +1,51 @@ +/// +/// this is a modified copy of the npm project: https://www.npmjs.com/package/json-decycle +/// the original code is used as replacer when stringifying JSON objects that have cycles. +/// However, we want an additional replacer that stringifies Dash Fields and Docs in a custom way. +/// So this modified code allows for a custom replacer to be added to this object that will run before this replacer +/// + +const g = e => typeof e === 'object' && e != null && !(e instanceof Boolean) && !(e instanceof Date) && !(e instanceof Number) && !(e instanceof RegExp) && !(e instanceof String); +const b = e => String('#') + e.map(t => String(t).replace(/~/g, '~0').replace(/\//g, '~1')).join('/'); +// eslint-disable-next-line node/no-unsupported-features/es-syntax +export function decycle(replacer) { + const e = new WeakMap(); + return function (n, rr) { + const r = replacer(n, rr); + if (n !== '$ref' && g(r)) { + if (e.has(r)) return { $ref: b(e.get(r)) }; + e.set(r, [...(e.get(this) === undefined ? [] : e.get(this)), n]); + } + return r; + }; +} +// eslint-disable-next-line node/no-unsupported-features/es-syntax +export function retrocycle() { + const e = new WeakMap(); + const t = new WeakMap(); + const n = new Set(); + function r(o) { + const c = o.$ref.slice(1).split('/'); + let s; + let a = this; + // eslint-disable-next-line no-plusplus + for (let p = 0; p < c.length; p++) { + s = c[p].replace(/~1/g, '/').replace(/~0/g, '~'); + a = a[s]; + } + const f = e.get(o); + f[t.get(o)] = a; + } + return function (c, s) { + if (c === '$ref') n.add(this); + else if (g(s)) { + const f = c === '' && Object.keys(this).length === 1; + if (f) n.forEach(r, this); + else { + e.set(s, this); + t.set(s, c); + } + } + return s; + }; +} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index ee164ab31..4d82551db 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -10,6 +10,7 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba import { SelectionManager } from '../client/util/SelectionManager'; import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper'; import { UndoManager } from '../client/util/UndoManager'; +import { decycle } from '../decycler/decycler'; import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils'; import { DateField } from './DateField'; import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols'; @@ -25,7 +26,7 @@ import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Ty import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from './URLField'; import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util'; import JSZip = require('jszip'); - +import * as JSZipUtils from '../JSZipUtils'; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { const onDelegate = Object.keys(doc).includes(key); @@ -33,20 +34,19 @@ export namespace Field { return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field)); } export function toScriptString(field: Field): string { - if (typeof field === 'string') { - if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s - return `"${field}"`; + switch (typeof field) { + case 'string': + if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s + return `"${field}"`; + case 'number': + case 'boolean': + return String(field); } - if (typeof field === 'number' || typeof field === 'boolean') return String(field); - if (field === undefined || field === null) return 'null'; - return field[ToScriptString](); + return field?.[ToScriptString]?.() ?? 'null'; } export function toString(field: Field): string { - if (typeof field === 'string') return field; - if (typeof field === 'number' || typeof field === 'boolean') return String(field); - if (field instanceof ObjectField) return field[ToString](); - if (field instanceof RefField) return field[ToString](); - return ''; + if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field); + return field?.[ToString]?.() || ''; } export function IsField(field: any): field is Field; export function IsField(field: any, includeUndefined: true): field is Field | undefined; @@ -79,17 +79,14 @@ export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { return Cast(field, Doc); } -export function NumListCast(field: FieldResult) { - return Cast(field, listSpec('number'), []); -} -export function StrListCast(field: FieldResult) { - return Cast(field, listSpec('string'), []); +export function NumListCast(field: FieldResult, defaultVal: number[] = []) { + return Cast(field, listSpec('number'), defaultVal); } -export function DocListCast(field: FieldResult) { - return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; +export function StrListCast(field: FieldResult, defaultVal: string[] = []) { + return Cast(field, listSpec('string'), defaultVal); } -export function DocListCastOrNull(field: FieldResult) { - return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; +export function DocListCast(field: FieldResult, defaultVal: Doc[] = []) { + return Cast(field, listSpec(Doc), defaultVal).filter(d => d instanceof Doc) as Doc[]; } export const WidthSym = Symbol('Width'); @@ -153,17 +150,8 @@ export function updateCachedAcls(doc: Doc) { } @scriptingGlobal -@Deserializable('Doc', updateCachedAcls).withFields(['id']) +@Deserializable('Doc', updateCachedAcls, ['id']) export class Doc extends RefField { - //TODO tfs: these should be temporary... - private static mainDocId: string | undefined; - public static get MainDocId() { - return this.mainDocId; - } - public static set MainDocId(id: string | undefined) { - this.mainDocId = id; - } - @observable public static CurrentlyLoading: Doc[]; // removes from currently loading display @action @@ -530,20 +518,13 @@ export namespace Doc { } export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) { if (key.startsWith('_')) key = key.substring(1); - const hasProto = doc.proto instanceof Doc; + const hasProto = Doc.GetProto(doc) !== doc ? Doc.GetProto(doc) : undefined; const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1; const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1; if (onDeleg || !hasProto || (!onProto && !defaultProto)) { doc[key] = value; } else doc.proto![key] = value; } - export async function SetOnPrototype(doc: Doc, key: string, value: Field) { - const proto = Object.getOwnPropertyNames(doc).indexOf('isPrototype') === -1 ? doc.proto : doc; - - if (proto) { - proto[key] = value; - } - } export function GetAllPrototypes(doc: Doc): Doc[] { const protos: Doc[] = []; let d: Opt<Doc> = doc; @@ -582,21 +563,13 @@ export namespace Doc { // compare whether documents or their protos match export function AreProtosEqual(doc?: Doc, other?: Doc) { - if (!doc || !other) return false; - const r = doc === other; - const r2 = Doc.GetProto(doc) === other; - const r3 = Doc.GetProto(other) === doc; - const r4 = Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined; - return r || r2 || r3 || r4; + return doc && other && Doc.GetProto(doc) === Doc.GetProto(other); } // Gets the data document for the document. Note: this is mis-named -- it does not specifically // return the doc's proto, but rather recursively searches through the proto inheritance chain // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype'). export function GetProto(doc: Doc): Doc { - if (doc instanceof Promise) { - // console.log("GetProto: warning: got Promise insead of Doc"); - } const proto = doc && (Doc.GetT(doc, 'isPrototype', 'boolean', true) ? doc : doc.proto || doc); return proto === doc ? proto : Doc.GetProto(proto); } @@ -722,89 +695,116 @@ export namespace Doc { return bestAlias ?? Doc.MakeAlias(doc); } - export async function makeClone( - doc: Doc, - cloneMap: Map<string, Doc>, - linkMap: Map<Doc, Doc>, - rtfs: { copy: Doc; key: string; field: RichTextField }[], - exclusions: string[], - topLevelExclusions: string[], - dontCreate: boolean, - asBranch: boolean - ): Promise<Doc> { + // this lists out all the tag ids that can be in a RichTextField that might contain document ids. + // if a document is cloned, we need to make sure to clone all of these referenced documents as well; + export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId']; + export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], cloneLinks: boolean): Promise<Doc> { if (Doc.IsBaseProto(doc)) return doc; if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; - const copy = dontCreate ? (asBranch ? Cast(doc.branchMaster, Doc, null) || doc : doc) : new Doc(undefined, true); + const copy = new Doc(undefined, true); cloneMap.set(doc[Id], copy); - const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== 'annotationOn') : exclusions; - const filter = [...fieldExclusions, ...topLevelExclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])]; + const filter = [...exclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])]; await Promise.all( Object.keys(doc).map(async key => { if (filter.includes(key)) return; - const assignKey = (val: any) => !dontCreate && (copy[key] = val); + const assignKey = (val: any) => (copy[key] = val); const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); const copyObjectField = async (field: ObjectField) => { const list = await Cast(doc[key], listSpec(Doc)); const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc); if (docs !== undefined && docs.length) { - const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch))); - !dontCreate && assignKey(new List<Doc>(clones)); - } else if (doc[key] instanceof Doc) { - assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields + const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, cloneLinks))); + assignKey(new List<Doc>(clones)); } else { - !dontCreate && assignKey(ObjectField.MakeCopy(field)); + assignKey(ObjectField.MakeCopy(field)); if (field instanceof RichTextField) { - if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) { + if (DocsInTextFieldIds.some(id => field.Data.includes(`"${id}":`))) { + const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g'); + const rawdocids = field.Data.match(docidsearch); + const docids = rawdocids?.map((str: string) => + DocsInTextFieldIds.reduce((output, exp) => { + return output.replace(new RegExp(`${exp}":`, 'g'), ''); + }, str) + .replace(/"/g, '') + .trim() + ); + const results = docids && (await DocServer.GetRefFields(docids)); + const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key])); + docs && docs.map(doc => Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks)); rtfs.push({ copy, key, field }); } } } }; - if (key === 'proto') { - if (doc[key] instanceof Doc) { - assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch)); - } - } else if (key === 'anchor1' || key === 'anchor2') { - if (doc[key] instanceof Doc) { - assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, [], true, asBranch)); - } - } else { - if (field instanceof RefField) { - assignKey(field); - } else if (cfield instanceof ComputedField) { - !dontCreate && assignKey(cfield[Copy]()); - // ComputedField.MakeFunction(cfield.script.originalScript)); - } else if (field instanceof ObjectField) { - await copyObjectField(field); - } else if (field instanceof Promise) { - debugger; //This shouldn't happen... + const docAtKey = doc[key]; + if (docAtKey instanceof Doc) { + if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || key === 'annotationOn' || key === 'proto' || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) { + assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, cloneLinks)); } else { - assignKey(field); + assignKey(docAtKey); } + } else if (field instanceof RefField) { + assignKey(field); + } else if (cfield instanceof ComputedField) { + assignKey(cfield[Copy]()); + // ComputedField.MakeFunction(cfield.script.originalScript)); + } else if (field instanceof ObjectField) { + await copyObjectField(field); + } else if (field instanceof Promise) { + debugger; //This shouldn't happen... + } else { + assignKey(field); } }) ); - for (const link of Array.from(doc[DirectLinksSym])) { - const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, [], dontCreate, asBranch); - linkMap.set(link, linkClone); - } - if (!dontCreate) { - Doc.SetInPlace(copy, 'title', (asBranch ? 'BRANCH: ' : 'CLONE: ') + doc.title, true); - asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc); - if (!Doc.IsPrototype(copy)) { - Doc.AddDocToList(doc, 'branches', Doc.GetProto(copy)); + Array.from(doc[DirectLinksSym]).forEach(async link => { + if ( + cloneLinks || + ((cloneMap.has(DocCast(link.anchor1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor1)?.annotationOn)?.[Id])) && (cloneMap.has(DocCast(link.anchor2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor2)?.annotationOn)?.[Id]))) + ) { + linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, cloneLinks)); } - cloneMap.set(doc[Id], copy); - } + }); + Doc.SetInPlace(copy, 'title', 'CLONE: ' + doc.title, true); + copy.cloneOf = doc; + cloneMap.set(doc[Id], copy); + Doc.AddFileOrphan(copy); return copy; } - export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) { - const linkMap = new Map<Doc, Doc>(); + export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, visited: Set<Doc>) { + if (visited.has(clone)) return; + visited.add(clone); + Object.keys(clone) + .filter(key => key !== 'cloneOf') + .map(key => { + const docAtKey = DocCast(clone[key]); + if (docAtKey && !Doc.IsSystem(docAtKey)) { + if (!Array.from(cloneMap.values()).includes(docAtKey)) { + if (cloneMap.has(docAtKey[Id])) { + clone[key] = cloneMap.get(docAtKey[Id]); + } else clone[key] = undefined; + } else { + repairClone(docAtKey, cloneMap, visited); + } + } + }); + } + export function MakeClones(docs: Doc[], cloneLinks: boolean) { + const cloneMap = new Map<string, Doc>(); + return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneMap)); + } + + export async function MakeClone(doc: Doc, cloneLinks = true, cloneMap: Map<string, Doc> = new Map()) { + const linkMap = new Map<string, Doc>(); const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = []; - const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], ['context'], dontCreate, asBranch); - Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true)); + const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], cloneLinks); + const repaired = new Set<Doc>(); + const linkedDocs = Array.from(linkMap.values()); + const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs]; + clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, repaired)); + linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true)); rtfMap.map(({ copy, key, field }) => { const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { const mapped = cloneMap.get(id); @@ -816,9 +816,10 @@ export namespace Doc { }; const regex = `(${Doc.localServerPath()})([^"]*)`; const re = new RegExp(regex, 'g'); - copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); + const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => `"${exp}":`).join('|') + ')"([^"]+)"', 'g'); + copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text); }); - return { clone: copy, map: cloneMap }; + return { clone: copy, map: cloneMap, linkMap }; } export async function Zip(doc: Doc) { @@ -827,9 +828,11 @@ export namespace Doc { // a.href = url; // a.download = `DocExport-${this.props.Document[Id]}.zip`; // a.click(); - const { clone, map } = await Doc.MakeClone(doc, true); + const { clone, map, linkMap } = await Doc.MakeClone(doc); + clone.LINKS = new List<Doc>(Array.from(linkMap.values())); + const proms = [] as string[]; function replacer(key: any, value: any) { - if (['branchOf', 'cloneOf', 'context', 'cursors'].includes(key)) return undefined; + if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined; else if (value instanceof Doc) { if (key !== 'field' && Number.isNaN(Number(key))) { const __fields = value[FieldsSym](); @@ -839,9 +842,14 @@ export namespace Doc { } } else if (value instanceof ScriptField) return { script: value.script, __type: 'script' }; else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: 'RichTextField' }; - else if (value instanceof ImageField) return { url: value.url.href, __type: 'image' }; - else if (value instanceof PdfField) return { url: value.url.href, __type: 'pdf' }; - else if (value instanceof AudioField) return { url: value.url.href, __type: 'audio' }; + else if (value instanceof ImageField) { + const extension = value.url.href.replace(/.*\./, ''); + proms.push(value.url.href.replace('.' + extension, '_o.' + extension)); + return { url: value.url.href, __type: 'image' }; + } else if (value instanceof PdfField) { + proms.push(value.url.href); + return { url: value.url.href, __type: 'pdf' }; + } else if (value instanceof AudioField) return { url: value.url.href, __type: 'audio' }; else if (value instanceof VideoField) return { url: value.url.href, __type: 'video' }; else if (value instanceof WebField) return { url: value.url.href, __type: 'web' }; else if (value instanceof MapField) return { url: value.url.href, __type: 'map' }; @@ -854,8 +862,34 @@ export namespace Doc { const docs: { [id: string]: any } = {}; Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1])); - const docString = JSON.stringify({ id: doc[Id], docs }, replacer); - + const docString = JSON.stringify({ id: clone[Id], docs }, decycle(replacer)); + + let generateZIP = (proms: string[]) => { + var zip = new JSZip(); + var count = 0; + var zipFilename = 'dashExport.zip'; + + proms + .filter(url => url.startsWith(window.location.origin)) + .forEach((url, i) => { + var filename = proms[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%'); + // loading a file and add it in a zip file + JSZipUtils.getBinaryContent(url, function (err: any, data: any) { + if (err) { + throw err; // or handle the error + } + zip.file(filename, data, { binary: true }); + count++; + if (count == proms.length) { + zip.file('doc.json', docString); + zip.generateAsync({ type: 'blob' }).then(function (content) { + saveAs(content, zipFilename); + }); + } + }); + }); + }; + generateZIP(proms); const zip = new JSZip(); zip.file('doc.json', docString); @@ -1540,8 +1574,12 @@ export namespace Doc { formData.append('remap', 'true'); const response = await fetch(upload, { method: 'POST', body: formData }); const json = await response.json(); + console.log(json); if (json !== 'error') { - const doc = await DocServer.GetRefField(json); + await DocServer.GetRefFields(json.docids as string[]); + const doc = DocCast(await DocServer.GetRefField(json.id)); + (await DocListCastAsync(doc?.LINKS))?.forEach(link => LinkManager.Instance.addLink(link)); + doc.LINKS = undefined; return doc; } } diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index d7edd4266..3e75a071f 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -1,11 +1,11 @@ -import { serializable } from "serializr"; -import { scriptingGlobal } from "../client/util/ScriptingGlobals"; -import { Deserializable } from "../client/util/SerializationHelper"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; -import { ObjectField } from "./ObjectField"; +import { serializable } from 'serializr'; +import { scriptingGlobal } from '../client/util/ScriptingGlobals'; +import { Deserializable } from '../client/util/SerializationHelper'; +import { Copy, ToScriptString, ToString } from './FieldSymbols'; +import { ObjectField } from './ObjectField'; @scriptingGlobal -@Deserializable("RichTextField") +@Deserializable('RichTextField') export class RichTextField extends ObjectField { @serializable(true) readonly Data: string; @@ -13,14 +13,14 @@ export class RichTextField extends ObjectField { @serializable(true) readonly Text: string; - constructor(data: string, text: string = "") { + constructor(data: string, text: string = '') { super(); this.Data = data; this.Text = text; } Empty() { - return !(this.Text || this.Data.toString().includes("dashField") || this.Data.toString().includes("align")); + return !(this.Text || this.Data.toString().includes('dashField') || this.Data.toString().includes('align')); } [Copy]() { @@ -28,14 +28,16 @@ export class RichTextField extends ObjectField { } [ToScriptString]() { - return `new RichTextField("${this.Data.replace(/"/g, "\\\"")}", "${this.Text}")`; + return `new RichTextField("${this.Data.replace(/"/g, '\\"')}", "${this.Text}")`; } [ToString]() { return this.Text; } public static DashField(fieldKey: string) { - return new RichTextField(`{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`, ""); + return new RichTextField( + `{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docId":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`, + '' + ); } - -}
\ No newline at end of file +} diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index bf055cd8b..239b59e83 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -264,18 +264,18 @@ export namespace RichTextUtils { const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => { const { url: src, width, agnostic } = image; - let docid: string; + let docId: string; const guid = Utils.GenerateDeterministicGuid(agnostic); const backingDocId = StrCast(textNote[guid]); if (!backingDocId) { const backingDoc = Docs.Create.ImageDocument(agnostic, { _width: 300, _height: 300 }); DocUtils.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument); - docid = backingDoc[Id]; - textNote[guid] = docid; + docId = backingDoc[Id]; + textNote[guid] = docId; } else { - docid = backingDocId; + docId = backingDocId; } - return schema.node('image', { src, agnostic, width, docid, float: null, location: 'add:right' }); + return schema.node('image', { src, agnostic, width, docId, float: null, location: 'add:right' }); }; const textNode = (schema: any, run: docs_v1.Schema$TextRun) => { diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index b5eca78dd..feb419597 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -103,30 +103,9 @@ export class ScriptField extends ObjectField { } this.rawscript = rawscript; this.setterscript = setterscript; - this.script = script ?? (CompileScript('false', { addReturn: true }) as CompiledScript); + this.script = script ?? ScriptField.GetScriptFieldCache('false:') ?? (CompileScript('false', { addReturn: true }) as CompiledScript); } - // init(callback: (res: Field) => any) { - // const options = this.options!; - // const keys = Object.keys(options.options.capturedIds); - // Server.GetFields(keys).then(fields => { - // let captured: { [name: string]: Field } = {}; - // keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]); - // const opts: ScriptOptions = { - // addReturn: options.options.addReturn, - // params: options.options.params, - // requiredType: options.options.requiredType, - // capturedVariables: captured - // }; - // const script = CompileScript(options.script, opts); - // if (!script.compiled) { - // throw new Error("Can't compile script"); - // } - // this._script = script; - // callback(this); - // }); - // } - [Copy](): ObjectField { return new ScriptField(this.script, this.setterscript, this.rawscript); } @@ -172,7 +151,7 @@ export class ComputedField extends ScriptField { _lastComputedResult: any; //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); - _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result); + _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) ?? doc, value: '', _last_: this._lastComputedResult, _readOnly_: true }, console.log).result); [ToValue](doc: Doc) { return ComputedField.toValue(doc, this); @@ -181,12 +160,8 @@ export class ComputedField extends ScriptField { return new ComputedField(this.script, this.setterscript, this.rawscript); } - public static MakeScript(script: string, params: object = {}) { - const compiled = ScriptField.CompileScript(script, params, false); - return compiled.compiled ? new ComputedField(compiled) : undefined; - } public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) { - const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); + const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables }); const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined; const compiledsetscript = compiledsetter?.compiled ? compiledsetter : undefined; return compiled.compiled ? new ComputedField(compiled, compiledsetscript) : undefined; diff --git a/src/fields/util.ts b/src/fields/util.ts index 6ff13d5d3..70d9ed61f 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -48,15 +48,11 @@ export function TraceMobx() { } const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { - if (SerializationHelper.IsSerializing()) { + if (SerializationHelper.IsSerializing() || typeof prop === 'symbol') { target[prop] = value; return true; } - if (typeof prop === 'symbol') { - target[prop] = value; - return true; - } if (value !== undefined) { value = value[SelfProxy] || value; } @@ -95,12 +91,8 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number delete target.__fields[prop]; } else { target.__fieldKeys && (target.__fieldKeys[prop] = true); - // if (target.__fields[prop] !== value) { - // console.log("ASSIGN " + prop + " " + value); - // } target.__fields[prop] = value; } - //if (typeof value === "object" && !(value instanceof ObjectField)) debugger; if (writeToServer) { if (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } }); @@ -143,13 +135,6 @@ export function denormalizeEmail(email: string) { return email.replace(/__/g, '.'); } -// playground mode allows the user to add/delete documents or make layout changes without them saving to the server -// let playgroundMode = false; - -// export function togglePlaygroundMode() { -// playgroundMode = !playgroundMode; -// } - /** * Copies parent's acl fields to the child */ @@ -317,7 +302,6 @@ export function setter(target: any, in_prop: string | symbol | number, value: an if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; - // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true; if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) { if (!prop.startsWith('__')) prop = prop.substring(1); @@ -377,9 +361,9 @@ export function getField(target: any, prop: string | number, ignoreProto: boolea export function deleteProperty(target: any, prop: string | number | symbol) { if (typeof prop === 'symbol') { delete target[prop]; - return true; + } else { + target[SelfProxy][prop] = undefined; } - target[SelfProxy][prop] = undefined; return true; } diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index fe4c475c9..6e28268a9 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -1,19 +1,19 @@ -import ApiManager, { Registration } from './ApiManager'; -import { Method, _success } from '../RouteManager'; import * as formidable from 'formidable'; -import v4 = require('uuid/v4'); -const AdmZip = require('adm-zip'); -import { extname, basename, dirname } from 'path'; import { createReadStream, createWriteStream, unlink, writeFile } from 'fs'; -import { publicDirectory, filesDirectory } from '..'; -import { Database } from '../database'; -import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils'; +import { basename, dirname, extname, normalize } from 'path'; import * as sharp from 'sharp'; -import { AcceptableMedia, Upload } from '../SharedMediaTypes'; -import { normalize } from 'path'; +import { filesDirectory, publicDirectory } from '..'; +import { retrocycle } from '../../decycler/decycler'; +import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils'; +import { Database } from '../database'; +import { Method, _success } from '../RouteManager'; import RouteSubscriber from '../RouteSubscriber'; -const imageDataUri = require('image-data-uri'); +import { AcceptableMedia, Upload } from '../SharedMediaTypes'; +import ApiManager, { Registration } from './ApiManager'; import { SolrManager } from './SearchManager'; +import v4 = require('uuid/v4'); +const AdmZip = require('adm-zip'); +const imageDataUri = require('image-data-uri'); const fs = require('fs'); export enum Directory { @@ -228,35 +228,41 @@ export default class UploadManager extends ApiManager { form.parse(req, async (_err, fields, files) => { remap = fields.remap !== 'false'; let id: string = ''; + let docids: string[] = []; try { for (const name in files) { const f = files[name]; const path_2 = Array.isArray(f) ? '' : f.path; const zip = new AdmZip(path_2); zip.getEntries().forEach((entry: any) => { - if (!entry.entryName.startsWith('files/')) return; - let directory = dirname(entry.entryName) + '/'; - const extension = extname(entry.entryName); - const base = basename(entry.entryName).split('.')[0]; + let entryName = entry.entryName.replace(/%%%/g, '/'); + if (!entryName.startsWith('files/')) { + return; + } + const extension = extname(entryName); + const pathname = publicDirectory + '/' + entry.entryName; + const targetname = publicDirectory + '/' + entryName; try { zip.extractEntryTo(entry.entryName, publicDirectory, true, false); - directory = '/' + directory; - - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_o' + extension)); - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_s' + extension)); - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_m' + extension)); - createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_l' + extension)); + createReadStream(pathname).pipe(createWriteStream(targetname)); + if (extension !== '.pdf') { + createReadStream(pathname).pipe(createWriteStream(targetname.replace('_o' + extension, '_s' + extension))); + createReadStream(pathname).pipe(createWriteStream(targetname.replace('_o' + extension, '_m' + extension))); + createReadStream(pathname).pipe(createWriteStream(targetname.replace('_o' + extension, '_l' + extension))); + } + unlink(pathname, () => {}); } catch (e) { console.log(e); } }); const json = zip.getEntry('doc.json'); try { - const data = JSON.parse(json.getData().toString('utf8')); + const data = JSON.parse(json.getData().toString('utf8'), retrocycle()); const datadocs = data.docs; id = getId(data.id); const docs = Object.keys(datadocs).map(key => datadocs[key]); docs.forEach(mapFn); + docids = docs.map(doc => doc.id); await Promise.all( docs.map( (doc: any) => @@ -279,7 +285,7 @@ export default class UploadManager extends ApiManager { unlink(path_2, () => {}); } SolrManager.update(); - res.send(JSON.stringify(id || 'error')); + res.send(JSON.stringify({ id, docids } || 'error')); } catch (e) { console.log(e); } |
