mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-10-31 10:07:15 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			287 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			287 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { createElement, onDestroy } from "../../lib/skeleton/index.js";
 | |
| import rxjs, { effect } from "../../lib/rx.js";
 | |
| import ajax from "../../lib/ajax.js";
 | |
| import { qs } from "../../lib/dom.js";
 | |
| import { join } from "../../lib/path.js";
 | |
| import { loadJS, loadCSS } from "../../helpers/loader.js";
 | |
| import { buttonDownload } from "../../pages/viewerpage/component_menubar.js";
 | |
| import { $ICON } from "../../pages/viewerpage/common_fab.js";
 | |
| import { save } from "../../pages/viewerpage/model_files.js";
 | |
| import "../../components/fab.js";
 | |
| 
 | |
| import { $toolbar } from "./lib/dom.js";
 | |
| 
 | |
| await loadCSS(import.meta.url, "./loader_lowa.css");
 | |
| 
 | |
| let $canvas = null;
 | |
| window.Module = {
 | |
|     uno_scripts: [join(import.meta.url, "./lib/lowa/zeta.js"), join(import.meta.url, "./loader_lowa.uno.js")],
 | |
|     locateFile: (path, prefix) => (prefix || join(import.meta.url, "./lib/lowa/")) + path,
 | |
| };
 | |
| 
 | |
