1

To increase usability on a website I want to change the following with Greasemonkey (JavaScript) :

data-bind="text: 'Price: ' + db.totalpr().toFixed(2) + ' GBP'"`

to

data-bind="text: 'Price: ' + db.totalpr().toFixed(2)*current_exchange_rate + ' USD'"`

Had tried

document.body.innerHTML = document.body.innerHTML.replace(text_to_find, text_to_replace)

but a page loses events and no data is being loaded: "Price" loads nothing and stays empty.

Then I've found this: Replace text in a website

function replaceTextOnPage(from, to){
  getAllTextNodes().forEach(function(node){
    node.nodeValue = node.nodeValue.replace(new RegExp(quote(from), 'g'), to);
  });

  function getAllTextNodes(){
    var result = [];

    (function scanSubTree(node){
      if(node.childNodes.length) 
        for(var i = 0; i < node.childNodes.length; i++) 
          scanSubTree(node.childNodes[i]);
      else if(node.nodeType == Node.TEXT_NODE) 
        result.push(node);
    })(document);

    return result;
  }

  function quote(str){
    return (str+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
  }
}

But, unfortunately, it's not working in my case: it can replace "Price" to any text I want but not the

db.totalpr().toFixed(2)

to

"db.totalpr().toFixed(2)*current_exchange_rate"

Any ideas how to make it work without losing events?

Update:

<div class="row">
    <div class="col-md-5">
        <h5 data-bind="text: 'Price: ' + db.totalpr().toFixed(2) + ' GBP'" style="margin-left:7px"></h5>
    </div>
</div>
Brock Adams
  • 90,639
  • 22
  • 233
  • 295
Mr D
  • 23
  • 5
  • Changing innerHTML wipes away everything, only way would be to adjust the elements directly, but if the page used the attributes before your script gets to it, there is not much you can do. – epascarello Apr 03 '18 at 20:04
  • When I change "Price" to "PriceUSD", for example, it shows for a second before data is loaded and then changes again to "Price". – Mr D Apr 03 '18 at 20:13
  • There are some really great resources for i18n. You don't have to reinvent that wheel: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat#Syntax – Randy Casburn Apr 03 '18 at 20:21
  • Link to the target page. We need to see *exactly* how that HTML is emplaced. – Brock Adams Apr 03 '18 at 20:35
  • @BrockAdams it's internal - only accessible within company. – Mr D Apr 03 '18 at 20:51
  • 1
    You can't multiply a string (the result of `toFixed()` is a string) by a number. You probably want `(data.totalpr() * current_exchange_rate).toFixed(2)`. However, I believe what @epascarello is saying, is that you likely can't change the value in the attribute like that. Wait until the value is rendered into the DOM, then parse the value as a number and apply the changes there. – Heretic Monkey Apr 03 '18 at 21:01
  • 1
    Am I right, if I think that page uses knockout.js? – Paul Apr 03 '18 at 21:08

2 Answers2

1

This looks line an "X Y problem". See below the fold...


The question implies replacing attributes, not text. (And, just the attributes so that you don't break that ajax-driven page.)

Since it's ajax-driven, you need something like MutationObserver or waitForKeyElements.

Here's a script that shows how to replace those kinds of attributes:

// ==UserScript==
// @name     _Dynamically replace JS-coded attributes
// @match    *://YOUR_SERVER.COM/YOUR_PATH/*
// @require  https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js
// @require  https://gist.github.com/raw/2625891/waitForKeyElements.js
// @grant    GM_addStyle
// @grant    GM.openInTab 
// ==/UserScript==
//- The @grant directives are needed to restore the proper sandbox.

var current_exchange_rate = 1.41;  //  Hard coded for demo purposes only.

waitForKeyElements ("[data-bind]", ReplacePriceAttributes);

function ReplacePriceAttributes (jNode) {
    // Following could alternatively could use `.data("bind")` since attribute is of `data-` type.
    var oldBind = jNode.attr ("data-bind");
    if (/Price:/.test (oldBind) ) {
        let newBind = oldBind.replace ("db.totalpr().toFixed(2)", `( db.totalpr() * ${current_exchange_rate} ).toFixed(2)`);
        jNode.attr ("data-bind", newBind)
    }
}

current_exchange_rate is hard-coded in the script. Getting live values is beyond the scope of this question and covered elsewhere, anyway.


The real problem:

Replacing those attribute values is very unlikely to accomplish what you really want (displaying prices in USD). This is especially true if the page is driven by Knockout.js (as it looks appears to be).

To change the displayed prices that you see, use a technique very similar to the linked answer...

waitForKeyElements ("[data-bind]", ReplacePriceText);

function ReplacePriceText (jNode) {
    var oldPriceTxt = jNode.text ();
    /* Appropriate HTML not provided by question asker, but convert price text
        as shown in linked answer, setting newPriceTxt
    */
    jNode.text (newPriceTxt);
}
Brock Adams
  • 90,639
  • 22
  • 233
  • 295
  • I am not sure whether this will work. In case it is knockout.js: Knockout bindings are usually applied on initialization and afterwards they are just processed internally. To change that binding, you need to clean the node and reapply the binding to the node with the corresponding data. – Paul Apr 03 '18 at 22:05
  • @Paul, GTK. Do you have a page that uses something like `db.totalpr()` that I can play with? – Brock Adams Apr 03 '18 at 22:11
  • Try there: http://knockoutjs.com/examples/helloWorld.html I will check for a knockout solution in the meantime. :) – Paul Apr 03 '18 at 22:19
  • I assume your code would work now. :) I didn't know waitForKeyElements until now, but I like the idea. The only annyoing issue is probably that you need to parse the value from the data in this case. – Paul Apr 04 '18 at 09:42
  • @BrockAdams Thanks for your code! Had tested it for many hours trying to figure out why it's not changing the values - unfortunately, was not able to make it work and I've done some debugging: it finds the right data-bind, changes what should be changed, but Firefox is not updating what is on my screen - for example I want to change "Price" to "PriceUSD": oldBind shows "Price", newBind shows "PriceUSD" and on my screen I see "Price", not "PriceUSD". We are almost there! – Mr D Apr 05 '18 at 20:10
  • Did you try Paul's code? My code will work, but I can't fill in the details for you, without seeing the actual page. You can save the page ***rendered*** source HTML to some place like pastebin and link to that. – Brock Adams Apr 05 '18 at 20:47
  • Yes I did - see my comment under Paul's code. Unfortunately, we have very strict rules concerning any data leakage :( – Mr D Apr 05 '18 at 21:13
  • Then you will have to adapt the "convert price text as shown in linked answer" part yourself. It's not hard. – Brock Adams Apr 05 '18 at 21:16
  • @BrockAdams I've tried to solve it with a general solution as there are also weight&dimensions - looks similar to GBP -> USD replacement... – Mr D Apr 06 '18 at 09:32
1

In case the page uses knockout.js, I would suggest the following.

Note: This does only work after the bindings have been applied. If you apply your js code before that, a single replacement of the binding should do it. Just like you already did it, but with attention to the ".toFixed(2)" issue (see the comment from Mike McCaughan). If that's the reason why it didn't work you should also see errors in the console log.

$( document ).ready(function() {
  // Their code. Just for demonstration.
  var viewModel = {
    db: {
      totalpr: new ko.observable(123.1234)
    }
  };
  
  ko.applyBindings(viewModel);
  
  // Your Greasemonkey code starts here:
  var current_exchange_rate = 1.41;  //  Hard coded for demo purposes only.
  
  var priceElements = $("h5[data-bind*= 'db.totalpr().toFixed(2)']")

  $.each(priceElements, function(index, value) {
    var data = ko.dataFor(value);

    // Add your new value to their model. Use ko.pureComputed to ensure its changed as soon as totalpr changes.
    data.db.modifiedTotalPr = ko.pureComputed(function () {
      return data.db.totalpr() * current_exchange_rate;
    });
    
    // Update data-bind attribute.
    $(value).attr("data-bind" , "text: 'Price: ' + db.modifiedTotalPr().toFixed(2) + ' USD'")
    
    // Apply binding.
    ko.cleanNode(value)
    ko.applyBindings(data, value);
  });
});
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div class="row">
    <div class="col-md-5">
        <h5 data-bind="text: 'Price: ' + db.totalpr().toFixed(2) + ' GBP'" style="margin-left:7px"></h5>
    </div>
</div>
Brock Adams
  • 90,639
  • 22
  • 233
  • 295
Paul
  • 2,086
  • 1
  • 8
  • 16
  • Interesting to see if the OP responds to this. And if there are side effects when inflated prices are submitted. (This appears to change both presentation *and data*.?.) – Brock Adams Apr 03 '18 at 23:09
  • If their code is well written, they are probably using `db.totalpr()` on submit. The code doesn't effect their existing data. It just adds a custom field. In case that makes any problems, it would be also possible to write a custom little view model with only the modifiedTotalPr observable and to let the data as it is. – Paul Apr 04 '18 at 09:39
  • @Paul Thank you for your code! Unfortunately, can't make it work: the priceElements array has a lot of "garbage" elements in it and the code starting @ $.each(priceElements, function(index, value) never executes in my case... – Mr D Apr 05 '18 at 20:54
  • The priceelements array can only contain garbage in case the selector $("h5[data-bind*= 'db.totalpr().toFixed(2)']")`` (all h5, which have a data-bind which contains db.totalpr().toFixed(2)) applies to many elements. Is that the case? Do you get a console error? – Paul Apr 06 '18 at 06:46
  • @Paul actually it's 1-5 h5 data-bind per page. What error should I get? – Mr D Apr 06 '18 at 12:38
  • Do you want to change them all or only specific ones? You might need to adjust the selector if it's not specific enough. It doesn't make sense, that it's not looping through `priceElements` since you mentioned there are elements in that array. Therefore I would suspect something like `can't read db of undefined`. When does your script run? Before or after initialization of the page? – Paul Apr 06 '18 at 23:17
  • @Paul First I want to change prices - that's my goal #1, then the rest. `can't read db of undefined` haven't seen it in logs. The script is executed via Greasemonkey, so I have no clue when exactly it starts. – Mr D Apr 09 '18 at 10:37
  • Could you update your question with your current code and could you check whether `priceElements` does contain any elements? – Paul Apr 09 '18 at 14:38