This issue is part of #1415788: Javascript winter clean-up

There is two files still using eval(): tableheader.js and ajax.js. The fix to tableselect should be straightforward, removing it from ajax.js will be trickier and will probably introduce a small API change (for the best).

Comments

gary4gar’s picture

StatusFileSize
new960 bytes

Patch for tableheader.js. I tested it and sticky table headers do work before & after applying these changes.
Let me know, if its acceptable

nod_’s picture

new Function is exactly the same as eval(). So no, it won't do.

Try going up from where Drupal.settings.tableHeaderOffset is defined, right now it's a string it should be a function that can be called directly without having to use eval.

gary4gar’s picture

StatusFileSize
new720 bytes

That makes sense. If we define Drupal.settings.tableHeaderOffset as an function, then such hackery won't be needed. Now, I need to find where is Drupal.settings.tableHeaderOffset defined.

Thanks for reply

Another patch - version 2. Please ignore this.new Function & eval are NOT the same

  • eval() wroks within the current execution scope and can affect local variables.
  • new Function() cannot affect local variables because the code runs in a separate scope

gary4gar’s picture

StatusFileSize
new708 bytes

Version 3 Patch, one line change. Tested in FF 9.0 no syntax errors & sticky table header do work after the change

droplet’s picture

if Drupal.settings.tableHeaderOffset = 'test'

eval => test()
Drupal.settings.tableHeaderOffset() = tableHeaderOffset() method inside Drupal.settings object

It doesn't same ?

damien tournoud’s picture

Status: Active » Needs work

The intent of the code is to call the function which name is stored in a variable, not to call a function stored in a variable.

nod_’s picture

Turns out, it's complicated who would have guessed :þ. There is a need for API change on ajax.js and tableheader.js.

Tableheader

Let's travel to 2007 when this eval() was introduced.

The issue is related to elements that are displayed fixed on top of the screen. If nothing is done, tableheader will set the header to go on top of the screen, under toolbar and shortcut. That's why the tableHeaderOffset setting was introduced, to have the offset from the top given by a js function that can tell the fixed element's height in order to display the header correctly.

Now there is 2 issues with this code (beside using eval()):

  1. a reference to a js function has nothing to do in PHP, it's dynamic JS it has to be defined/detemined on the JS side, not PHP,
  2. it's not sufficient, overlay_child.js had to monkey-patch it's way around :
    /**
     * Use displacement from parent window.
     */
    Drupal.overlayChild.behaviors.alterTableHeaderOffset = function (context, settings) {
      if (Drupal.settings.tableHeaderOffset) {
        Drupal.overlayChild.prevTableHeaderOffset = Drupal.settings.tableHeaderOffset;
      }
      Drupal.settings.tableHeaderOffset = 'Drupal.overlayChild.tableHeaderOffset';
    };
    
    /**
     * Callback for Drupal.settings.tableHeaderOffset.
     */
    Drupal.overlayChild.tableHeaderOffset = function () {
      var topOffset = Drupal.overlayChild.prevTableHeaderOffset ? eval(Drupal.overlayChild.prevTableHeaderOffset + '()') : 0;
    
      return topOffset + parseInt($(document.body).css('marginTop'));
    };

There should be a js-only solution to this problem. A script should only have to register it's function to tableheader that will go through the array of registered function summing it's results to end up with the right offset.

Ajax