| export default async function(render, { mime, getDownloadUrl, getFilename, $menubar, acl$ }) {
 | |
|     const canWrite = (await acl$.toPromise()).indexOf("POST") >= 0;
 | |
|     const $page = createElement(`
 | |
|         <div class="component_word">
 | |
|             <canvas id="qtcanvas" contenteditable="${canWrite}" style="visibility:hidden"></canvas>
 | |
|             <button is="component-fab" class="hidden"></button>
 | |
|         </div>
 | |
|     `);
 | |
|     render($page);
 | |
| 
 | |
|     // feature1: init
 | |
|     const filename = getFilename();
 | |
|     const $fab = qs($page, `[is="component-fab"]`);
 | |
|     const $qcanvas = qs($page, "canvas");
 | |
|     if ($canvas) {
 | |
|         $qcanvas.remove();
 | |
|         $page.appendChild($canvas);
 | |
|     } else {
 | |
|         $canvas = $qcanvas;
 | |
|     }
 | |
|     Object.assign($canvas.style, {
 | |
|         width: "100%",
 | |
|         height: "100%",
 | |
|     });
 | |
| 
 | |
|     // feature2: toolbar init
 | |
|     if (canWrite) {
 | |
|         $menubar.add(buttonDownload(filename, getDownloadUrl()));
 | |
|         if (isWriter(mime)) {
 | |
|             $menubar.add($toolbar.bullet);
 | |
|             $menubar.add($toolbar.alignment);
 | |
|             $menubar.add($toolbar.title);
 | |
|         }
 | |
|         $menubar.add($toolbar.size);
 | |
|         $menubar.add($toolbar.strike);
 | |
|         $menubar.add($toolbar.underline);
 | |
|         $menubar.add($toolbar.italic);
 | |
|         $menubar.add($toolbar.bold);
 | |
|         $menubar.add($toolbar.color);
 | |
|     }
 | |
| 
 | |
|     // feature3: setup lowa
 | |
|     window.Module.canvas = $canvas;
 | |
|     await loadJS(import.meta.url, "./lib/lowa/soffice.js");
 | |
|     let port = await Module.uno_main;
 | |
|     onDestroy(() => {
 | |
|         $canvas.style.visibility = "hidden";
 | |
|         port.postMessage({ cmd: "destroy", mime });
 | |
|     });
 | |
| 
 | |
|     // feature4: display rule for save button
 | |
|     const action$ = new rxjs.Subject();
 | |
|     if (canWrite) effect(rxjs.merge(rxjs.fromEvent($canvas, "keyup"), action$).pipe(rxjs.tap(() => {
 | |
|         $fab.classList.remove("hidden");
 | |
|         $fab.render($ICON.SAVING);
 | |
|         $fab.onclick = () => {
 | |
|             $fab.render($ICON.LOADING);
 | |
|             $fab.disabled = true;
 | |
|             port.postMessage({ cmd: "save" });
 | |
|         };
 | |
|     })));
 | |
| 
 | |
|     // feature5: load file
 | |
|     await effect(ajax({ url: getDownloadUrl(), responseType: "arraybuffer" }).pipe(
 | |
|         rxjs.mergeMap(async ({ response }) => {
 | |
|             try { FS.mkdir("/tmp/office/"); } catch {}
 | |
|             await FS.writeFile("/tmp/office/" + filename , new Uint8Array(response));
 | |
|             await port.postMessage({ cmd: "load", filename, mime });
 | |
|             onDestroy(() => FS.unlink("/tmp/office/" + filename));
 | |
|             $canvas.focus();
 | |
|         }),
 | |
|     ));
 | |
|     await new Promise((resolve) => {
 | |
|         port.onmessage = function(e) {
 | |
|             switch (e.data.cmd) {
 | |
|             case "loaded":
 | |
|                 window.dispatchEvent(new Event("resize"));
 | |
|                 setTimeout(() => {
 | |
|                     resolve();
 | |
|                     $canvas.style.visibility = "visible";
 | |
|                 }, 250);
 | |
|                 break;
 | |
|             case "save":
 | |
|                 const bytes = FS.readFile("/tmp/office/" + filename);
 | |
|                 effect(save(new Blob([bytes], {})).pipe(rxjs.tap(() => {
 | |
|                     $fab.classList.add("hidden");
 | |
|                     $fab.render($ICON.SAVING);
 | |
|                     $fab.disabled = false;
 | |
|                 })));
 | |
|                 break;
 | |
|             case "setFormat":
 | |
|                 switch(e.data.id) {
 | |
|                 case "Bold":
 | |
|                     e.data.state ? $toolbar.bold.classList.add("active") : $toolbar.bold.classList.remove("active");
 | |
|                     break;
 | |
|                 case "Italic":
 | |
|                     e.data.state ? $toolbar.italic.classList.add("active") : $toolbar.italic.classList.remove("active");
 | |
|                     break;
 | |
|                 case "Underline":
 | |
|                     e.data.state ? $toolbar.underline.classList.add("active") : $toolbar.underline.classList.remove("active");
 | |
|                     break;
 | |
|                 case "Strikeout":
 | |
|                     e.data.state ? $toolbar.strike.classList.add("active") : $toolbar.strike.classList.remove("active");
 | |
|                     break;
 | |
|                 case "LeftPara":
 | |
|                     if (e.data.state) qs($toolbar.alignment, "select").value = "left";
 | |
|                     break;
 | |
|                 case "RightPara":
 | |
|                     if (e.data.state) qs($toolbar.alignment, "select").value = "right";
 | |
|                     break;
 | |
|                 case "CenterPara":
 | |
|                     if (e.data.state) qs($toolbar.alignment, "select").value = "center";
 | |
|                     break;
 | |
|                 case "JustifyPara":
 | |
|                     if (e.data.state) qs($toolbar.alignment, "select").value = "justify";
 | |
|                     break;
 | |
|                 case "DefaultBullet":
 | |
|                     qs($toolbar.bullet, "select").value = e.data.state ? "ul" : "normal";
 | |
|                     break;
 | |
|                 case "DefaultNumbering":
 | |
|                     qs($toolbar.bullet, "select").value = e.data.state ? "ol" : "normal";
 | |
|                     break;
 | |
|                 case "StyleApply":
 | |
|                     let value = "normal";
 | |
|                     if (e.data.state === "Title") value = "title";
 | |
|                     else if (e.data.state === "Heading 1") value = "head1";
 | |
|                     else if (e.data.state === "Heading 2") value = "head2";
 | |
|                     else if (e.data.state === "Heading 3") value = "head3";
 | |
|                     qs($toolbar.title, "select").value = value;
 | |
|                     break;
 | |
|                 case "Color":
 | |
|                     const hex = e.data.state && e.data.state > 0 ? "#" + e.data.state.toString(16).padStart(6, "0") : "#000000";
 | |
|                     $toolbar.color.children[0].style.fill = hex;
 | |
|                     $toolbar.color.children[1].value = hex;
 | |
|                     break;
 | |
|                 case "FontHeight":
 | |
|                     const fontSize = e.data.state;
 | |
|                     qs($toolbar.size, "input").value = fontSize;
 | |
|                     break;
 | |
|                 default:
 | |
|                     console.log("format", e);
 | |
|                     throw new Error("Unknown format");
 | |
|                 }
 | |
|                 $canvas.focus();
 | |
|                 break;
 | |
|             default:
 | |
|                 console.log("message", e);
 | |
|                 throw new Error("Unknown message");
 | |
|             }
 | |
|         };
 | |
|     });
 | |
| 
 | |
|     // feature6: toolbar events
 | |
|     $toolbar.bold.onclick = () => {
 | |
|         $toolbar.bold.classList.toggle("active");
 | |
|         action$.next();
 | |
|         port.postMessage({ cmd: "toggleFormatting", id: "Bold" });
 | |
|     };
 | |
|     $toolbar.italic.onclick = () => {
 | |
|         $toolbar.italic.classList.toggle("active");
 | |
|         action$.next();
 | |
|         port.postMessage({ cmd: "toggleFormatting", id: "Italic" });
 | |
|     };
 | |
|     $toolbar.underline.onclick = () => {
 | |
|         $toolbar.underline.classList.toggle("active");
 | |
|         action$.next();
 | |
|         port.postMessage({ cmd: "toggleFormatting", id: "Underline" });
 | |
|     };
 | |
|     $toolbar.bullet.onchange = (e) => {
 | |
|         switch(e.target.value) {
 | |
|         case "normal":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "RemoveBullets" });
 | |
|             break;
 | |
|         case "ul":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "DefaultBullet" });
 | |
