I'm struggling with Javascript functionality and want to add jQuery to every page as well as a CDN link. I thought the first could be solved by amending the main theme library - e.g.

global:
  js:
     core/jquery

but this doesn't work. I've sifted through a lot of posts but can find an example of either use case. (I can achieve both of these requirements by CDN url's between <script> tags on a Basic Page.)

Any help much appreciated. Thanks.

Comments

ressa’s picture

I am not a JavaScript developer at all, but it seems like jQuery is still available in Drupal 12, after checking with this command in the Firefox console:

$ var foo = jQuery('H1');
$ foo;
Object { 0: h1.title.page-title, length: 1, prevObject: {…} }
0: <h1 class="title page-title">
length: 1

But you could share your use case here, in case someone can suggest a plain JavaScript solution?

About CDN's, see CDN / externally hosted libraries > Adding assets (CSS, JS) to a Drupal module via *.libraries.yml.

peterk900’s picture

That link was useful.  And what you found out about D12.

Returning to my wish to include jQuery on each page...

This script displays a jQuery dropdown in a Drupal article;

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script><script> 
$(document).ready(function(){
  $("#flip").click(function(){
    $("#panel").slideDown("slow");
  });
});
</script><style> 
#panel, #flip {
  padding: 5px;
  text-align: center;
  background-color: #e5eecc;
  border: solid 1px #c3c3c3;
}
#panel {
  padding: 50px;
  display: none;
}
</style>
<div id="flip">
    Click to slide down panel
</div>
<div id="panel">
    Hello world!
</div>

But when I remove the CDN link to jQuery and update libraries.yml, it fails, without error.

I have cloned Olivero. This is libraries.yml, where I have added a dependency on jQuery.

# Main theme library.
global:
  js:
    js/olivero-clone.js: {}
    dependencies:
      - core/jquery:
  css:
    base:
      css/base/elements.css: {}
    component:
      css/component/block.css: {}
      css/component/breadcrumb.css: {}
      css/component/field.css: {}
      css/component/form.css: {}
      css/component/header.css: {}
      css/component/menu.css: {}
      css/component/messages.css: {}
      css/component/node.css: {}
      css/component/sidebar.css: {}
      css/component/table.css: {}
      css/component/tabs.css: {}
      css/component/buttons.css: {}
    layout:
      css/layout/layout.css: {}
    theme:
      css/theme/print.css: { media: print }

The .info file is unaltered...

name: Olivero_Clone
type: theme
base theme: olivero
description: A flexible theme with a responsive, mobile-first layout.
package: Custom
core_version_requirement: ^10 || ^11
libraries:
  - olivero_clone/global
regions:
  header: Header
  primary_menu: Primary menu
  secondary_menu: Secondary menu
  hero: Hero (full width)
  highlighted: Highlighted
  breadcrumb: Breadcrumb
  social: Social Bar
  content_above: Content Above
  content: Content
  sidebar: Sidebar
  content_below: Content Below
  footer_top: Footer Top
  footer_bottom: Footer Bottom

Can anyone suggest what I'm doing wrong?

ressa’s picture

You're welcome @peterk900, I am glad to hear it was useful.

About the theme, you write "clone", but why not create a sub-theme?

In Olivero's olivero.libraries.yml, the dependencies items are only indented one level (two spaces):

global-styling:
  aggregate_target:
    css: true
  version: VERSION
  fonts:
    fonts/metropolis/Metropolis-Regular.woff2:
[...]
  js:
    js/checkbox.js: {}

  dependencies:           <<<< THIS ONE
    - core/drupal
    - core/once
[...]

But, more importantly -- I do not see jQuery mentioned, maybe jQuery support is added by Drupal core?

peterk900’s picture

Thanks for your continued help on this.

The spacing for the dependencies was wrong. And my words were careless. I am working with an Olivero sub-theme.

I've corrected the spacing problem...

global:
  js:
    js/olivero-clone.js: {}
  dependencies:          
    - core/jquery
    - core/drupal
    - core/once

But it still doesn't work.

I understand that jQuery is part of core in D11 but jQuery UI is not. So I installed the jQuery UI module and cleared cache but, sadly, this has had no effect. 