This actually was introduced in ahah.js (yeah it's old too, both were a few days apart).

I'm a bit lost, I couldn't find code using this, to me that's dead code that should be removed, unless someone tell me which module uses it. The problem and solutions are the same as above, lucky.

gary4gar’s picture

in that case how about:

window[ Drupal.settings.tableHeaderOffset ]() assuming it's a global function?

In order to make that easier and provide some flexibility, here is a convenience function:

function executeFunctionByName(functionName, context /*, args */) {
  var args = Array.prototype.slice.call(arguments).splice(2);
  var namespaces = functionName.split(".");
  var func = namespaces.pop();
  for(var i = 0; i < namespaces.length; i++) {
    context = context[namespaces[i]];
  }
  return context[func].apply(this, args);
}

calling it as

executeFunctionByName("Namespace.functionName", window, arguments);

Source: http://stackoverflow.com/a/359910/367985

nod_’s picture

No that won't work, the API needs to be changed it's flawed.

nod_’s picture

StatusFileSize
new5.34 KB

Here is a patch, a side effect is that It should be much smoother to scroll down a module list with a lot of packages now, eval was used at the worst possible place.

the API change is that Drupal.settings.tableHeaderOffset is now an array added by the theme_table function, where modules add their offset function, I got rid of overlay monkey-patch. Too bad jQuery doesn't have a .reduce() function that would have helped :þ.

I'm still looking for code that uses this.progress.update_callback in ajax.js. In the meantime please review this.

(edit) removed a bit of exaggeration :)

damien tournoud’s picture

Status: Needs work » Needs review

#10 looks like a decent change, although I really doubt it would lead to any significant performance improvement :)

aspilicious’s picture

