Need to process numbers in js in a HtmlDialog but js doesn't cope with SketchUp format length strings

I need to process user entered numbers in a HtmlDialog but if the numbers are in SketchUp format length strings then javascript can’t deal with them. I’m tempted not to use SketchUp format lengths but stick to javascript decimals for user input but I thought I’d ask in case anyone had a solution. The Extension Warehouse reviewers are critical of extensions which don’t use SketchUp format lengths.

It would be great if it were possible to somehow pass the user entered lengths back to SketchUP Ruby, turn them there into decimal inches and then pass that back to the dialog for processing but I don’t know how to do that, or even if it is possible.

You can register an action callback for sending a string from JavaScript to Ruby, parse it as a length and make some changes to it, and execute a JavaScript to send the result back to the dialog.

What is the use case for modifying the lengths on the JS side?

I’ve written a visual design tool for window design which lets you very quickly create different window layouts in the dialog and then when you are satisfied it gets turned into a 3d SketchUp component.

That sounds interesting. How do you do that?

Some years ago for another purpose I wrote two JavaScript functions to convert between foot-inch-fraction lengths and decimal inches (in both directions).

Here they are, zipped.

foot-inch-fractions.zip (4.9 KB)

With a little bit of effort, you could convert the code to Ruby, I think. The logic would be the same or very similar, but the syntax and code would be different.

Or since SU Ruby understands SU lengths it might be very much simpler!

parseHtml

# encoding: UTF-8
#dezmo_test/main.rb
module DezmoTest
  extend self
  
  def to_locale_string( string )
    string.tr(',', '.').tr('.', Sketchup::RegionalSettings::decimal_separator)
  end
  
  def create_dialog
    options = {
        :dialog_title => "DezmoTest",
        :width => 400,
        :height => 200,
        :left => 100,
        :top => 100
      }
    dialog = UI::HtmlDialog.new(options)
    dialog.set_file(File.dirname( __FILE__ ) + "/test.html")
    dialog
  end

  def add_callbacks
    @dialog.add_action_callback('parseHtmlText') {|_context, data|
      text = to_locale_string( JSON.parse(data) )
      begin
        value = text.to_l
        raise ArgumentError if value == 0.to_l
      rescue ArgumentError
        UI.beep
        value  = "Please enter valid length > 0"
        result = Sketchup.set_status_text("Can not parse length")
      end
      puts "Parsed to: #{value}"
      js1 = %{
        document.getElementById('id1').value = #{value.to_json};
        processMore();
      }
      @dialog.execute_script(js1)
    }
  end
  
  def show_dialog
    @dialog = create_dialog()
    add_callbacks()
    @dialog.show
  end
end
DezmoTest.show_dialog

<!DOCTYPE html>
<!-- #dezmo_test/test.html -->
<html>
  <body>
    <div>Test: Please enter valid length > 0 .</div>
    <div id="example">
       Length: <input id='id1' type='text' value='10' onchange="myvalidator(this);"
    </div>
    <script>
      function myvalidator(e){
        sketchup.parseHtmlText(JSON.stringify(e.value));
      }
      function processMore(){
        console.log("Here you can do more... ");
      }
    </script>
  </body>
</html>

dezmo_test.zip (1.2 KB)

2 Likes