This link refers to D11, jQuery 4 and Bootstrap theme problems but as I'm not using Bootstrap, I can't see this as relevant.

I'm not sure what else to try!

ressa’s picture

Glad that it got more correct :) Too bad it doesn't work ...

But since jQuery seems to now be difficult to get working in Drupal 11, maybe it is better to aim for a JavaScript solution, hopefully someone has a tip about that.

peterk900’s picture

You may be right. JavaScript today has many of the features which jQuery provides. It’s just I’m used to jQuery!

jaypan’s picture

Your current declaration of dependencies looks correct. I'm sure you cleared your cache after doing it, but did you clear your cache after doing it? Upon doing so, I would look at the generated HTML to verify whether the jquery library is being included. I'm not seeing any reason it shouldn't be. If it's not, this would seem like a bug, but lets start there.

Contact me to contract me for D7 -> D10/11 migrations.

peterk900’s picture

I've only just noticed your response. I'm back on the case and I'll carry out the checks you suggest. 

peterk900’s picture

jQuery is being loaded from core...

<script src="/web/core/assets/vendor/jquery/jquery.min.js?v=4.0.0"></script>

both with and without the presence of  a CDN link.

This code...

if (typeof jQuery !=undefined) { alert(jQuery.fn.jquery);

pops up the CDN version - 3.7.1. 

So it seems to be that the core version of jQuery is loaded when the page is displayed but the script does not use the core code. But a CDN script, as part of page content, is processed and so the jQuery functionality works.

If there is anything I can try to throw light on what's happening, please let me know.

Update

I think the problem may be related to writing HTML/Javascript using Full HTML and in Source mode which means using CKEditor.

When I create a new text format without a text editor and set no options, I find a jQuery example script runs without the need for a CDN - which means it's using the jQuery code which comes from core. At a stroke, this may solve all the above issues!  Interestingly, including a CDN link appears to have no effect. 

I'll play with this a little more and post again but I'm fairly convinced CKEditor was responsible for the problem. 

jaypan’s picture

Ahhhh I see what has happened. I didn't look closely enough at the JavaScript you were using in your code. Drupal includes jQuery when the core/jquery library is included, however it does not alias the jQuery object into the $ variable, in order to prevent conflicts with other libraries. So when you try to do something like:

$("body")

You will get an error that jQuery is not present, for even though jQuery is present in the DOM, the $ alias does not (yet) exist. To use jQuery in raw, global JavaScript, you have to call the jQuery object directly:

jQuery("body")

Using the $ alias in your code in an anonymous function that aliases the jQuery object:

// The code is wrapped in an anonymous function. Two variables
// are passed to this function, $ (jQuery) and Drupal.
(function ($, Drupal) {
  // In here you can use the $ variable:
  $("body").on(() => {
    alert("body has loaded");
  });
// The next line of code closes the anonymous function, then calls
// it immediately, passing in the variables jQuery, and Drupal, which
// are received in the first line of this code.
})(jQuery, Drupal);

I wrote a tutorial for Drupal 7 on using JavaScript that is almost all still entirely valid: https://www.jaypan.com/tutorial/high-performance-javascript-using-drupal...

The only real difference from that tutorial and current Drupal versions is that jQuery $.once() no longer exists, and instead once is used as follows:

$(once('some-arbitrary-key', '.some-selector', context)).each(function () {
  
});

Contact me to contract me for D7 -> D10/11 migrations.

peterk900’s picture

What I wrote earlier about changing Text Format solving the problem isn't repeatable, so maybe it was a cache issue. As I think you realised, changing Text Format does not have any bearing on this issue.

I've enclosed the Javascript in my script in a function as you suggested...

<script>
(function ($, Drupal) { 
$(document).ready(function(){
  $("#flip").click(function(){
    $("#panel").slideDown("slow");
  });
});
})(jQuery, Drupal);
</script>
<style> 
#panel, #flip {
  padding: 5px;
  text-align: center;
  background-color: #e5eecc;
  border: solid 1px #c3c3c3;
}
#panel {
  padding: 50px;
  display: none;
}
</style>
<div id="flip">
    Click to slide down panel
</div>
<div id="panel">
    Hello world!
</div>

