diff --git a/src/addon.ts b/src/addon.ts index 5028ec0..cf6d93c 100644 --- a/src/addon.ts +++ b/src/addon.ts @@ -1,11 +1,13 @@ import AddonEvents from "./events"; import AddonViews from "./views"; import AddonPrefs from "./prefs"; +import { Knowledge } from "./knowledge"; class Knowledge4Zotero { public events: AddonEvents; public views: AddonViews; public prefs: AddonPrefs; + public knowledge: Knowledge; constructor() { this.events = new AddonEvents(this); diff --git a/src/base.ts b/src/base.ts index de707b5..ef9381a 100644 --- a/src/base.ts +++ b/src/base.ts @@ -8,9 +8,8 @@ class AddonBase { class EditorMessage { public type: string; public content: { - itemID?: string; event?: XULEvent; - editorInstance?: EditorInstance + editorInstance?: EditorInstance; }; constructor(type: string, content: object) { this.type = type; diff --git a/src/events.ts b/src/events.ts index ab10267..054fc3e 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,4 +1,5 @@ import { AddonBase, EditorMessage } from "./base"; +import { Knowledge } from "./knowledge"; class AddonEvents extends AddonBase { constructor(parent: Knowledge4Zotero) { @@ -7,11 +8,12 @@ class AddonEvents extends AddonBase { public async onInit() { Zotero.debug("Knowledge4Zotero: init called"); - this.addNoteInstanceListener(); + this._Addon.knowledge = new Knowledge(undefined); + this.addEditorInstanceListener(); this.resetState(); } - public addNoteInstanceListener() { + public addEditorInstanceListener() { Zotero.Notes._registerEditorInstance = Zotero.Notes.registerEditorInstance; Zotero.Notes.registerEditorInstance = (instance: EditorInstance) => { Zotero.Notes._registerEditorInstance(instance); @@ -28,7 +30,10 @@ class AddonEvents extends AddonBase { event: string, message: EditorMessage ) { - let editor: Element = await this._Addon.views.getEditor(instance); + await instance._initPromise; + let editor: Element = this._Addon.views.getEditor( + instance._iframeWindow.document + ); editor.addEventListener(event, (e: XULEvent) => { message.content.event = e; message.content.editorInstance = instance; @@ -36,6 +41,21 @@ class AddonEvents extends AddonBase { }); } + public async addEditorDocumentEventListener( + instance: EditorInstance, + event: string, + message: EditorMessage + ) { + await instance._initPromise; + let doc: Document = instance._iframeWindow.document; + + doc.addEventListener(event, (e: XULEvent) => { + message.content.event = e; + message.content.editorInstance = instance; + this.onEditorEvent(message); + }); + } + public async onEditorEvent(message: EditorMessage) { Zotero.debug( `Knowledge4Zotero: onEditorEvent\n${message.type}\n${message.content}` @@ -64,21 +84,34 @@ class AddonEvents extends AddonBase { "addToKnowledge", "addToKnowledge", "Add Note Link to Knowledge Workspace", - new EditorMessage("addToKnowledge", {}) + new EditorMessage("addToKnowledge", { + itemID: message.content.editorInstance._item.id, + }) ); this.addEditorEventListener( message.content.editorInstance, "click", new EditorMessage("noteEditorClick", {}) ); + if (!message.content.editorInstance._knowledgeSelectionInitialized) { + this.addEditorDocumentEventListener( + message.content.editorInstance, + "selectionchange", + new EditorMessage("noteEditorSelectionChange", {}) + ); + message.content.editorInstance._knowledgeSelectionInitialized = true; + } } else if (message.type === "addToKnowledge") { /* message.content = { editorInstance } */ - // TODO: Complete this part Zotero.debug("Knowledge4Zotero: addToKnowledge"); + this._Addon.knowledge.addLink( + -1, + message.content.editorInstance._item.id + ); } else if (message.type === "setMainKnowledge") { /* message.content = { @@ -128,19 +161,58 @@ class AddonEvents extends AddonBase { } else if (message.type === "noteEditorClick") { let el: XUL.Element = message.content.event.target; if (el.children.length !== 0) { + // This is not a line element return; } - let urlIndex = el.innerHTML.search(/zotero:\/\/note\//g); - if (urlIndex >= 0) { - let noteID = parseInt( - el.innerHTML.substring(urlIndex + "zotero://note/".length) + if ( + el.tagName === "A" && + (el as HTMLLinkElement).href.search(/zotero:\/\/note\//g) >= 0 + ) { + let urlIndex = (el as HTMLLinkElement).href.search( + /zotero:\/\/note\//g ); - let note = Zotero.Items.get(noteID); - if (note && note.isNote()) { - // TODO: Open note - Zotero.debug(`Knowledge4Zotero: noteEditorClick ${note.id}`); + if (urlIndex >= 0) { + let noteID = parseInt( + (el as HTMLLinkElement).href.substring( + urlIndex + "zotero://note/".length + ) + ); + let note = Zotero.Items.get(noteID); + if (note && note.isNote()) { + // TODO: Open note + Zotero.debug(`Knowledge4Zotero: noteEditorClick ${note.id}`); + } } } + } else if (message.type === "noteEditorSelectionChange") { + if ( + message.content.editorInstance._item.id === + Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID") + ) { + // Update current line index + let focusNode = + message.content.editorInstance._iframeWindow.document.getSelection() + .focusNode; + if (!focusNode) { + return; + } + + function getChildIndex(node: Node) { + return Array.prototype.indexOf.call(node.parentNode.childNodes, node); + } + + // Make sure this is a direct child node of editor + while ( + !focusNode.parentElement.className || + focusNode.parentElement.className.indexOf("primary-editor") === -1 + ) { + focusNode = focusNode.parentNode; + } + + let currentLineIndex = getChildIndex(focusNode); + this._Addon.knowledge.currentLine = currentLineIndex; + Zotero.debug(`Knowledge4Zotero: line ${currentLineIndex} selected.`); + } } else { Zotero.debug(`Knowledge4Zotero: message not handled.`); } diff --git a/src/knowledge.ts b/src/knowledge.ts index b8f4c50..7a6d27c 100644 --- a/src/knowledge.ts +++ b/src/knowledge.ts @@ -1,27 +1,104 @@ const TreeModel = require("./treemodel"); class Knowledge { - _note: ZoteroItem; + currentLine: number; constructor(noteItem: ZoteroItem) { - this._note = noteItem; + this.currentLine = 0; // this.createKnowledgeItem(); } // createKnowledgeItem() { // return; // } - addNoteLink(noteItem: ZoteroItem) {} - - getOutline(): Node { - let parser = Components.classes[ - "@mozilla.org/xmlextras/domparser;1" - ].createInstance(Components.interfaces.nsIDOMParser); - let doc = parser.parseFromString(this._note.getNote(), "text/html"); - - let metadataContainer: Element = doc.querySelector( - "body > div[data-schema-version]" + getWorkspaceNote() { + return Zotero.Items.get( + Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID") ); + } + getLines(): string[] { + let note = this.getWorkspaceNote(); + if (!note) { + return []; + } + let noteText: string = note.getNote(); + let containerIndex = noteText.search(/
/g); + if (containerIndex != -1) { + noteText = noteText.substring( + containerIndex + '
'.length, + noteText.length - "
".length + ); + } + let noteLines: string[] = noteText.split("\n").filter((s) => s); + return noteLines; + } + + getLineParent(lineIndex: number = -1): TreeModel.Node { + if (lineIndex < 0) { + lineIndex = this.currentLine; + } + let nodes = this.getOutlineList(); + if (!nodes.length || nodes[0].model.lineIndex > lineIndex) { + // There is no parent node + return undefined; + } else if (nodes[nodes.length - 1].model.lineIndex <= lineIndex) { + return nodes[nodes.length - 1]; + } else { + for (let i = 0; i < nodes.length - 1; i++) { + if ( + nodes[i].model.lineIndex <= lineIndex && + nodes[i + 1].model.lineIndex > lineIndex + ) { + return nodes[i]; + } + } + } + } + + addLine(text: string, lineIndex: number) { + let note = this.getWorkspaceNote(); + if (!note) { + return; + } + let noteLines = this.getLines(); + noteLines.splice(lineIndex, 0, text); + note.setNote(`
${noteLines.join("\n")}
`); + note.saveTx(); + } + + addLineToParent(text: string, lineIndex: number = -1) { + if (lineIndex < 0) { + lineIndex = this.currentLine; + } + let parentNode = this.getLineParent(); + if (!parentNode) { + this.addLine(text, lineIndex); + return; + } + let nodes = this.getOutlineList(); + let i = 0; + for (let node of nodes) { + if (node.model.lineIndex === parentNode.model.lineIndex) { + break; + } + i++; + } + // Get next header line index + i++; + if (i >= nodes.length) { + i = nodes.length - 1; + } + // Add line before next header, which is also the end of current parent header + this.addLine(text, nodes[i].model.lineIndex); + } + + addLink(lineIndex: number, noteID: number) { + this.addLineToParent(`${Zotero.Items.get(noteID).getNoteTitle()}`, lineIndex); + } + + getOutline(): TreeModel.Node { + // See http://jnuno.com/tree-model-js + let metadataContainer = this.parseNoteHTML(); let tree = new TreeModel(); /* tree-model/index.js: line 40 @@ -60,9 +137,35 @@ class Knowledge { return root; } + getOutlineList(doFilter: boolean = true): TreeModel.Node[] { + return this.getOutline().all( + (node) => !doFilter || node.model.lineIndex >= 0 + ); + } + jumpToNote(noteLinke: string) {} export() {} + + parseNoteHTML(): Element { + let note = this.getWorkspaceNote(); + if (!note) { + return undefined; + } + let noteText = note.getNote(); + if (noteText.search(/
${noteText}\n
`; + } + let parser = Components.classes[ + "@mozilla.org/xmlextras/domparser;1" + ].createInstance(Components.interfaces.nsIDOMParser); + let doc = parser.parseFromString(noteText, "text/html"); + + let metadataContainer: Element = doc.querySelector( + "body > div[data-schema-version]" + ); + return metadataContainer; + } } export { Knowledge }; diff --git a/src/views.ts b/src/views.ts index 6802e24..ed728fc 100644 --- a/src/views.ts +++ b/src/views.ts @@ -18,12 +18,8 @@ class AddonViews extends AddonBase { }; } - async getEditor(instance: EditorInstance) { - await instance._initPromise; - let editor = - instance._iframeWindow.document.getElementsByClassName( - "primary-editor" - )[0]; + getEditor(_document: Document) { + let editor = _document.getElementsByClassName("primary-editor")[0]; return editor; } @@ -58,7 +54,8 @@ class AddonViews extends AddonBase { } async scrollToLine(instance: EditorInstance, lineIndex: number) { - let editor = await this.getEditor(instance); + await instance._initPromise; + let editor = this.getEditor(instance._iframeWindow.document); if (lineIndex > editor.children.length) { lineIndex = editor.children.length - 1; } diff --git a/typing/addon.d.ts b/typing/addon.d.ts index 1d5f4e7..2650a72 100644 --- a/typing/addon.d.ts +++ b/typing/addon.d.ts @@ -2,4 +2,5 @@ declare interface Knowledge4Zotero { events: import("../src/events"); views: import("../src/view"); prefs: import("../src/prefs"); + knowledge: import("../src/knowledge").Knowledge; } diff --git a/typing/global.d.ts b/typing/global.d.ts index 0cd60be..32b669c 100644 --- a/typing/global.d.ts +++ b/typing/global.d.ts @@ -84,6 +84,8 @@ declare interface ZoteroItem { isRegularItem: () => boolean; isNote: () => boolean; getNote: () => string; + setNote: (string) => void; + getNoteTitle: () => string; isAttachment: () => boolean; isAnnotation?: () => boolean; itemTypeID: number; @@ -163,6 +165,7 @@ declare class ReaderObj { } declare class EditorInstance { + [attr: string]: any; _iframeWindow: XULWindow; _item: ZoteroItem; _initPromise: Promise; diff --git a/typing/treemodel.d.ts b/typing/treemodel.d.ts new file mode 100644 index 0000000..d7a1a86 --- /dev/null +++ b/typing/treemodel.d.ts @@ -0,0 +1,58 @@ +// Project: https://github.com/joaonuno/tree-model-js +// Definitions by: Abhas Bhattacharya +// TypeScript Version: 2.2 + +declare class TreeModel { + constructor(config?: TreeModel.Config); + + private config: TreeModel.Config; + + parse(model: TreeModel.Model): TreeModel.Node; +} + +declare namespace TreeModel { + class Node { + constructor(config: any, model: Model); + + isRoot(): boolean; + hasChildren(): boolean; + addChild(child: Node): Node; + addChildAtIndex(child: Node, index: number): Node; + setIndex(index: number): Node; + getPath(): Array>; + getIndex(): number; + + walk(options: Options, fn: NodeVisitorFunction, ctx?: object): void; + walk(fn: NodeVisitorFunction, ctx?: object): void; + + all(options: Options, fn: NodeVisitorFunction, ctx?: object): Array>; + all(fn: NodeVisitorFunction, ctx?: object): Array>; + + first(options: Options, fn: NodeVisitorFunction, ctx?: object): Node | undefined; + first(fn: NodeVisitorFunction, ctx?: object): Node | undefined; + + drop(): Node; + + [propName: string]: any; + } + + interface Config { + /** + * The name for the children array property. Default is "children". + */ + childrenPropertyName?: string; + modelComparatorFn?: ComparatorFunction; + [propName: string]: any; + } + + interface Options { + strategy: StrategyName; + } + + type StrategyName = "pre" | "post" | "breadth"; + + type ComparatorFunction = (left: any, right: any) => boolean; + type NodeVisitorFunction = (visitingNode: Node) => boolean; + + type Model = T & { children?: Array> }; +} diff --git a/typing/xul.d.ts b/typing/xul.d.ts index 6c38e89..8f5cfea 100644 --- a/typing/xul.d.ts +++ b/typing/xul.d.ts @@ -68,6 +68,8 @@ declare class ClassList { declare class XULEvent extends Event { public target: XUL.Element; + clientX: number; + clientY: number; } declare class XULWindow extends Window {