|             break;
 | |
|         case "ol":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "DefaultNumbering" });
 | |
|             break;
 | |
|         }
 | |
|         action$.next();
 | |
|     };
 | |
|     $toolbar.strike.onclick = () => {
 | |
|         $toolbar.strike.classList.toggle("active");
 | |
|         action$.next();
 | |
|         port.postMessage({ cmd: "toggleFormatting", id: "Strikeout" });
 | |
|     };
 | |
|     $toolbar.alignment.onchange = (e) => {
 | |
|         switch(e.target.value) {
 | |
|         case "left":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "LeftPara" });
 | |
|             break;
 | |
|         case "right":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "RightPara" });
 | |
|             break;
 | |
|         case "center":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "CenterPara" });
 | |
|             break;
 | |
|         case "justify":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "JustifyPara" });
 | |
|             break;
 | |
|         default:
 | |
|             throw new Error("Unknown tool alignment");
 | |
|         }
 | |
|         action$.next();
 | |
|     };
 | |
|     $toolbar.title.onchange = (e) => {
 | |
|         switch(e.target.value) {
 | |
|         case "normal":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "StyleApply?Style:string=Standard&FamilyName:string=ParagraphStyles" });
 | |
|             break;
 | |
|         case "title":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "StyleApply?Style:string=Title&FamilyName:string=ParagraphStyles" });
 | |
|             break;
 | |
|         case "head1":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "StyleApply?Style:string=Heading 1&FamilyName:string=ParagraphStyles" });
 | |
|             break;
 | |
|         case "head2":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "StyleApply?Style:string=Heading 2&FamilyName:string=ParagraphStyles" });
 | |
|             break;
 | |
|         case "head3":
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: "StyleApply?Style:string=Heading 3&FamilyName:string=ParagraphStyles" });
 | |
|             break;
 | |
|         default:
 | |
|             throw new Error("Unknown text style");
 | |
|         }
 | |
|         action$.next();
 | |
|     };
 | |
|     $toolbar.color.onclick = (e) => {
 | |
|         if (e.target.tagName === "INPUT") return;
 | |
|         const $svg = e.target.closest("svg")
 | |
|         const $input = $svg.nextElementSibling;
 | |
|         $input.onchange = (e) => {
 | |
|             $svg.style.fill = e.target.value;
 | |
|             const color = parseInt(e.target.value.slice(1), 16);
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: `Color?Color:long=${color}` })
 | |
|         };
 | |
|         $input.click();
 | |
|         action$.next();
 | |
|     };
 | |
|     effect(rxjs.fromEvent(qs($toolbar.size, "input"), "keyup").pipe(
 | |
|         rxjs.debounceTime(250),
 | |
|         rxjs.tap((e) => {
 | |
|             const fontSize = parseInt(e.target.value);
 | |
|             port.postMessage({ cmd: "toggleFormatting", id: `FontHeight?FontHeight.Height:float=${fontSize}` });
 | |
|             action$.next();
 | |
|         }),
 | |
|     ));
 | |
| 
 | |
|     // feature7: workaround known lowa bug
 | |
|     // - when pressing escape, lowa goes out of fullscreen and show some unwanted stuff
 | |
|     // - context menu functions like "replace" image which does crash everything with errors generated from soffice.js
 | |
|     // - ctrl + s is broken
 | |
|     effect(rxjs.fromEvent($page, "keydown", { capture: true }).pipe(rxjs.tap((e) => {
 | |
|         if (e.key === "Escape") e.stopPropagation();
 | |
|         if (e.key === "s" && e.ctrlKey) e.stopPropagation();
 | |
|     })));
 | |
|     effect(rxjs.fromEvent($page, "mousedown", { capture: true }).pipe(rxjs.tap((e) => {
 | |
|         if (e.which === 3) e.stopPropagation();
 | |
|     })));
 | |
| }
 | |
| 
 | |
| function isWriter(mime) {
 | |
|     return ["application/word", "application/msword", "application/rtf", "application/vnd.oasis.opendocument.text"].indexOf(mime) >= 0;
 | |
| }
 | 