But the Javascript fails to run even though the page source shows jQuery is loaded, as my code snippet above shows.  What am I doing wrong? 

jaypan’s picture

I'm guessing it's due to adding the library in script tags, rather than using the Drupal JS API, and adding it as a Drupal library with a dependency on core/jquery. Using the APIs ensure that the files/scripts are loaded in the correct order.

You can read about the APIs here: https://www.drupal.org/docs/drupal-apis/javascript-api/javascript-api-ov...

Main points:

  1. Declare your script as a library, and add a dependence on core/jquery
  2. Declare the library in [theme].info.yml so it is added to each page by Drupal
  3. Convert $(document).ready() to  Drupal.behaviors.

Contact me to contract me for D7 -> D10/11 migrations.

peterk900’s picture

I've done the first two main points and looked at the link you provide for Drupal behaviours. But I'm not sure how to use Drupal.behaviours in a web page.  My research, including looking at the API link you provide,  suggests this is module code which isn't what I want. My use case is creation of an article on the fly where I want a bit of jQuery interactivity. At the moment the only way I can do this in D11 is with a jQuery CDN link - which actually works well and shows no performance hit that I can detect. And a recent web post seems to support this view. That said, I'd still like to find a way I can use Drupal site installed jQuery inside <script> tags or to hear that it simply isn't possible. Can someone share  <script> based code which does what I want and works without requiring module code?  Thanks.

jaypan’s picture

Drupal behaviors is a replacement for $(document).ready(). It's not specific to modules; core, modules and themes can all add JS files, and these files can each add one or more elements to Drupal.behaviors. So the link I gave you is applicable also to your  JS file that you are adding to your theme.

In the background, Drupal core creates the Drupal object, with the behaviors element, early in the pipeline with this code:

window.Drupal = { behaviors: {}, locale: {} };

After that, core, modules, and themes, can add their own elements to the Drupal.behaviors element.

Bringing this all together. This code:

$(document).ready(function () {
  alert('The document has loaded');
});

is replaced in Drupal by adding an element containing an attach() method to the Drupal.behaviors object:

Drupal.behaviors.myModule = {
  attach: function () {
    alert('The document has loaded');
  }
};

When the document (page) has loaded, Drupal loops through all elements of the Drupal.behaviors object, and executes the attach() method for each object. In this case, it will pop up the the alert() text on page load.

Note however that there is one major difference between $(document).ready() and Drupal.behaviors. The former is only called on page load, the latter is called on page load and after any AJAX requests. It is called after AJAX requests, so that handlers can be attached to any new HTML being inserted in the DOM from the AJAX request. Due toDrupal.behaviors being called multiple times, the code below, which looks like it should be fine, can become a problem that requires a fix:

Drupal.behaviors.myModule = {
  attach: function () {
    $(this).click(function (e) {
      e.preventDefault();
      alert('Clicked');
    });
  }
};

With the above code, the click handler is attached on page load, and when a.some-selector is clicked, the alert will fire. But then, if some ajax on happens on the page, the above code is executed again, and the click handler is attached again. So when a.some-selector is clicked with the handler attached twice, the alert is fired twice. If more AJAX is fired, the handler keeps getting attached again again. The fix for this is to use once():

Drupal.behaviors.myModule = {
  attach: function () {
    // Loop through all instances of a.some-selector, and execute the
    // contained code, but only once. This code will not be executed
    // again for Ajax loads.
    $(once('my-module', a.some-selector')).each(function () {
      // Code in here is executed for each instance of a.some-selector,
      // and is only ever executed one time for each instance.
      // $(this) refers to the current iteration of a.some-selector.
      $(this).click(function (e) {
        e.preventDefault();
        alert('Clicked');
      });
    });
  }
};

