From ef7071266a09055411ef52cbab99926ebc2366fb Mon Sep 17 00:00:00 2001 From: xiangyu <3170102889@zju.edu.cn> Date: Mon, 24 Oct 2022 11:30:34 +0800 Subject: [PATCH] add: resize image --- build.js | 30 ++++++++ package.json | 1 + src/editor/editorController.ts | 9 +++ src/editor/editorScript.ts | 135 +++++++++++++++++++++++++++++++++ src/editor/editorViews.ts | 96 +++++++++++------------ src/zotero/views.ts | 3 + typing/global.d.ts | 1 + 7 files changed, 224 insertions(+), 51 deletions(-) create mode 100644 src/editor/editorScript.ts diff --git a/build.js b/build.js index 8a4a1e1..0500ca6 100644 --- a/build.js +++ b/build.js @@ -112,6 +112,36 @@ async function main() { } s.` ); + await esbuild + .build({ + entryPoints: ["src/editor/editorScript.ts"], + bundle: true, + outfile: path.join(buildDir, "temp/editorScript.js"), + // minify: true, + target: ["firefox60"], + }) + .catch(() => process.exit(1)); + + const optionsScript = { + files: [path.join(buildDir, "addon/chrome/content/scripts/index.js")], + from: [/__placeholder:editorScript.js__/g], + to: [ + fs + .readFileSync(path.join(buildDir, "temp/editorScript.js")) + .toString() + .replace(/\\/g, "\\\\"), + ], + countMatches: true, + }; + + _ = replace.sync(optionsScript); + console.log( + "[Build] Run replace in ", + _.filter((f) => f.hasChanged).map( + (f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}` + ) + ); + const optionsAddon = { files: [ path.join(buildDir, "**/*.rdf"), diff --git a/package.json b/package.json index 31aaa0f..23d9a1d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "compressing": "^1.5.1", "crypto-js": "^4.1.1", "esbuild": "^0.14.34", + "prosemirror-transform": "^1.7.0", "replace-in-file": "^6.3.2", "seedrandom": "^3.0.5", "tree-model": "^1.0.7", diff --git a/src/editor/editorController.ts b/src/editor/editorController.ts index aaeedb0..0d0a0c0 100644 --- a/src/editor/editorController.ts +++ b/src/editor/editorController.ts @@ -26,6 +26,15 @@ class EditorController extends AddonBase { await this.editorPromise.promise; } + injectScripts(_window: Window) { + if (!_window.document.getElementById("betternotes-script")) { + const messageScript = _window.document.createElement("script"); + messageScript.id = "betternotes-script"; + messageScript.innerHTML = `__placeholder:editorScript.js__`; + _window.document.head.append(messageScript); + } + } + recordEditor(instance: Zotero.EditorInstance) { this.editorHistory.push({ instance: instance, diff --git a/src/editor/editorScript.ts b/src/editor/editorScript.ts new file mode 100644 index 0000000..a9a5404 --- /dev/null +++ b/src/editor/editorScript.ts @@ -0,0 +1,135 @@ +// TODO: Move this somewhere else +const { Fragment, Slice } = require("prosemirror-model"); +const { Step, StepResult } = require("prosemirror-transform"); + +class SetAttrsStep extends Step { + // :: (number, Object | null) + constructor(pos, attrs) { + super(); + this.pos = pos; + this.attrs = attrs; + } + + apply(doc) { + let target = doc.nodeAt(this.pos); + if (!target) return StepResult.fail("No node at given position"); + let newNode = target.type.create(this.attrs, Fragment.emtpy, target.marks); + let slice = new Slice(Fragment.from(newNode), 0, target.isLeaf ? 0 : 1); + return StepResult.fromReplace(doc, this.pos, this.pos + 1, slice); + } + + invert(doc) { + let target = doc.nodeAt(this.pos); + return new SetAttrsStep(this.pos, target ? target.attrs : null); + } + + map(mapping) { + let pos = mapping.mapResult(this.pos, 1); + return pos.deleted ? null : new SetAttrsStep(pos.pos, this.attrs); + } + + toJSON() { + return { stepType: "setAttrs", pos: this.pos, attrs: this.attrs }; + } + + static fromJSON(schema, json) { + if ( + typeof json.pos != "number" || + (json.attrs != null && typeof json.attrs != "object") + ) + throw new RangeError("Invalid input for SetAttrsStep.fromJSON"); + return new SetAttrsStep(json.pos, json.attrs); + } +} + +// @ts-ignore +window.SetAttrsStep = SetAttrsStep; + +// @ts-ignore +window.updateImageDimensions = function ( + nodeID, + width, + height, + state, + dispatch +) { + let { tr } = state; + console.log(nodeID, width, height, state, dispatch); + state.doc.descendants((node, pos) => { + if (node.type.name === "image" && node.attrs.nodeID === nodeID) { + tr.step(new SetAttrsStep(pos, { ...node.attrs, width, height })); + tr.setMeta("addToHistory", false); + tr.setMeta("system", true); + dispatch(tr); + return false; + } + }); +}; + +window.addEventListener( + "message", + async (e) => { + if (e.data.type === "exportPDF") { + console.log("exportPDF"); + const container = document.getElementById( + "editor-container" + ) as HTMLElement; + container.style.display = "none"; + + const fullPageStyle = document.createElement("style"); + fullPageStyle.innerHTML = + "@page { margin: 0; } @media print{ body { height : auto; -webkit-print-color-adjust: exact; color-adjust: exact; }}"; + document.body.append(fullPageStyle); + + let t = 0; + let imageFlag = false; + while (!imageFlag && t < 500) { + await new Promise(function (resolve) { + setTimeout(resolve, 10); + }); + imageFlag = !Array.prototype.find.call( + document.querySelectorAll("img"), + (e) => !e.getAttribute("src") || e.style.display === "none" + ); + t += 1; + } + + const editNode = document.querySelector(".primary-editor") as HTMLElement; + const printNode = editNode.cloneNode(true) as HTMLElement; + printNode.style.padding = "20px"; + document.body.append(printNode); + + let printFlag = false; + window.onafterprint = (e) => { + console.log("Print Dialog Closed.."); + printFlag = true; + document.title = "Printed"; + }; + window.onmouseover = (e) => { + if (printFlag) { + document.title = "Printed"; + printNode.remove(); + container.style.removeProperty("display"); + } + }; + document.title = (printNode?.firstChild as HTMLElement).innerText; + console.log(document.title); + window.print(); + } else if (e.data.type === "resizeImage") { + console.log("resizeImage"); + // @ts-ignore + window.updateImageDimensions( + // @ts-ignore + _currentEditorInstance._editorCore.view.state.selection.node.attrs + .nodeID, + e.data.width, + undefined, + // @ts-ignore + _currentEditorInstance._editorCore.view.state, + // @ts-ignore + _currentEditorInstance._editorCore.view.dispatch + ); + } + }, + false +); diff --git a/src/editor/editorViews.ts b/src/editor/editorViews.ts index f074359..2f27516 100644 --- a/src/editor/editorViews.ts +++ b/src/editor/editorViews.ts @@ -324,57 +324,7 @@ class EditorViews extends AddonBase { _window.document.body.append(style); } - if (!_window.document.getElementById("betternotes-script")) { - const messageScript = _window.document.createElement("script"); - messageScript.id = "betternotes-script"; - messageScript.innerHTML = ` - window.addEventListener('message', async (e)=>{ - if(e.data.type === "exportPDF"){ - console.log("exportPDF"); - const container = document.getElementById("editor-container"); - container.style.display = "none"; - - const fullPageStyle = document.createElement("style"); - fullPageStyle.innerHTML = - "@page { margin: 0; } @media print{ body { height : auto; -webkit-print-color-adjust: exact; color-adjust: exact; }}"; - document.body.append(fullPageStyle); - - let t = 0; - let imageFlag = false; - while(!imageFlag && t < 500){ - await new Promise(function (resolve) { - setTimeout(resolve, 10); - }); - imageFlag = !Array.prototype.find.call(document.querySelectorAll('img'), e=>(!e.getAttribute('src') || e.style.display === 'none')); - t += 1; - } - - const editNode = document.querySelector(".primary-editor"); - const printNode = editNode.cloneNode(true); - printNode.style.padding = "20px"; - document.body.append(printNode); - - let printFlag = false; - window.onafterprint = (e) => { - console.log('Print Dialog Closed..'); - printFlag = true; - document.title = "Printed"; - }; - window.onmouseover = (e) => { - if (printFlag) { - document.title = "Printed"; - printNode.remove(); - container.style.removeProperty('display'); - } - }; - document.title = printNode.firstChild.innerText; - console.log(document.title); - window.print(); - } - }, false) - `; - _window.document.head.append(messageScript); - } + this._Addon.EditorController.injectScripts(_window); const moreDropdown: HTMLElement = Array.prototype.filter.call( _window.document.querySelectorAll(".more-dropdown"), @@ -898,6 +848,13 @@ class EditorViews extends AddonBase { return res; }; + const checkImageSelected = () => { + return ( + (instance._iframeWindow as any).wrappedJSObject._currentEditorInstance + ._editorCore.view.state.selection.node?.type?.name === "image" + ); + }; + const elementOptions: XULElementOptions = { tag: "fragment", subElementOptions: [ @@ -907,6 +864,43 @@ class EditorViews extends AddonBase { checkExistanceParent: instance._popup, ignoreIfExists: true, }, + { + tag: "menuitem", + id: "menupopup-resizeImage", + checkExistanceParent: instance._popup, + ignoreIfExists: true, + attributes: [["label", "Resize Image"]], + customCheck: checkImageSelected, + listeners: [ + [ + "command", + (e) => { + const newWidth = parseFloat( + prompt( + "Enter new width (px):", + (instance._iframeWindow as any).wrappedJSObject + ._currentEditorInstance._editorCore.view.state.selection + .node?.attrs?.width + ) + ); + if (newWidth && newWidth > 10) { + instance._iframeWindow.postMessage( + { type: "resizeImage", width: newWidth }, + "*" + ); + } + }, + undefined, + ], + ], + }, + { + tag: "menuseparator", + id: "menupopup-resizeimagesplitter", + checkExistanceParent: instance._popup, + customCheck: checkImageSelected, + ignoreIfExists: true, + }, { tag: "menuitem", id: "menupopup-copylink", diff --git a/src/zotero/views.ts b/src/zotero/views.ts index 4121ea7..58eae1f 100644 --- a/src/zotero/views.ts +++ b/src/zotero/views.ts @@ -386,6 +386,9 @@ class ZoteroViews extends AddonBase { doc.querySelector(`#${options.id}`).remove(); } } + if (options.customCheck && !options.customCheck()) { + return undefined; + } const element = createElement(); if (options.id) { element.id = options.id; diff --git a/typing/global.d.ts b/typing/global.d.ts index d2de67b..dba0b8f 100644 --- a/typing/global.d.ts +++ b/typing/global.d.ts @@ -19,5 +19,6 @@ declare interface XULElementOptions { checkExistanceParent?: HTMLElement; ignoreIfExists?: boolean; removeIfExists?: boolean; + customCheck?: () => boolean; subElementOptions?: Array; }