0
<body>
  <button onclick="myFunction()">CLICK</button>
  <script>
    function myFunction(event) {
      // do something based on the value of the event's property
    }
  </script>
</body>
<body>
  <button id="myButton">CLICK</button>
  <script>
    var button = document.getElementById("myButton");
    button.addEventListener("click", myFunction);
    function myFunction(event) {
      // do something based on the value of the event's property
    }
  </script>
</body>

I might add click events to the button by the two methods above.

I want to modify some properties of the event object that myFunction will receive before it executes.

For example, in the click event, the event has clientX and clientY properties, and assuming the original values are 100 and 100, I want them to be 150 and 150 (how they change is determined by certain conditions) and the other property values to be the same as the original values.

Is there any way to achieve this?


Additional Information

I needed the modified event because for some reason I added the scale, width style attributes to the body element, which made the position-related coordinate points calculate incorrectly.

Some events related to coordinate points, such as click, drag, are not behaving properly.

Basically, every point-related event object has to be fixed, most notably MouseEvent, PointerEvent.

According to @Mr. Polywhirl, I need to write a function to handle the event object first, and then call the actual callback in the handling function, which is more replicated.

Is it possible to achieve a senseless modification by modifying the addEventListener on the prototype?

If it works, how would a binding like <button onclick="myFunction(event)">CLICK</button> achieve the same event modification?

Try this

Press F12 on this page to open the console, and type document.body.style="scale: 0.8;width: 125%;transform-origin: 0 0;" in the console. Then click on the recent inbox messages at the top of the page, which is probably what you're seeing.

What I need to achieve

I don't want to make any changes to the callback function myFunction

A hypothetical case:

// Original call
<script>
  button.addEventListener("click", myFunction);
  function myFunction(event) {
    // event.clientX maybe 100
  }
</script>


// After applying some magic code
<script>

  // some magic code
  // ...

  button.addEventListener("click", myFunction);
  function myFunction(event) {
    // event.clientX is the original value of 100 and then changed according to some conditions
    // such as expanding it by 1.5 times, now it is 150 (This is what the magic code needs to accomplish)
  }
</script>
lai9fox
  • 59
  • 7
  • The properties of an Event object are read-only. – Pointy Jul 07 '23 at 13:11
  • @lai9fox ... The OP's most recent edit with additional information provided with the new section **What I need to achieve** indicates that the OP is looking for maybe even an already implemented higher abstraction of what can be described as [**method modification**](https://stackoverflow.com/search?q=%5Bjavascript%5D+%22method+modification%22). – Peter Seliger Jul 08 '23 at 08:36

3 Answers3

1

You will need to make a copy of the event, since the clientX and clientY properties are read-only.

Intercept the event in another function, modifiy the event, and send it into the original handler.

var button = document.getElementById('myButton');
button.addEventListener("click", myModifiedFunction);

function myModifiedFunction(event) {
  // The clientX and clientY properties are read-only
  const { clientX, clientY, ...rest } = event;
  
  // We need to make a copy of the event object information
  const eventCopy = { clientX, clientY, ...rest };
  
  // Mutate the (x, y) coordinates with an offset of (1,000, 1,000)
  eventCopy.clientX += 1000;
  eventCopy.clientY += 1000;
  
  // Call the original handler
  myFunction(eventCopy);
}

// Original click handler callback
function myFunction(event) {
  const point = {
    x: event.clientX,
    y: event.clientY
  };
  console.log(`Point: ${JSON.stringify(point)}`);
}
<button onclick="myFunction(event)">CLICK</button>
<button id="myButton">CLICK (MODIFIED)</button>

If you want to properly clone the event, you can try:

var button = document.getElementById('myButton');
button.addEventListener("click", myModifiedFunction);

function myModifiedFunction(event) {
  // We need to make a copy of the event object information
  const eventCopy = cloneEvent(event);
  
  // Mutate the (x, y) coordinates with an offset of (1,000, 1,000)
  Object.assign(eventCopy, {
    clientX: eventCopy.clientX + 1000,
    clientY: eventCopy.clientY + 1000
  });

  // Call the original handler
  myFunction(eventCopy);
}

// Original click handler callback
function myFunction(event) {
  const point = {
    x: event.clientX,
    y: event.clientY
  };
  console.log(`Point: ${JSON.stringify(point)}`);
}