Selectors found with once('my-module', a.some-selector') are only executed once (tracked using the key passed as the first argument, in this case my-module), so when a.some-selector is first loaded, whether that be on the initial page load, or a subsequent AJAX request, the handler is attached, but then on subsequent AJAX calls, will not be re-attached.

Final structure of the code, wrapped in an anonymous function:

(function ($, Drupal)N {
  Drupal.behaviors.myModule = {
    attach: function () {
      // Loop through all instances of a.some-selector, and execute the
      // contained code, but only once. This code will not be executed
      // again for Ajax loads.
      $(once('my-module', a.some-selector')).each(function () {
        // Code in here is executed for each instance of a.some-selector,
        // and is only ever executed one time for each instance.
        // $(this) refers to the current iteration of a.some-selector.
        $(this).click(function (e) {
          e.preventDefault();
          alert('Clicked');
        });
      });
    }
  };
)(jQuery, Drupal);

Contact me to contract me for D7 -> D10/11 migrations.

peterk900’s picture

Thank you for this detailed explanation. With what you've written and re-reading your D7 article, I'm I now much clearer on Drupal.behaviours and how to use this functionality to achieve what I want. Support like you have provided here and in the past is one key reason why I continue to develop in Drupal.   Your help is much appreciated.

jaypan’s picture

Happy to have helped :) 

Contact me to contract me for D7 -> D10/11 migrations.

peterk900’s picture

I've now got  Drupal.behaviours to write to the browser console just once. Great! And thanks once again.

olivero-clone-1/js/olivero-clone-1.js

# Main theme library.
global:
  js:
    js/olivero-clone-1.js: {}
  dependencies:          
    - core/jquery
    - core/drupal
    - core/once

olivero-clone-1.js

/**
 * @file
 * Olivero Clone 1 behaviors.
 */
(function (Drupal, once) {
'use strict';

  Drupal.behaviors.oliveroClone1 = {
    attach (context, settings) {

       // Select elements and ensure the code runs only once for each
      once('my-once-key', 'html', context).forEach(function (element) {
	 
      console.log('It works new!!');      	      
       
      });

    }
  };
  
} (Drupal, once));

But when I try to modify the code to use jQuery to add a click event to every <p> tag and change the text color, the code fails.

For example, I tried this code which fails completely...

(function ($, once) {
'use strict';

  Drupal.behaviors.oliveroClone1 = {
    attach (context, settings) {

       // Select elements and ensure the code runs only once for each
      once('my-once-key', 'html', context).forEach(function (element) {
          $("p").on("mouseover", function () {
          // Set the text color of
          // this element to red
          $(this).css("color", "red");
          console.log('It works new!!');
      });
    }
  };
} (jQuery, once));

The jQuery code works fine in a Drupal page with a <script> tags for the code and a jQuery CDN.

If you can point out what's wrong with my script that would be great. 

Updated

I've managed to specify jQuery and it's $ alias correctly. And I think the Javascript in the above post was in error. This code now does what I want.

/**
 * @file
 * Olivero Clone 1 behaviors.
 */
(function ($, Drupal, once) {
     
'use strict';

  Drupal.behaviors.oliveroClone1 = {
    attach (context, settings) {

       // Select elements and ensure the code runs only once for each
      once('my-once-key', 'html', context).forEach(function (element) {
        $("p").each(function () {
        $(this).css("color", "red");
        })

          console.log('It works new!!');
      });
    }
  };       

  
} (jQuery, Drupal, once));

So it looks as though the support I've got might really mean I'm getting the hang of Javascript and Drupal!  Thanks again.

jaypan’s picture

You're close, but I would change it to this:

// Select the element you want with once(), and wrap the whole thing in a jQuery wrapper
$(once('my-once-key', 'p', context)).each(function (element) {
  // $(this) refers to the element found with once()
  $(this).css("color", "red");
});

While your code would have worked, it was doing something slightly different; I optimized it a bit.

With the code you had, your call to once() was finding the HTML tag, and then if it finds it, executing on all found <p> tags. Where you would run into a problem is if an ajax call returned an iframe. Iframes contain <html> tags, and I believe your code would fire again, and re-apply the internal code to all found p tags.

The change I made was so that each <p> tag ever loaded on the page, both first page load, and any returned from future ajax calls, will have the color applied once. Then that <p> tag will not be found the next time once('my-once-key', 'p') is called. 

Contact me to contract me for D7 -> D10/11 migrations.

peterk900’s picture

Thanks for this. You've helped me understand 'Once' a bit better. 

jaypan’s picture

Glad to have helped :)

Contact me to contract me for D7 -> D10/11 migrations.