diff --git a/module.json b/module.json index f77652d..495e62a 100644 --- a/module.json +++ b/module.json @@ -13,6 +13,7 @@ } ], "esmodules": [ + "src/libwrapper_shim.js", "src/main.js", "src/socket.js" ], diff --git a/src/libwrapper_shim.js b/src/libwrapper_shim.js new file mode 100644 index 0000000..b74a73b --- /dev/null +++ b/src/libwrapper_shim.js @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro + + +'use strict'; + +// A shim for the libWrapper library +export let libWrapper = undefined; + +export const VERSIONS = [1,11,0]; +export const TGT_SPLIT_RE = new RegExp("([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])", 'g'); +export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", 'g'); + +// Main shim code +Hooks.once('init', () => { + // Check if the real module is already loaded - if so, use it + if(globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) { + libWrapper = globalThis.libWrapper; + return; + } + + // Fallback implementation + libWrapper = class { + static get is_fallback() { return true }; + + static get WRAPPER() { return 'WRAPPER' }; + static get MIXED() { return 'MIXED' }; + static get OVERRIDE() { return 'OVERRIDE' }; + + static register(package_id, target, fn, type="MIXED", {chain=undefined}={}) { + const is_setter = target.endsWith('#set'); + target = !is_setter ? target : target.slice(0, -4); + const split = target.match(TGT_SPLIT_RE).map((x)=>x.replace(/\\(.)/g, '$1').replace(TGT_CLEANUP_RE,'')); + const root_nm = split.splice(0,1)[0]; + + let obj, fn_name; + if(split.length == 0) { + obj = globalThis; + fn_name = root_nm; + } + else { + const _eval = eval; + fn_name = split.pop(); + obj = split.reduce((x,y)=>x[y], globalThis[root_nm] ?? _eval(root_nm)); + } + + let iObj = obj; + let descriptor = null; + while(iObj) { + descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name); + if(descriptor) break; + iObj = Object.getPrototypeOf(iObj); + } + if(!descriptor || descriptor?.configurable === false) throw `libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`; + + let original = null; + const wrapper = (chain ?? (type.toUpperCase?.() != 'OVERRIDE' && type != 3)) ? function() { return fn.call(this, original.bind(this), ...arguments); } : function() { return fn.apply(this, arguments); }; + + if(!is_setter) { + if(descriptor.value) { + original = descriptor.value; + descriptor.value = wrapper; + } + else { + original = descriptor.get; + descriptor.get = wrapper; + } + } + else { + if(!descriptor.set) throw `libWrapper Shim: '${target}' does not have a setter`; + original = descriptor.set; + descriptor.set = wrapper; + } + + descriptor.configurable = true; + Object.defineProperty(obj, fn_name, descriptor); + } + } +}); diff --git a/src/main.js b/src/main.js index d1a794f..62ca441 100644 --- a/src/main.js +++ b/src/main.js @@ -3,6 +3,7 @@ import {currentSpeedProvider, getColorForDistanceAndToken, getMovedDistanceFromToken, getRangesFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js"; import {checkDependencies, getHexSizeSupportTokenGridCenter, highlightMeasurementTerrainRuler} from "./compatibility.js"; import {moveEntities, onMouseMove} from "./foundry_imports.js" +import {libWrapper} from "./libwrapper_shim.js"; import {performMigrations} from "./migration.js" import {getMovementHistory, removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js"; import {extendRuler} from "./ruler.js"; @@ -16,8 +17,8 @@ Hooks.once("init", () => { initApi() hookDragHandlers(Token); hookDragHandlers(MeasuredTemplate); - hookKeyboardManagerFunctions() - hookLayerFunctions(); + libWrapper.register("drag-ruler", "KeyboardManager.prototype._handleKeys", forwardIfUnahndled(handleKeys), "MIXED"); + libWrapper.register("drag-ruler", "TokenLayer.prototype.undoHistory", tokenLayerUndoHistory, "WRAPPER"); extendRuler(); @@ -57,59 +58,35 @@ Hooks.on("getCombatTrackerEntryContext", function (html, menu) { menu.splice(1, 0, entry); }); +function forwardIfUnahndled(newFn) { + return function(oldFn, ...args) { + const eventHandled = newFn(...args); + if (!eventHandled) + oldFn(...args); + }; +} + function hookDragHandlers(entityType) { - const originalDragLeftStartHandler = entityType.prototype._onDragLeftStart - entityType.prototype._onDragLeftStart = function(event) { - originalDragLeftStartHandler.call(this, event) - onEntityLeftDragStart.call(this, event) - } - - const originalDragLeftMoveHandler = entityType.prototype._onDragLeftMove - entityType.prototype._onDragLeftMove = function (event) { - if (entityType === Token) - applyGridlessSnapping.call(this, event); - originalDragLeftMoveHandler.call(this, event) - onEntityLeftDragMove.call(this, event) - } - - const originalDragLeftDropHandler = entityType.prototype._onDragLeftDrop - entityType.prototype._onDragLeftDrop = function (event) { - const eventHandled = onEntityDragLeftDrop.call(this, event) - if (!eventHandled) - originalDragLeftDropHandler.call(this, event) - } - - const originalDragLeftCancelHandler = entityType.prototype._onDragLeftCancel - entityType.prototype._onDragLeftCancel = function (event) { - const eventHandled = onEntityDragLeftCancel.call(this, event) - if (!eventHandled) - originalDragLeftCancelHandler.call(this, event) - } + const entityName = entityType.name + libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftStart`, onEntityLeftDragStart, "WRAPPER"); + if (entityType === Token) + libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMoveSnap, "WRAPPER"); + else + libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMove, "WRAPPER"); + libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftDrop`, forwardIfUnahndled(onEntityDragLeftDrop), "MIXED"); + libWrapper.register("drag-ruler", `${entityName}.prototype._onDragLeftCancel`, forwardIfUnahndled(onEntityDragLeftCancel), "MIXED"); } -function hookKeyboardManagerFunctions() { - const originalHandleKeys = KeyboardManager.prototype._handleKeys - KeyboardManager.prototype._handleKeys = function (event, key, up) { - const eventHandled = handleKeys.call(this, event, key, up) - if (!eventHandled) - originalHandleKeys.call(this, event, key, up) - } -} - -function hookLayerFunctions() { - const originalTokenLayerUndoHistory = TokenLayer.prototype.undoHistory; - TokenLayer.prototype.undoHistory = function () { - const historyEntry = this.history[this.history.length - 1]; - return originalTokenLayerUndoHistory.call(this).then((returnValue) => { - if (historyEntry.type === "update") { - for (const entry of historyEntry.data) { - const token = canvas.tokens.get(entry._id); - removeLastHistoryEntryIfAt(token, entry.x, entry.y); - } - } - return returnValue; - }); +async function tokenLayerUndoHistory(wrapped) { + const historyEntry = this.history[this.history.length - 1]; + const returnValue = await wrapped(); + if (historyEntry.type === "update") { + for (const entry of historyEntry.data) { + const token = canvas.tokens.get(entry._id); + removeLastHistoryEntryIfAt(token, entry.x, entry.y); + } } + return returnValue; } function handleKeys(event, key, up) { @@ -180,7 +157,8 @@ function onKeyEscape(up) { return true; } -function onEntityLeftDragStart(event) { +function onEntityLeftDragStart(wrapped, event) { + wrapped(event); const isToken = this instanceof Token; const ruler = canvas.controls.ruler ruler.draggedEntity = this; @@ -218,7 +196,13 @@ function startDragRuler(options, measureImmediately=true) { ruler.measure(destination, options); } -function onEntityLeftDragMove(event) { +function onEntityLeftDragMoveSnap(wrapped, event) { + applyGridlessSnapping.call(this, event); + onEntityLeftDragMove.call(this, wrapped, event); +} + +function onEntityLeftDragMove(wrapped, event) { + wrapped(event); const ruler = canvas.controls.ruler if (ruler.isDragRuler) onMouseMove.call(ruler, event)