+++ b/core/modules/overlay/overlay-child.jsundefined
@@ -173,20 +173,17 @@ Drupal.overlayChild.behaviors.shortcutAddLink = function (context, settings) {
+  return parseInt($(document.body).css('marginTop'), 10);

Are you hardcoding the offset in code? Isn't that bad practice?

(maybe I just don't know what it does :p )

-27 days to next Drupal core point release.

nod_’s picture

that's not an offset that's the base the number will be parsed with/in. it's because parseInt('08') gives an… interesting result. just fool-proofing this bit of code.

sun’s picture

Title: Remove eval() from JS files » Remove eval() from tableHeader JavaScript
Status: Needs review » Needs work
Issue tags: +Needs manual testing

I'm still looking for code that uses this.progress.update_callback in ajax.js.

That should be a separate issue. The Batch API / batch.js was originally supposed to use ajax.js, but we never got around to implement that.

You likely won't find usage of update_callback in core, but CTools in contrib might. In any case, let's move this to a separate issue.

+++ b/core/includes/theme.inc
@@ -1798,6 +1798,7 @@ function theme_table($variables) {
+    drupal_add_js(array('tableHeaderOffset' => array()), 'setting');

Doesn't tableheader already have a dedicated settings object in Drupal.settings?

+++ b/core/misc/tableheader.js
@@ -104,4 +104,20 @@ Drupal.tableHeader.prototype.eventhandlerRecalculateStickyHeader = function (eve
+ * Allow plugins to define

?

+++ b/core/misc/tableheader.js
@@ -104,4 +104,20 @@ Drupal.tableHeader.prototype.eventhandlerRecalculateStickyHeader = function (eve
+  var localOffset, i, imax = headerOffset && headerOffset.length;
+  if (imax) {
+    for (i = 0; i < imax; i += 1) {

unless I'm terribly mistaken, the assigned expression makes imax a Boolean value, so I'm not sure how i < imax can work (aside from counting from 0 to true == 1)...?

nod_’s picture

1) the var was defined by core/modules/toolbar/toolbar.module which has nothing to do with tableheader, I took it out and forced the empty array in the theme function to avoid painful checking on the JS side.

2) yeah wasn't inspired for comments.

3) It's more complicated, it's basically an inline if, expanded it's the same as :

var imax;
if (headerOffset) {
  imax = headerOffset.length;
}
else {
  imax = false;
}

// this works too
var value = condition && myDynamicValue();

It's to be extra sure Drupal.settings.tableHeaderOffset is defined and that the script won't choke on it.

About ajax.js since it's the same problem with the same solution I didn't think i needed a separate issue.

sun’s picture

I took it out and forced the empty array in the theme function to avoid painful checking on the JS side.

We don't have a coding standard for this yet, but in all of my core code and contrib modules, I preferred a conditional JS initialization. That is, because Drupal.settings produces code in the HTML page, and thus, even if the setting is empty - whereas the JS is not only cached but also aggregated/optimized. It's a very small thing when only looking at one setting, but when projecting it to many settings that might be empty, it starts to make a difference.

In a sense, it also avoids a dependency in the JS on the setting to be there, which in turn avoids JS errors.

it's basically an inline if

I'd avoid the separate imax variable for readability and clarity then:

  var localOffset, i;
  if (headerOffset && headerOffset.length) {
    for (i = 0; i < headerOffset.length; i++) {
nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new5.08 KB

That makes sense, I like when js gets out from PHP.

met you halfway for the imax thing.

Comments might still need some work, i'm just not very good at it.

nod_’s picture

Issue tags: +JavaScript clean-up
nod_’s picture

Status: Needs review » Needs work

I broke stuff when the table gets reloaded by ajax.

nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new5.14 KB

Working patch. Did some profiling while scrolling on the module page (with just the list of core modules). What I'm looking at is the time spent in the function eventhandlerRecalculateStickyHeader which is called on page scroll, even if it's not scrolling over a table.

I'm scrolling gently on the module page like you'd do to look at modules titles (takes around 10 seconds). Below is the average time spent on this function, used Firebug profiler, google chrome profiler gives similar results.

Patched

  • without overlay: 0.757ms
  • with overlay: 0.622ms

Eval version

  • without overlay: 1.358ms
  • with overlay: 1.263ms

It's twice as fast now. Just need to check I haven't broken other things, ajax and overlay is working obviously :)

Oh and it looks like two binds are useless: drupalDisplaceAnchor.drupal-tableheader, drupalDisplaceFocus.drupal-tableheader. I didn't get how there were called.

nod_’s picture

Just tested on a website with around 70 tables on the module page (don't ask) half of that with sticky headers. It makes a very significant difference, so real world usage +1.

droplet’s picture

Status: Needs review » Needs work
+++ b/core/misc/tableheader.jsundefined
@@ -104,4 +112,23 @@ Drupal.tableHeader.prototype.eventhandlerRecalculateStickyHeader = function (eve
+        count += 1;

unused var ?

(needs reroll)

26 days to next Drupal core point release.

nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new5.07 KB

You're right, here it is fixed.

cosmicdreams’s picture

@nod_ are you sure you're not changing the functionality of Drupal.overlayChild.tableHeaderOffset ? That seems like a fundamentally different calculation.

nod_’s picture

topoffset was always 0, because Drupal.overlayChild.prevTableHeaderOffset (which is Drupal.settings.tableHeaderOffset) is always undefined. what is defined here is top.Drupal.settings.tableHeaderOffset. there is an iframe involve and two different Drupal objects.

So no, i'm not changing the calculation. Moreover, Drupal.settings.tableHeaderOffset is an object now, you can add as many functions you want and they'll be summed up.

(edit) Oh and $(document.body).css('marginTop') is actually calculated earlier in the iframe creation and guess what it's calculated from… yes, the toolbar size.

It's just a big mess, this patch solve the eval issue, not the crazy way everything is calculated, that'll be a followup patch.

cosmicdreams’s picture

@nod_ Gotcha. Thanks for the head's up. This patch does succeed in removing the evals from all Drupal-authored javascript. Is there a table javascript followup already created?

nod_’s picture

I'd like to keep talking in this issue: #1440628: [Meta] javascript toolbar/tableheader with url fragment mess there are links to other issues related there.

nod_’s picture

Status: Needs review » Needs work

The last submitted patch, core-kill-evil-eval-1417378-28-D7.patch, failed testing.

nod_’s picture

Status: Needs work » Needs review

yeah yeah, failling patch is for D7.

klonos’s picture

jcisio’s picture

Status: Needs review » Reviewed & tested by the community

Tested D8 patch. I really like this direction: embed the offset in each module JS (so it is added up to Drupal.settings) instead of using inline Drupal.settings code.

Not sure if we need a change record for this.

sun’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/misc/tableheader.js
@@ -1,6 +1,12 @@
 /**
+ * Object of functions indexed by module name that will be used to
+ * compute the header offset.
+ */
+$.extend(true, Drupal.settings, {tableHeaderOffset: {}});

Do I need to understand why .extend() is used here? It doesn't seem to merge anything in? So why .extend() and not:

Drupal.settings.tableHeaderOffset = Drupal.settings.tableHeaderOffset || {};

In any case, the code is missing a space before and after tableHeaderOffset: {}.

+++ b/core/misc/tableheader.js
@@ -81,15 +88,16 @@ Drupal.tableHeader.prototype.eventhandlerRecalculateStickyHeader = function (eve
+  this.computeStickyOffsetTop();

I'd love to see a follow-up issue to investigate whether we can generalize this approach, in order to make it work for other "sticky" things on the page (e.g., toolbar, admin_menu, etc), not only tableHeader.

+++ b/core/modules/overlay/overlay-child.js
@@ -175,20 +175,15 @@ Drupal.overlayChild.behaviors.shortcutAddLink = function (context, settings) {
+Drupal.overlayChild.behaviors.tableHeaderOffset = function (context, settings) {
+  $.extend(true, Drupal.settings, {tableHeaderOffset: {overlay: Drupal.overlayChild.tableHeaderOffset}});
 };

1) Why .extend() again?

2) Missing spaces also here.

+++ b/core/modules/overlay/overlay-child.js
@@ -175,20 +175,15 @@ Drupal.overlayChild.behaviors.shortcutAddLink = function (context, settings) {
 Drupal.overlayChild.tableHeaderOffset = function () {
-  var topOffset = Drupal.overlayChild.prevTableHeaderOffset ? eval(Drupal.overlayChild.prevTableHeaderOffset + '()') : 0;
-
-  return topOffset + parseInt($(document.body).css('marginTop'));
+  return parseInt($(document.body).css('marginTop'), 10);
 };

I'm not sure why Overlay takes into account the marginTop of the page. That looks bogus to me - this implementation shouldn't care for that. If anything, then it would be computeStickyOffsetTop().

+++ b/core/modules/toolbar/toolbar.js
@@ -6,7 +6,8 @@ Drupal.toolbar = Drupal.toolbar || {};
+    $.extend(true, settings, {tableHeaderOffset: {toolbar: Drupal.toolbar.height}});

settings.tableHeaderOffset.toolbar = Drupal.toolbar.height;

?

Still not sure why or how .extend() is useful here.

If the idea is to allow overrides by other modules, then the settings would have to merged in last?

However, I guess that custom overrides can be easily handled via JS file weights/ordering when added to the page.

nod_’s picture

All right I think I haven't explained enough. I'm changing the nature of the tableHeaderOffset setting.

Before it was a string that we called with eval() later on.
Drupal.settings.tableHeaderOffset = 'Drupal.toolbar.height';

This is not flexible. If another module, say overlay wants to add an offset it has to do monkey patching.

What it is now is an object of function:

Drupal.settings.tableHeaderOffset = {
  "toolbar": Drupal.toolbar.height, // this is a function!
  "overlay": Drupal.overlayChild.tableHeaderOffset // another function
};

And in the tableheader script, we just sum all the functions of this object. I choose an object instead of an array to be able to modify all that and avoid duplicates. extend is just an easy way to merge things and create what is needed if it doesn't exists. Since you can have several values, the || trick wouldn't work here.

Overlay is looking for the wrong value at the wrong place. Turns out this margin comes from toolbar to begin with, it should look for parent.Drupal.tableHeaderOffset instead of that. I'll change the patch :) didn't want to move things around too much for this patch.

So i'm actually making that extensible and contrib friendly. Which it is not right now.

The follow-up to make this generalized is to use a data-offset-top attribute on the right HTML elements and expose an event that scripts can be bound to and react in case the value changes. the values would be fetched from a "[data-offset-top]" CSS selector so that you won't have to care about which element it is.

There are other solution but this one would be the simplest from a code and extensibility perspective. I think there is an issue already i'll look it up.

nod_’s picture

Status: Needs work » Needs review
StatusFileSize
new5.09 KB

reroll

nod_’s picture

Status: Needs review » Closed (duplicate)
nod_’s picture

Issue summary: View changes

add reference to meta issue