// Adapted from: https://stackoverflow.com/a/39669794/1762224
function cloneEvent(e) {
  if (!e) return;
  const clone = new Function();
  for (let p in e) {
    let d = Object.getOwnPropertyDescriptor(e, p);
    if (d && (d.get || d.set)) Object.defineProperty(clone, p, d);
    else clone[p] = e[p];
  }
  Object.setPrototypeOf(clone, e);
  return clone;
}
<button onclick="myFunction(event)">CLICK</button>
<button id="myButton">CLICK (MODIFIED)</button>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
  • Hey! I just added some information to this question . Any other suggestions? @Mr. Polywhirl – lai9fox Jul 07 '23 at 14:01
  • @lai9fox like [relative/absolute](https://stackoverflow.com/questions/3234256/find-mouse-position-relative-to-element) coordinates? – Mr. Polywhirl Jul 07 '23 at 14:33
  • Press F12 on this page to open the console, and type `document.body.style="scale: 0.8;width: 125%;transform-origin: 0 0;"` in the console. Then click on the recent inbox messages at the top of the page, which is probably what you're seeing. – lai9fox Jul 07 '23 at 14:56
  • @Mr.Polywhirl ... The `event` object does not need to be copied. One just needs to redefine `clientX` and `clientY` directly at the original event reference via e.g. `Object.defineProperties`. – Peter Seliger Jul 07 '23 at 16:54
1

A generic solution with Proxy to modify an event. You can create a global getter object to fix your event values, in the snippet there's a correction to make event.clientX/Y to have the pointer coordinates relative to the target element for all elements inside a container element of choice:

var button = document.getElementById('myButton');

const _add = HTMLElement.prototype.addEventListener;

// monkey patch our event handlers for our canvas element only

HTMLElement.prototype.addEventListener = function(name, fn, ...args){
    return _add.call(this, name, event => {
       if(event instanceof MouseEvent && event.target.closest('#\\$canvas')){
        return proxify(clientEvent, fn)(event);
       }
       return fn.call(this, event);
    });
};


// corrects clientX and clientY to be relative to the target element
const clientEvent = {
  clientX(val){
    const rect = this.target.getBoundingClientRect();
    return Math.round(val - rect.left);
  },
  clientY(val){
    const rect = this.target.getBoundingClientRect();
    return Math.round(val - rect.top);
  }
};

// add mouse handlers for the both canvases
'mousemove mousedown mouseup'.split(' ').forEach(name => $canvas.addEventListener(name, myFunction));
'mousemove mousedown mouseup'.split(' ').forEach(name => $canvas2.addEventListener(name, myFunction));

// Original cliekc handler callback
function myFunction(event) {
  const point = {
    x: event.clientX,
    y: event.clientY
  };
  $log.insertBefore(document.createElement('div'), $log.children[0]).textContent = `Point: ${JSON.stringify(point)}`;
}

function proxify(getters, fn) {
  return obj => {
    const proxy = new Proxy(obj, {
      get(target, prop, receiver){
        if(prop in getters){
          return getters[prop].bind(obj)(obj[prop]);
        }
        return obj[prop];
      }
    });
    return fn(proxy);
  }
}
[id=\$canvas]{
  margin:10px;
  background:lightgray;
  border-radius:4px;
  width:200px;
  height:100px;
}

[id=\$canvas2]{
  margin:10px;
  background:lightgray;
  border-radius:4px;
  width:200px;
  height:100px;
}

body{
  display:flex;
}

[id=\$log]{
  font-size:12px;
}
<div id="$canvas"></div>
<div id="$canvas2"></div>
<div id="$log"></div>
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17
  • Hey! I just added some information to this question .@Alexander Nenashev – lai9fox Jul 07 '23 at 14:00
  • @lai9fox made in suitable for all pointer events – Alexander Nenashev Jul 07 '23 at 15:00
  • Thanks for your answer @Alexander Nenashev Press F12 on this page to open the console, and type `document.body.style="scale: 0.8;width: 125%;transform-origin: 0 0;";` in the console Then click on the `recent inbox messages` at the top of the page, which is probably what you're seeing. This is a brief example of how I modified the body element, unfortunately, the `getBoundingClientRect` calculation is also subject to error. – lai9fox Jul 07 '23 at 15:05
  • @lai9fox updated the answer with monkey patching, so you don't need set event handlers manually – Alexander Nenashev Jul 07 '23 at 15:07
  • @lai9fox if you like my answer you are welcome to accept it (the checkbox) and upvote (the arrow up). that way i'll be rewarded and motivated to help you further... – Alexander Nenashev Jul 07 '23 at 15:08
  • @lai9fox updated with the final solution, looks awesome – Alexander Nenashev Jul 07 '23 at 15:15
1

This or similar tasks could be described as method modification which is the manipulation of a function's/method's control- and/or data flow.

Such behavior of cause can be implemented at a higher abstraction level as e.g. around, before, after, afterThrowing and afterFinally modifiers.

The OP is looking for a modification which happens before the invocation of an original method.

The following provided code shows the original event handling of a click event by a handleClickEvent handler and the manipulated event handling by a modified version of the very same handleClickEvent function. The latter utilizes an also provided possible implementation of a before modifier.

The entire solution then boils down to 3 small functions, the before modifier implementation, the actual/original click handler and an additional function which does manipulate the event data ...

function handleClickEvent(evt) {
  const mouseCoords = {
    x: evt.clientX,
    y: evt.clientY,
  };
  console.log({ mouseCoords });
}
function manipulateEventData([evt]) {
  // - manipulate the original event's data.
  Object.defineProperties(evt, {
    clientX: {
      value: evt.clientX + 1000,
    },
    clientY: {
      value: evt.clientY + 1000,
    },
  });
}

document
  .querySelector(`[data-original-handler]`)
  .addEventListener('click', handleClickEvent);

document
  .querySelector(`[data-modified-handler]`)
  .addEventListener(
    'click',
    modifyBefore(handleClickEvent, manipulateEventData)
  );
<button data-original-handler>original click handler</button>
<button data-modified-handler>modified click handler</button>

<script>
function modifyBefore(proceed, handler, target = null) {
  // - `target` equals the `this` context of the to be modified method.
  return (
    typeof proceed === 'function' &&
    typeof handler === 'function' &&

    function beforeType(...args) {
      // - the target/context of the initial modifier/modification time
      //   still can be overruled by a handler's apply/call time context.
      const context = this ?? target;

      // - invocation of the `before` handler.
      handler.call(context, args);

      // - proceed with invoking the original function/method.
      return proceed.apply(context, args);
    }
  ) || proceed;
}
</script>
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
  • @lai9fox ... The OP might check out the above, late provided but very comfortable solution to the OP's problem. – Peter Seliger Jul 07 '23 at 17:01
  • Hey, adding the same event multiple times on the same element via addEventListener triggers the `redefine property` error. What about bubbling triggered ones? How do I properly remove the added event via removeEventListener?@Peter Seliger – lai9fox Jul 10 '23 at 13:24
  • @lai9fox ... Since the above example code does not show such an error being thrown, one needs to know/see how you're utilizing the approach described in the above answer. Maybe you want to open another (a new) question where you provide exactly the failing use case of yours? – Peter Seliger Jul 10 '23 at 13:32
  • [https://codepen.io/lai9fox/pen/BaGmrgX](https://codepen.io/lai9fox/pen/BaGmrgX) check this out@Peter Seliger – lai9fox Jul 10 '23 at 14:11
  • @lai9fox ... Good, opening the codepen console shows exactly the same result as the stack snippet console. Both do log the event data as expected, the original and the manipulated data's one. No errors are thrown; in none of them. – Peter Seliger Jul 10 '23 at 14:15
  • I apologize for my oversight. Now you can see the mistake.@Peter Seliger – lai9fox Jul 10 '23 at 14:24
  • 1
    @lai9fox ... This has nothing to do with the wrapping approach itself or with registering mutiple handler functions to one and the same event at one and the same object. It rather is the implementation of the data modifying `manipulateEventData` function. Just change its implementation to allowing an redefined property to be `configurable` ... like so ... `function manipulateEventData([evt]) { Object.defineProperties(evt, { clientX: { configurable: true, value: evt.clientX + 1000, }, clientY: { configurable: true, value: evt.clientY + 1000, } }); }`. – Peter Seliger Jul 10 '23 at 15:19
  • @lai9fox ... and regarding yours ... _"How do I properly remove the added event via removeEventListener?"_ ... this is done as always by the correct handler reference. Thus one needs to assign the created, modified handler to e.g a constant like ... `const handleChangedEventData = modifyBefore(handleClickEvent, manipulateEventData);` ... and then ... `... addEventListener('click', handleChangedEventData);` respectively `... removeEventListener('click', handleChangedEventData);`. – Peter Seliger Jul 10 '23 at 21:22