Thank you. That is very kind of you. I will look at the code with interest. I had tried some code I found here GitHub - dobriai/footinch: Length string parser and formatter - imperial and metric but it didn’t do everything necessary to cope with SketchUp length input. It converted feet and inch to decimal feet but treated 2 1/2 (without the ") as two and a half feet. I’d have had to do a lot of extra work on it to make it work for me.

That looks like what I need. Thank you for the example. That is very helpful and I’ll try that.

1 Like

You can register a callback and then execute a javascript from within that callback, similar to how all communication between Ruby and HTML is done.

dialog = UI::HtmlDialog.new
dialog.set_html("Test")

dialog.add_action_callback("text_to_float") do |_, text|
  float = text.to_l.to_f
  # Can replace alert by any method of your choice.
  dialog.execute_script("alert(#{float})")
rescue
  dialog.execute_script("alert('Invalid length')")
end

dialog.add_action_callback("float_to_length") do |_, float|
  string = float.to_l.to_s
  # Using to_json to add quotation marks around the string and 
  # escape any special characters.
  dialog.execute_script("alert(#{string.to_json})")
end

dialog.show

2022-05-09_11h15_04

Right click the dialog and open the developer tools. In the JavasScript console you can try running commands like sketchup.text_to_float("2m") or sketchup.float_to_string(100) to test the code.

Generally when making an extension you never need to re-invent length formatting, re-invent length parsing, swap out characters in the length strings, or check what units the SketchUp model uses. String#to_l and Length#to_s handles this. Trying to re-invent what SketchUp already offers is more work to implement, and makes the extension inconsistent to SketchUp.

This doesn’t just apply to lengths and SketchUp. When working with dates, you want to use existing date functions that handles leap years and other special cases. When you want to send data as a single String, you can use something like JSON to handle serialization and escaping special characters. Re-inventing things that already exists and are well tested creates additional work and risks adding more bugs.

I can only think of one example when you need to manually check what units the model uses, and that is to come up with a reasonable default value in the current unit system. In other cases I’ve seen, checking the model unit seems to be a detour to doing what String#to_l and Length#to_s already does for free.

def metric?
  unit_options = Sketchup.active_model.options["UnitsOptions"]
  return false unless unit_options["LengthFormat"] == Length::Decimal
  
  [Length::Millimeter, Length::Centimeter, Length::Meter].include?(unit_options["LengthUnit"])
end

default = metric? ? 100.mm : 4.inch
puts default

I am doing that so I will leave that in my code. The text size in SVG needs to be approximately corrected to the likely numbers used in the size of the object being drawn in SVG.

Thank you for the advice. Both you and Demzo have been very helpful.

I just hadn’t played around with call backs before and was unsure of my ground there. Your example and Demzo’s are very helpful.

Thank you again.

1 Like

I had a look at thomthom and Eneroth3’s UI::HtmlDialog Examples on github. They use the Vue framework in the examples. I rewrote Dezmo’s example using the Vue framework just to see how it works. My rewrite is also doing the length conversions. Here is what I’ve done for what it is worth.

<html>
<head>
<script src="../vendor/vue.js"></script>
</head>
  <body>
  <div id="app">
    <template>
      <h1>{{ name }}</h1>
				<div id="example">
					 Length: <input id="id1"  v-model="user_input" placeholder="type length" v-on:change="myvalidator(this.user_input);">
					<p id="id2">This will become your length in decimal</p>
				</div>
    </template>
  </div>
    <script>
			var app = new Vue({
				el: '#app',
				data: {
					name: "My Little Test...",
					user_input: '',
				}
				,
				methods: {
					myvalidator: function() {
						sketchup.parseHtmlText(this.user_input);
					},
				}
			});
			
			function convertUserInput2Decimal(float_number) {
				document.getElementById("id2").innerHTML = float_number;
			}
    </script>
  </body>
</html>
# encoding: UTF-8
module DezmoTest
  extend self
  
  def to_locale_string( string )
    string.tr(',', '.').tr('.', Sketchup::RegionalSettings::decimal_separator)
  end
  
  def create_dialog
    options = {
        :dialog_title => "DezmoTest",
        :width => 400,
        :height => 200,
        :left => 100,
        :top => 100
      }
    dialog = UI::HtmlDialog.new(options)
    dialog.set_file(File.dirname( __FILE__ ) + "/test_vue.html")
    dialog
  end

  def add_callbacks
    @dialog.add_action_callback('parseHtmlText') {|_context, data|
      text = to_locale_string( data.to_s )
      begin
        length_number = text.to_l
        raise ArgumentError if length_number == 0.to_l
				float_number = length_number.to_f
				js1 = float_number ? JSON.pretty_generate(float_number) : 'null'
				@dialog.execute_script("convertUserInput2Decimal(#{js1})") 
      rescue ArgumentError
        float_number  = "Please enter valid length > 0"
				js1 = JSON.pretty_generate(float_number)
				puts"there is an error and float_number is #{float_number}"
				puts"there is an error and js1 is #{js1}"
				@dialog.execute_script("convertUserInput2Decimal(#{js1})") 
      end
    }
  end
  def show_dialog
    @dialog = create_dialog()
    add_callbacks()
    @dialog.show
  end
end
DezmoTest.show_dialog

Some notes when using a framework like Vue; you then want to avoid modifying the HTML directly - instead rely on binding values via the framework.

Instead if convertUserInput2Decimal you bind it’s content to a value within the Vue app’s data values.

<html>
<head>
<script src="../vendor/vue.js"></script>
</head>
  <body>
  <div id="app">
    <template>
      <h1>{{ name }}</h1>
        <div id="example">
           Length: <input v-model="user_input" placeholder="type length" v-on:change="myvalidator(this.user_input);">
          <p>{{ formatted_user_input }}</p>
        </div>
    </template>
  </div>
    <script>
      var app = new Vue({
        el: '#app',
        data: {
          name: "My Little Test...",
          user_input: '',
          formatted_user_input: '',
        }
        ,
        methods: {
          myvalidator: function() {
            sketchup.parseHtmlText(this.user_input);
          },
        }
      });
      
      function convertUserInput2Decimal(float_number) {
        app.formatted_user_input = float_number;
      }
    </script>
  </body>
</html>

(Untested)

Note how this only binds HTML elements/attributes to Vue values and lets Vue take care of all HTML changes. From Ruby you make calls that changes Vue’s data - and Vue propagate the changes as appropriate.

That is really cool. That is exactly what I’ll do. I’m still learning about Vue so thank you for the tip. It certainly saves you writing long references to the document model.

I’ve just tried it and it works perfectly.

Thank you again.

1 Like

I have taken the Vue example one step further and used it to convert 3 different user entered lengths. It is a bit complicated but works. I’m sharing it because it is more realistic that you would want to convert multiple lengths to decimal for processing in javascript.

In this first version I use number variables in the logic in the js function convertUserInput2Decimal to pick the HTML elements/attributes bound to Vue values and in the second I use strings.

Using number variables

# encoding: UTF-8
module DezmoFMSTest
  extend self
  
  def to_locale_string( string )
    string.tr(',', '.').tr('.', Sketchup::RegionalSettings::decimal_separator)
  end
  
  def create_dialog
    options = {
        :dialog_title => "DezmoFMSTest",
        :width => 400,
        :height => 400,
        :left => 100,
        :top => 100
      }
    dialog = UI::HtmlDialog.new(options)
    dialog.set_file(File.dirname( __FILE__ ) + "/test_vue_number_logic.html")
    dialog
  end

  def add_callbacks
    @dialog.add_action_callback('parseHtmlText') {|_context, user_length, w_h_d_dim_str|
		puts"user_length is #{user_length}"
		puts"w_h_d_dim_str is #{w_h_d_dim_str}"
      text = to_locale_string( user_length.to_s )
      begin
        length_number = text.to_l
        raise ArgumentError if length_number == 0.to_l
				float_number = length_number.to_f
				js11 = float_number ? JSON.pretty_generate(float_number) : 'null'
				js12 = w_h_d_dim_str ? JSON.pretty_generate(w_h_d_dim_str) : 'null'
				@dialog.execute_script("convertUserInput2Decimal(#{js11}, #{js12})") 
      rescue ArgumentError
        float_number  = "Please enter valid length > 0"
				js11 = float_number ? JSON.pretty_generate(float_number) : 'null'
				js12 = JSON.pretty_generate(4)
				puts"there is an error and float_number is #{float_number}"
				puts"there is an error and js11 is #{js11}"
				puts"there is an error and js12 is #{js12}"
				@dialog.execute_script("convertUserInput2Decimal(#{js11}, #{js12})") 
      end
    }
  end
  def show_dialog
    @dialog = create_dialog()
    add_callbacks()
    @dialog.show
  end
end
DezmoFMSTest.show_dialog
<html>
<head>
<script src="../vendor/vue.js"></script>
</head>
  <body>
  <div id="app">
	<template>
      <h1>{{ name }}</h1>
				<div id="example">
					 Height: <input id="id1"  v-model="user_height" placeholder="type height" v-on:change="userHeightToDecimal();">
					<p>{{formatted_height}}</p>
					 Width: <input id="id1"  v-model="user_width" placeholder="type width" v-on:change="userWidthToDecimal();">
					<p>{{formatted_width}}</p>
					 Depth: <input id="id1"  v-model="user_depth" placeholder="type depth" v-on:change="userDepthToDecimal();">
					<p>{{formatted_depth}}</p>
				</div>
	</template>
  </div>
    <script>
			var app = new Vue({
				el: '#app',
				data: {
					name: "My Little Test...",
					user_height: '',
					user_width: '',
					user_depth: '',
					formatted_height: '',
					formatted_width: '',
					formatted_depth: '',
				},
				
				methods: {
					userHeightToDecimal: function() {
						sketchup.parseHtmlText(this.user_height, 1);
					},
					userWidthToDecimal: function() {
						sketchup.parseHtmlText(this.user_width, 2);
					},
					userDepthToDecimal: function() {
						sketchup.parseHtmlText(this.user_depth, 3);
					},
				}
			});
			
			function convertUserInput2Decimal(float_number, w_h_d_dim_str) {
			console.log("in convertUserInput2Decimal and w_h_d_dim_str is " + w_h_d_dim_str)
				switch (w_h_d_dim_str){
					case 1:
						app.formatted_height = float_number;
						break;
					case 2:
						app.formatted_width = float_number;
						break;
					case 3:
						app.formatted_depth = float_number;
						break;
					default:
						alert("Please enter a valid length > 0");
				}
			}
    </script>
  </body>
</html>

Strings version

# encoding: UTF-8
module DezmoFMSTest
  extend self
  
  def to_locale_string( string )
    string.tr(',', '.').tr('.', Sketchup::RegionalSettings::decimal_separator)
  end
  
  def create_dialog
    options = {
        :dialog_title => "DezmoFMSTest",
        :width => 400,
        :height => 400,
        :left => 100,
        :top => 100
      }
    dialog = UI::HtmlDialog.new(options)
    dialog.set_file(File.dirname( __FILE__ ) + "/test_vue_string_logic.html")
    dialog
  end

  def add_callbacks
    @dialog.add_action_callback('parseHtmlText') {|_context, user_length, w_h_d_dim_str|
		puts"user_length is #{user_length}"
		puts"w_h_d_dim_str is #{w_h_d_dim_str}"
      text = to_locale_string( user_length.to_s )
      begin
        length_number = text.to_l
        raise ArgumentError if length_number == 0.to_l
				float_number = length_number.to_f
				js11 = float_number ? JSON.pretty_generate(float_number) : 'null'
				js12 = w_h_d_dim_str ? JSON.pretty_generate(w_h_d_dim_str) : 'null'
				@dialog.execute_script("convertUserInput2Decimal(#{js11}, #{js12})") 
      rescue ArgumentError
        float_number  = "Please enter valid length > 0"
				js11 = float_number ? JSON.pretty_generate(float_number) : 'null'
				js12 = w_h_d_dim_str ? JSON.pretty_generate(w_h_d_dim_str) : 'null'
				puts"there is an error and float_number is #{float_number}"
				puts"there is an error and js1 is #{js1}"
				@dialog.execute_script("convertUserInput2Decimal(#{js11}, #{js12})") 
      end
    }
  end
  def show_dialog
    @dialog = create_dialog()
    add_callbacks()
    @dialog.show
  end
end
DezmoFMSTest.show_dialog
<html>
<head>
<script src="../vendor/vue.js"></script>
</head>
  <body>
  <div id="app">
	<template>
      <h1>{{ name }}</h1>
				<div id="example">
					 Height: <input id="id1"  v-model="user_height" placeholder="type height" v-on:change="userHeightToDecimal();">
					<p>{{formatted_height}}</p>
					 Width: <input id="id1"  v-model="user_width" placeholder="type width" v-on:change="userWidthToDecimal();">
					<p>{{formatted_width}}</p>
					 Depth: <input id="id1"  v-model="user_depth" placeholder="type depth" v-on:change="userDepthToDecimal();">
					<p>{{formatted_depth}}</p>
				</div>
	</template>
  </div>
    <script>
			var app = new Vue({
				el: '#app',
				data: {
					name: "My Little Test...",
					user_height: '',
					user_width: '',
					user_depth: '',
					formatted_height: '',
					formatted_width: '',
					formatted_depth: '',
				},
				
				methods: {
					userHeightToDecimal: function() {
						sketchup.parseHtmlText(this.user_height, 'height');
					},
					userWidthToDecimal: function() {
						sketchup.parseHtmlText(this.user_width, 'width');
					},
					userDepthToDecimal: function() {
						sketchup.parseHtmlText(this.user_depth, 'depth');
					},
				}
			});
			
			function convertUserInput2Decimal(float_number, dim_str) {
			console.log("in convertUserInput2Decimal and dim_str is " + dim_str)
				switch (dim_str){
					case 'height':
						app.formatted_height = float_number;
						break;
					case 'width':
						app.formatted_width = float_number;
						break;
					case 'depth':
						app.formatted_depth = float_number;
						break;
					default:
						alert("Please enter a valid length > 0");
				}
			}
    </script>
  </body>
</html>

With apologies, this is my 3rd edit of this post.