I’ve been saving profiles by writing them to JSON. You can keep values that way. I’m guessing you’ll have to save between sessions.
The Ruby part of my extension already stores them as a json between sessions and loads them at start up.
Current dialog code:
def go
@dialog = UI::HtmlDialog.new(
{
:dialog_title => 'DC Tools - Attribute Handler',
:scrollable => true,
:resizable => true,
:width => 500,
:height => 300,
:left => 200,
:top => 200,
:min_width => 235,
:min_height => 125,
:max_width =>600,
:style => UI::HtmlDialog::STYLE_DIALOG
})
att_hash = {
att_1: 'dc att_1 value',
att_2: 'dc att_2 value',
att_3: 'dc att_3 value'
}
@dialog.add_action_callback('ready') do |context, params|
puts "Dialog callback \"ready\" (dialog static content was loaded)"
pass_atts_in = att_hash.to_json
@dialog.execute_script( %[var get_atts = JSON.parse('#{pass_atts_in}');] )
@dialog.execute_script( %[populate_list();] )
end
html = %q[
<!DOCTYPE html>
<html>
<head>
<title>Dynamic Attributes Handler</title>
<style>
body {
width: 100%;
height: 100%;
}
.buttonred {
border: none;
border-radius: 4px;
font-weight: bold;
color: white;
background-color: rgb(238, 102, 102);
padding: 3px 9px;
display: inline-block;
font-size: 14px;
margin: 2px 4px;
cursor: pointer;
}
.buttongreen {
border: none;
border-radius: 4px;
font-weight: bold;
color: white;
background-color: rgb(152, 200, 146);
padding: 3px 8px;
display: inline-block;
font-size: 14px;
margin: 2px 0px;
cursor: pointer;
}
.container {
width: 100%;
}
input[type=text] {
width: calc(100% - 77px);
}
</style>
<script>
document.addEventListener("readystatechange", function (e) {
if( document.readyState == 'complete' ) {
sketchup.ready();
}
});
</script>
</head>
<body>
<h3 style='text-align:left'>DC Attributes List <button class='buttongreen' onclick='new_att_line(null, start_point)'>+</button></h3>
<div class='container'>
<div id='dc_att_list'>
<!-- create 'div0' so new lines are placed in dc_att_list -->
<div id='start_point'></div>
</div>
</div>
<script>
var parent_element = document.getElementById('start_point')
// Starts as div id='start_point' to place first element inside the dc_att_list div
function add_listener(my_button) {
my_button.addEventListener('click', function handleClick(event) {
new_att_line(null, this.parentElement);
});
}
function populate_list() {
// Run once at start. Iterate the get_atts Object imported from Ruby hash:
Object.keys(get_atts).forEach( function(key) {
new_att_line(key, parent_element);
})
}
var num = 0;
function new_att_line(key, parent_element) {
// Create a new div element to hold line inputs.
num +=1;
var div_nam = 'div' + num;
var new_line = document.createElement('div');
//new_line.setAttribute('id',div_nam);
if (key) {
//add the saved dc attribute feild
var load_dc_att = document.createElement('input');
load_dc_att.setAttribute('type','text');
load_dc_att.setAttribute('name','att_text');
load_dc_att.setAttribute('id',key);
load_dc_att.setAttribute('class','text');
load_dc_att.setAttribute('value',get_atts[key]);
new_line.appendChild(load_dc_att);
} else {
//add an empty dc attribute field
var new_dc_att = document.createElement('input');
new_dc_att.setAttribute('type','text');
new_dc_att.setAttribute('name','att_text');
new_dc_att.setAttribute('id','att_');
new_dc_att.setAttribute('class','text');
new_dc_att.setAttribute('placeholder','enter_attribute');
new_line.appendChild(new_dc_att);
}
//ceate and add the remove_line button
var rmv_button = document.createElement('input');
rmv_button.setAttribute('name','remove_line');
rmv_button.setAttribute('type','button');
rmv_button.setAttribute('class','buttonred');
rmv_button.setAttribute('onclick','return this.parentElement.remove();');
rmv_button.setAttribute('value','-');
new_line.appendChild(rmv_button);
//ceate and add the add_line button
var add_button = document.createElement('input');
add_button.setAttribute('name','add_line');
add_button.setAttribute('type','button');
add_button.setAttribute('class','buttongreen');
add_button.setAttribute('value','+');
add_listener(add_button);
new_line.appendChild(add_button);
// Add a break to group the elements together.
var br = document.createElement('br');
new_line.appendChild(br);
// add the newly created dc attribute line into the DOM
// after the clicked btton line.
parent_element.insertAdjacentElement('afterend', new_line);
}
// Now to send it all back out:
function make_export_hash () {
var input_tags = dc_att_list.getElementsByTagName('input');
}
</script>
</body>
</html>
] # end of HTML
@dialog.set_html(html)
@dialog.show
@dialog.add_action_callback('sendDataToRuby') { |action_context, my_hash|
att_hash = my_hash
puts my_hash
}
end # go()
go
I don’t know. Test, test, … more test.
Upon extensive testing it appears that the get_atts list will not adjust easily to the dynamicness of the dialog. Could use a remove attribute function to call when a line is removed. However any new entries are entered last in the hash, so a line inserted near the top of the list still gets recorded as the last entry. On reload the list will be scrambled, and I think maintaining it in the moment might be a lot of code. So tomorrow back to exploring building a hash from the inputs to export…
EDIT: Investigating inserting a key value pair in the middle of a hash, with the button click. Still thinking it’s simpler to just build a new hash from the list with new att_#s each time. This would also avoid duplicate keys at load. Is there a way to leverage name and submit?
def go
@dialog = UI::HtmlDialog.new(
{
:dialog_title => 'DC Tools - Attribute Handler',
:scrollable => true,
:resizable => true,
:width => 500,
:height => 300,
:left => 200,
:top => 200,
:min_width => 249,
:min_height => 125,
:max_width =>600,
:style => UI::HtmlDialog::STYLE_DIALOG
})
att_hash = {
att_1: 'dc att_1 value',
att_2: 'dc att_2 value',
att_3: 'dc att_3 value'
}
@dialog.add_action_callback('ready') do |context, params|
puts "Dialog callback \"ready\" (dialog static content was loaded)"
pass_atts_in = att_hash.to_json
@dialog.execute_script( %[var get_atts = JSON.parse('#{pass_atts_in}');] )
@dialog.execute_script( %[populate_list();] )
end
html = %q[
<!DOCTYPE html>
<html>
<head>
<title>Dynamic Attributes Handler</title>
<style>
body {
width: 100%;
height: 100%;
}
.buttonred {
border: none;
border-radius: 3px;
font-weight: bold;
color: white;
background-color: rgb(238, 102, 102);
padding: 3px 9px;
display: inline-block;
font-size: 14px;
margin: 2px 2px;
cursor: pointer;
}
.buttongreen {
border: none;
border-radius: 3px;
font-weight: bold;
color: white;
background-color: rgb(167, 208, 162);
padding: 3px 8px;
display: inline-block;
font-size: 14px;
margin: 2px 2px;
cursor: pointer;
}
button span{
border: 1px black;
}
.container {
width: 100%;
}
input[type=text] {
width: calc(100% - 77px);
margin: 2px 2px;
}
</style>
<script>
document.addEventListener("readystatechange", function (e) {
if( document.readyState == 'complete' ) {
sketchup.ready();
}
});
</script>
</head>
<body>
<div class='container'>
<h3 style='text-align:left'><b>DC Attributes List</b> <button class='buttongreen' onclick='make_export_hash()'><span>Save</span></button><button class='buttongreen' onclick='new_att_line(null, start_point)'>+</button></h3>
<div id='dc_att_list'>
<!-- create 'div0' so new lines are placed in dc_att_list -->
<div id='start_point'></div>
</div>
</div>
<script>
var num = 0;
var parent_element = document.getElementById('start_point');
// Starts as div id='start_point' to place first element inside the dc_att_list div
// Add button oncick to insert new next line.
function add_listener(my_button) {
my_button.addEventListener('click', function handleClick(event) {
new_att_line(null, this.parentElement);
});
}
function text_listener(text_field) {
var att_txt = ('att_' + num);
text_field.addEventListener('input', (event) => {
var att = event.target;
var key = att.id;
var val = att.value;
get_atts[key] = val;
console.log(key + ': ' + val)
console.log(get_atts)
});
}
function populate_list() {
// Run once at start. Iterate the get_atts Object imported from Ruby hash:
Object.keys(get_atts).forEach( function(key) {
new_att_line(key, parent_element);
})
}
function new_att_line(key, parent_element) {
num +=1;
// Create a new div element to hold line inputs.
var new_line = document.createElement('div');
//add att key and empty val here
//var div_nam = 'div' + num;
//new_line.setAttribute('id',div_nam);
if (key) {
//add the saved dc attribute feild
var load_dc_att = document.createElement('input');
load_dc_att.setAttribute('type','text');
load_dc_att.setAttribute('name','att_text');
load_dc_att.setAttribute('id',key);
load_dc_att.setAttribute('class','text');
load_dc_att.setAttribute('value',get_atts[key]);
text_listener(load_dc_att);
new_line.appendChild(load_dc_att);
} else {
//add an empty dc attribute field
var new_dc_att = document.createElement('input');
new_dc_att.setAttribute('type','text');
new_dc_att.setAttribute('name','att_text');
new_dc_att.setAttribute('id','att_' + num);
new_dc_att.setAttribute('class','text');
new_dc_att.setAttribute('placeholder','enter_attribute');
text_listener(new_dc_att);
new_line.appendChild(new_dc_att);
}
//ceate and add the remove_line button
var rmv_button = document.createElement('input');
rmv_button.setAttribute('name','remove_line');
rmv_button.setAttribute('type','button');
rmv_button.setAttribute('class','buttonred');
rmv_button.setAttribute('onclick','return this.parentElement.remove();');
rmv_button.setAttribute('value','-');
new_line.appendChild(rmv_button);
//ceate and add the add_line button
var add_button = document.createElement('input');
add_button.setAttribute('name','add_line');
add_button.setAttribute('type','button');
add_button.setAttribute('class','buttongreen');
add_button.setAttribute('value','+');
add_listener(add_button);
new_line.appendChild(add_button);
// Add a break to group the elements together.
var br = document.createElement('br');
new_line.appendChild(br);
// add the newly created dc attribute line into the DOM
// after the clicked btton line.
parent_element.insertAdjacentElement('afterend', new_line);
}
// Now to send it all back out:
function make_export_hash() {
}
</script>
</body>
</html>
] # end of HTML
@dialog.set_html(html)
@dialog.show
#@dialog.add_action_callback('sendDataToRuby') { |action_context, my_hash|
# att_hash = my_hash
# puts my_hash
#}
end # go()
go
Hey . Ok so I am trying to grab all the ‘text’ input elements, and then their ‘value’. I am getting an undefined error: Cannot read property ‘getAttribute’ of undefined
Why? The console lists dc_atts_inputs as the input fields, what am I missing?
Thanks for any help, off to see Grandma back later on.
function export_atts() {
var dc_atts_div = document.getElementById('dc_att_list');
var dc_atts_inputs = dc_atts_div.getElementsByTagName('input')
console.log(dc_atts_inputs)
dc_atts_inputs.forEach(get_text_input_data())
}
function get_text_input_data(my_input) {
if (my_input.getAttribute('type') == 'text') {
console.log(my_input + 'text field')
}
}
You are not passing each input reference to the get_text_input_data()
function, as far as I can see.
(a) Using query selectors would be more efficient here:
var dc_atts_inputs = dc_atts_div.querySelectorAll("div input[type='text']");
REF: Element: querySelectorAll() method - Web APIs | MDN (mozilla.org)
- Learning about selectors (think “filters”) will give you a glimpse into JQuery library use.
REF: CSS selectors - Learn web development | MDN (mozilla.org)
(b) Then to iterate the array, using the for
… of
statement would be better to understand the code.
for (const my_input of dc_atts_inputs) {
get_text_input_data(my_input);
}
REF: for…of - JavaScript | MDN (mozilla.org)
- It is weird in JS:
for
…in
is for objects andfor
…of
is for arrays.
Then in the console.log
call I would think you need to use my_input.value
Hey Dan thanks! Happy Friday night. I am flipping back and forth between these two approaches and have ended up pursuing both. Sooo I have another question on that front. Commented in at the bottom of the code block.
function text_listener(text_field) {
var att_txt = ('att_' + num);
text_field.addEventListener('input', (event) => {
var att = event.target;
var key = att.id;
var val = att.value;
get_atts[key] = val;
console.log(key + ': ' + val)
console.log(get_atts)
});
}
function del_att(line_to_del) {
// line_to_del is the containing div with the id set
// to 'att_x' the get_atts key value
var att_id = line_to_del.id;
// all the correct info appears in console here
console.log(att_id + ' winner!' + get_atts[att_id]);
delete get_atts.att_id; // nothing gets deleted from get_atts
// if I replace the att_id with att_2 it deletes the correct attribute
// from get_atts. Why is my variable att_id not working here?
}
I think because att_id
is a local variable name within your del_att()
function, and not an actual property of the get_atts
object.
I would say first try using
delete get_atts[att_id];
ding! ding! ding! It’s not what you say, it’s how you say it. It’s almost like you have years of experience doing this.
I am now looking at how to insert the new attributes in the correct position. I can’t find an approach or method to insert a new key value pair at a specific point in an object, like after key value pair att_x insert new key value pair. Is this possible with an object? From what I can see so far, maps and objects do not allow specific insertion, but array does. Do I want to convert to array for this? That would kind of defeat the purpose of modifying the original Object tho… seems like the last hurdle to just sending the get_atts object back out as you suggested.
Back to plan B…
Yes I find for …of is much simpler than .forEach, side by side for anyone following along:
// same result from both
dc_atts_inputs.forEach(function (currentValue, currentIndex, listObj) {
console.log(currentValue.value);
}, "myThisArgString");
for (const my_input of dc_atts_inputs) {
console.log(my_input.value);
}
Thanks Dan, this gets my ordered list of attributes. Next step is to hash them into an object (not a js hash I think) to pass back to Ruby… a clearish path forward.
EDIT: Not a new object just redefine the existing get_atts object, and add the edited list as new key value pairs.
Mozilla developer docs are really helpful too, thanks!
Re: experience ...
Well, I started with SketchUp circa 2008 (in the v7 days.) And some few years before that I had taken & received certificates for courses in HTML, JavaScript and CSS from an online college that my sister worked for. (It was free so I couldn’t complain.) So, I’d say 18 years at least. I had dabbled even before that using MS FrontPage probably ~ 1993, so that’s back 30 years.
It also helps when you get to know your way around the Mozilla documentation website. The more familiar you get the quicker you find answers.
Well, normally a Ruby Hash and JS Object are unordered data collections accessed via keys. Ruby allows hashes to be rehashed (ie reordered,) but this is really meant to make it internally faster accessing the keys. I am not sure if this actually reorders the items in alphanumeric key order. I think that I had read that the order of members of a Ruby hash could not be guaranteed after something has been added.
Anyway, back to JavaScript. You may want to switch to a JS Map, see: Objects vs. Maps
So, if order is necessary. Each time you insert an attribute line, you can rebuild the Object (or Map.) IE … create a new empty object (or map,) copy all the key/values before the insertion, insert the new key/value, copy the rest of the key/values after the inserted member. Lastly, reassign the global variable referencing the data object (or map) to point to the newly ordered object (or map.)
Or … you can go back to the building of the output object from the attribute list after the Apply (or Save) button is clicked; and forego the “input” listeners.
I don’t know if the add_action_callback
will convert JS Maps to Ruby Hash
, you’ll have to test. (The documentation does not say specifically.)
But, if you think that it will make it easier for you to understand and maintain the JS code, yes you can just keep an ordered Array of [ key, value ]
subarrays instead of an object. These are also easily saved as JSON and easily converted to and from Ruby hashes (if this even is needed. It may not be, depending upon what you need on the Ruby-side.)
For example, in Ruby, Hash#to_a
returns an Array
of [ key, value ]
subarrays. And conversely, Array#to_h
converts an Array
of [ key, value ]
subarrays into a Hash
.
Your other option could be a JS Array of Objects …
var inventory = [
{ name: 'asparagus', type: 'vegetables', quantity: 9 },
{ name: 'bananas', type: 'fruit', quantity: 5 },
{ name: 'goat', type: 'meat', quantity: 23 },
{ name: 'cherries', type: 'fruit', quantity: 12 },
{ name: 'fish', type: 'meat', quantity: 22 },
];
When sent back to Ruby, it becomes a Ruby Array
of Hash
, where each attribute is it’s own hash of properties.
This is super interesting, was thinking about saved pre-sets to pick from. This could maybe be a way to store and retrieve those lists… but that’s for later.
I went with clearing and repopulating the existing object, I have my new, properly ordered list on ‘Save’ button click. Which is the same initial object imported from ruby.
// get_atts is now dc_attributes, seems more appropriate since it
// is both in and out now.
// Redefine dc_attributes, then send it all back out:
function export_atts() {
var dc_atts_div = document.getElementById('dc_att_list');
var dc_atts_inputs = dc_atts_div.querySelectorAll("div input[type='text']");
var att_num = 1
// Clear object attributes from dc_attributes.
for (var key in dc_attributes) delete dc_attributes[key];
// Rebuild dc_attributes with new key value pairs.
for (const my_input of dc_atts_inputs) {
var att_id = ('att_' + att_num);
att_num +=1;
var key = att_id;
var att = my_input.value;
dc_attributes[key] = att;
}
console.log(dc_attributes);
};
Now just to send it back to ruby. But before that I am reading about this and target, Ruby self vs JS this discussed here.
re:Experience
I got interested in spread sheets when I was a cook at 17, as a supervisor I was exposed to costing spreads in Lotus123. Found it fun to develop a statistics based predictive spread sheet to guess business volumes for the next week, automated super un-fun invoice coding, recipe costing etc… Which inevitably led to some macro writing. Then Lotus died and I had to learn Excel, and I started building complete comprehensive P&L generating inventory/labor spreads off and on for 20 years. Switched careers at 40 and went to school for Architectural Technology and met CAD, loved SketchUp and hated Revit. AutoCAD is still what I use for 2D. Automating my design process with DCs was obvious with the excel background, and were a gateway to the Ruby, and now here we are…
Also after the last 2 weeks, I get this now:
Wow that was super simple, dc_attributes back out and saved. Interestingly multiple open dialogs allow for switching between different lists. When a dialogs save button is clicked that attributes list becomes the list currently used by the extension. Now for those saved pre-sets…
ruby:
@dialog.add_action_callback('SendAtts') { |action_context, dc_attributes|
@ruby_att_hash = dc_attributes
}
JS:
// Redefine dc_attributes, then send it all back out:
function export_atts() {
var dc_atts_div = document.getElementById('dc_att_list');
var dc_atts_inputs = dc_atts_div.querySelectorAll("div input[type='text']");
var att_num = 1
// Clear object attributes from dc_attributes.
for (var key in dc_attributes) delete dc_attributes[key];
// Rebuild dc_attributes with new key value pairs.
for (const my_input of dc_atts_inputs) {
var att_id = ('att_' + att_num);
att_num +=1;
var key = att_id;
var att = my_input.value;
dc_attributes[key] = att;
}
sketchup.SendAtts(dc_attributes);
console.log(dc_attributes);
};