DOM Manipulation with jQuery

With so many frameworks and libraries coming out these days, jQuery is looking like that old man sitting on his porch yelling at the kids to keep off his grass.

When it came out, it was a Godsend. Being one of the first major JavaScript libraries, it took code that typically looked likedocument.getElementById and gave us shorthand such as $(“#dom-element-id”).val(). Now, if you’ve ever created a dynamic page that had heavy utilization of JavaScript, you can sympathize with me when I say, there are few positions in life closer to Hell than that. Vanilla JavaScript reads ugly, it gets unruly at length, and frankly, it downright sucks. So yeah, when jQuery came out it was AWESOME. There’s reason that after 11 years, it’s still one of the predominant JavaScript libraries used on the web today.

When John Resig released jQuery at BarCamp NYC in 2006, it hit the web by storm. Influenced by cssQuery, it provides a means of traversing and manipulating the DOM that is much more clumsy in vanilla js. By providing commonly desired functions as part of the library, we are able to do fade-ins, slides, trigger events, and even take advantage of AJAX all with a handful of easy to remember function calls. What’s even better is most of the work in jQuery can be done one of two ways: either by creating a JavaScript object (essentially a hashed array), or calling parent functions in a series. This makes for elegant code that, those of us who remember the early 2000’s JavaScript, dreamed of for years. jQuery is now being maintained by Timmy Willison and is licensed under the MIT License. Now that we’ve knocked out a little bit of history on this, let’s get into it.

What does jQuery make available to the developer?

There’s a series of fundamental components jQuery provides that we need to understand and be aware of to take advantage of if we intend on being proficient and highly effective with jQuery. These are:

  • DOM element selection based on CSS selectors
  • Events
  • Effects and animations
  • AJAX
  • JSON parsing
  • Extensibility through plug-ins
  • Utilities - i.e. feature detection
  • Compatibility methods, which are inherent in modern browsers, but fall short in older browser (cough*IE9 and older*cough)
  • Multi-browser support

Alright, that’s quite a series of components that make jQuery something that CoffeeScript was able to abstract on (quite elegantly I might add), and other frameworks have strived to replicate. Let’s iterate through that list to further understand exactly what each function is about.

DOM element selection based on CSS selectors

If you’re reading this, I’m assuming you know what a CSS selector is. This can be an id, a class, or an attribute. Say you want to show a BootStrap alert when a button gets clicked. Your HTML looks something like:

<div id=“msg-id” class=“alert alert-success”>Hey, there! I’m a Bootstrap Alert</div>

<button id=“show-msg-id” class=“btn btn-primary”>Show Message</button>

Easy enough, right? We have a simple div with the id of msg-id, and we have a button with the id of show-msg-id. For the sake of brevity, assume that our CSS file has a line like #msg-id { display: none; }. This ensures that our message isn’t just hanging out and being visible when we don’t want it to be. Now, to make this work how we want it to, let’s write up some jQuery.

$(“#show-msg-id”).bind(‘click’, function(e) {
    setTimeout(function(){
        $(“#msg-id”).fadeIn(600, function(){ 
            setTimeout(function(){ 
                $("#msg-id").fadeOut(600); 
            }, 3500); 
        });
    },500);

    e.preventDefault();
}

I know this looks like a lot of code, and it is, for what it does. Let’s go line by line. setTimeout() is a function which does exactly what its name insinuates — it takes two parameters, a function call and a duration in milliseconds. The function we’re calling doesn’t have a name, this is an abstract function which will run the code within the curly braces. Next, we call $(“#msg-id”).fadeIn(…). fadeIn() is another jQuery function which does as it says also (pretty nice, having these types of inherent functions, no?). It doesn’t require any arguments and default to 400 milliseconds for the duration when no arguments are passed to it. Here, we’re passing in 600 milliseconds, and another anonymous function. Inside this anonymous function, we’re calling setTimeout() again, and this time, we’re calling fadeOut(). This does the opposite of fadeIn() and we also set it to 600 milliseconds.

The reason we call setTimeout() within fadeIn() is because if we did not, and simply called fadeOut(), we wouldn’t be able to see the alert. We want this alert to be visible for a moment, think like the growl notifications on your Mac or the toast notifications on your Android. These have a series of pauses the developer defined (in the case of Android, these times are constants, but that’s a whole other article). We let the alert stay visible for three and a half seconds before the anonymous function is fired which then fades out the alert. Coming down to the next line, we see 500, and that’s our timeout for how long we want to want after the button is clicked to begin the whole chain of events.

Another method of using CSS selectors is to grab a series of elements based off a CSS class. Say you have a series of inputs:

<form id=“form-id”>
    <input type="text" id=“input_one-id” class="form-control required">
    <input type="text" id="input_two-id" class="form-control required">
    <input type="text" id="input_three-id" class="form-control required">
    <input type="text" id="input_four-id" class="form-control required">
</form>

Obviously, each has a different ID since HTML elements can only have unique IDs. This is even more important when manipulating the DOM since you’ll get unexpected results if you have multiple elements with the same id on the same page. To iterate over these with jQuery we can do something like:

$(“#form-id”).find(‘.required’).each(function() {
    if($(this).val())
        console.log($(this).val());
    else
        console.log(“#”+$(this).attr(‘id’)+” has no value”);
}

What we’re doing here is referencing the DOM element with the id form-id, and within that element, we are searching for all the elements with the class of required. Then, since the DOM has an object with each of the referenced we can iterate over each element. For this example, we’re simply logging the value of each element to the console, and if there’s no value in that input we log that fact as well with the id so we can investigate further. This is a fundamental method of input validation, so it’s really important to keep this kind of function in mind.

This last bit of code showed another method of accessing DOM elements with CSS selectors as well. the call $(this).attr(‘id’) is using a jQuery function called attr(); short for attribute. This works for any common HTML attribute. If you have data- attributes, you can use.data(‘custom-attribute-name’) to retrieve its value. In addition, you can reference element based on their type, or search for attribute values with prefixes or suffixes. The possibilities are endless. Here’s some examples:

$(‘input[type=“text”]’).val();
$(‘a[href*=“-suffix”]).val();
$(‘a[href^=“prefix-“]).val();
$(‘div.class’).data(‘custom-attribute-name’);

Events - clicks, focuses, keyups, and more

Our last line, and probably the most used line you will see in jQuery, is e.preventDefault(). You can see in the first line of our script our anonymous function takes an argument of e. This isn’t special, and in fact is short for event. bind() is an event handler, and the first argument it takes is what event do we want to bind to. Since we’re working with a button here, we want to bind to click. The problem is, browsers all have default functions they anticipate DOM elements to perform. Buttons trigger responses, links go places, and inputs receive input. By passing the event into our anonymous function, we can dictate exactly what we want to happen. In this case, we don’t want the button to perform its regular behavior (would reload the page or scroll to the top, and that’s not the UX we’re after.) Instead, we want it to do what we told it to do: fade in an alert and fade out the alert after set periods of time. e.preventDefault() ensures that’s all the button does — forget it, browser, we’re in control here.

Another beautiful thing about events is we can trigger auto-completes, date pickers, calendar date selection windows, modal events, and more. For example, let’s take a look at jQuery’s datepicker plug-in. This allows for a user to click on an input box, and select a date from a small window that pops up. The date can be formatted in any way you want and makes for a pleasant UX (though there are much better options for this, jQuery’s datepicker suffices for 90% of use cases).

Our HTML looks something like:

<label class=“control-label” for="date">Date</label>
<input type="text" id=“dateDisp" class="form-control">
<input type="hidden" name="date" id="date-id">

Simple enough. Never mind the classes if you aren’t familiar with Bootstrap, they’re classes that provide rapid ways of developing user interfaces. I highly recommend learning Bootstrap if you’re unfamiliar. Anyways, here’s the jQuery that makes the awesomeness happen:

$('#dateDisp').datepicker({
    showOn: 'focus',
    altField: ‘’#date-id',
    altFormat: 'yy-mm-dd',
    dateFormat: 'mm/dd/yy',
    maxDate: 0
});

And that’s it. jQuery takes care of the rest. You end up with a beautiful box as seen below, and your user’s experience will be all the better for it. Read through the jQuery documentation for all the events you can respond to, as there are thousands of ways to interact with the DOM and create a unique experience for your users, makes your life easier for managing data flow, and generally improve the performance of any dynamic page.

Effects and animations - where’s the eye candy?!

Now before you get too excited, let me remind you this isn’t 1999. No one wants to go to a site with glittering text, and shit flying all over the place. The era of flash animations and GIFs galore is over (ok, maybe GIFs are seeing a resurgence). Regardless of that last tidbit, animations put the dynamic in a dynamic website. While there are thousands of methods of accomplishing many of the effects jQuery offers by using CSS3 transforms, transitions and keyframes, jQuery enables you to achieve much of the same with sometimes fewer lines of code. We’ve touched on two of the built in animation functions already in the first section of this article, and there are plenty more for you to take advantage of. I’m not going to go into too much depth on this portion as I don’t need you junior developers getting crazy, and pissing off your senior developers to the point they email me threats for showing you all the “totally rad animations” you decided to implement on your latest project. Find your way to the jQuery documentation and discover for yourself all the sweet shorthand this library offers.

AJAX - cooking with gas in jQuery

I was on Quora the other day and came across a question from someone wanting to make their blog more dynamic. They wanted to the article links to change the blog content without refreshing the page. While they were insistent on not using any kind of server side language or database to store their data, my first point of response was educating them on AJAX. What AJAX does for us is provide an efficient, asynchronous means of taking our client-side events and interfacing them to a back-end processor to do whatever we need it to. For sake of brevity, I’m going to show you a simple contact form.

You have your form built like so:

<form id="form-id” class="form">
    <div class="row">
        <div class="col-sm-12">
            <input type="text" id="fname-id" name="fname" class="form-control" size="33" value="" placeholder="FIRST NAME" maxlength="25" />
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12">
            <input type="text" id="lname-id" name="lname" class="form-control" size="33" value="" placeholder="LAST NAME" maxlength="25" />
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12">
            <input type="text" id="email-id" name="email" class="form-control" size="33" value="" placeholder="EMAIL" maxlength="25" />
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12">
            <input type="text" id="phone-id" name="phone" class="form-control" size="33" value="" placeholder="PHONE" maxlength="12" />
        </div>
    </div>
    <div class="row">
        <div class="row text-center btn-row">
            <h2><a id="submit" class="btn-orange btn-xl">SUBMIT</a></h2>
        </div>
    </div>
</form>

Now, let’s collect these form values and send them off to some script so that we can build a contact list. Think of this as the foundation for a very minimal CRM.

$(document).ready(function() {
    $(“#submit”).bind(‘click’, function(e) {
        submit();
    });
});

function submit(){
    // gather all the valid info and store it for POST to PHP script
    var formData =
    {
        'firstName' : $(“#fname-id").val(),
        'lastName' : $(“#lname-id").val(),
        'email'      : $("#email-id").val(),
        'phone'      : $("#phone-id").val()
    };
    // AJAX POST function
    $.ajax({
        type        : 'POST', 
        url     : 'scripts/script.php',
        data        : formData,
        success : function(result){
                        // log data to the console so we can see
                            // console.log("Result From Server:\n\n" + result);
                            result = parseInt(result);

                switch(result){
                    case 0:
                        $(‘#form-id’).empty().html("<p class='alert alert-danger center'>We were unable to process your request at this time. We apologize for the inconvenience. Please try again later.</p>");
                        break;
                    case 1:
                        $(‘#form-id’).empty().html("<p class='alert alert-success center'>Thank you for your suggestions! We may be in touch shortly.</p>");
                        break;
                    case 2:
                        $("#form-msg").fadeIn(300, function(){ setTimeout(function(){ $("#form-msg").fadeOut(300); }, 4000); });
                        break;
                }
                },
        error       : function(xhr, type, exception){
                    // console.log("ajax error response type: ", type);
                    $('#form-id').empty().html("<p class='alert alert-danger'>We were unable to process your request at this time. We apologize for the inconvenience. Please try again later.</p>");
                }
    });
}

There’s a lot that goes into AJAX, so I’m going to just give a very high-level review of what this script does. Something i haven’t touched on yet is $(document).ready(). This is another event we can act on and use. It fires whatever code is within its anonymous function as soon as the DOM is ready, or basically done loading all the resources. This means any bindings you need to do, or timers that need to start running, or whatever, need to be put in here. Anyways, we bind to the button at this point. Inside the button’s function, we call submit().

submit() starts by constructing a JavaScript object of the form values. Why do we do this, you ask? Because it’s easier to manipulate an object than pass in individual values when working with datasets. If you’re unfamiliar with arrays, I’ll write about them at a later date. Just know if you’re new to development, you’ll use arrays a lot so you better get comfortable with them. Moving forward you see this funny looking call: $.ajax . No, this isn’t a made up method, this is the beauty of jQuery at its very essence. What the $. does is create a utility function:a function that does not act upon a jQuery object directly.

To construct an AJAX request of this nature, we need to specify a type. In this case, we specify it as POST as we’re sending data to the server. url specifies what script to call, data passes our form values object in, and success is where we are able to start doing some interesting things.

We pass in a function with the argument result. result is the response coming from our script on the backend (I hope you’re returning messages from your backend scripts). In this case, my PHP script returns some error codes, so we call parseInt() on the value contained in the result. Then we run the result through a switch statement and notify the user accordingly. At this point, you’ve seen most of these functions. empty() empties all the HTML from a DOM element, and html() insert HTML into a DOM element. Mind you, success does not necessarily mean everything was successful on the server, it simply means your request returned a 200 response. That’s why you need to do further validation on the backend of the input you’re sending (NEVER trust user input).

The final piece of our AJAX call is error. This is fired when our request doesn’t go through, or in other words, the client receives a response other than 200 from the server. We handle this accordingly, and you can see I have commented out a console.log() used during debugging to determine what the specific failure was. Do study AJAX documentation thoroughly; it’s an indispensable function of the jQuery library and absolutely vital for dynamic websites. It also goes beyond what I’ve shown here and is capable of so much more.

JSON parsing - make data management easy

JSON is JavaScript Object Notation. In a nutshell, JSON is a hashed array of key, value pairs. Typically you’ll see something like this as a JSON response from a backend script:

{ id: 3, name: “Jane Smith”, age: “25”, city: “Portland”, state: “OR” }

When it comes to parsing JSON, we used to have to do this manually. Now we can call a fancy little method named parseJSON(). This breaks the object down into individual components which you then can reference from within your object. So, for instance, so we have a callback function that receives a JSON response. We need to populate a formerly populated form with this data. We could do something like:

function load(response) {
    var r = $.parseJSON(response);

    $(“#name-id”).val(r.name);
    $(“#age-id”).val(r.age);
    $(“#city-id”).val(r.city);
    $(“#state-id”).val(r.state);
    $(“#object-id”).val(r.id);
}

It’s a simple and elegant solution to a problem that has plagued web developers for years and used to be much more clumsy with alternative formats (XML, CSV, TSV, etc.). Flex JSON whenever you can as you’ll be hard pressed to find a more efficient means of handling large datasets.

Extensibility - Through plug-ins, anything is possible

Long ago, when it came to using plugins, you had two options. Either you used libraries which were system level and part of the OS your program would function on, or you built modular code that could be repurposed across many projects. While you still do both, there are certain tasks every developer faces regularly, and frankly, needed to be standardized. Thanks to the mindset of open source, and jQuery being part of that ecosystem, we now have thousands of plug-ins available for a developer to rapidly deploy common functionality without having to craft their own methods. From validations, to form controls/events, and even some epic animations, if you need to something accomplished and don't want to code it yourself (due to ambiguity or common nature) hit up the jQuery plugin registry to solve your woes.

We've already used one such plugin in this article when we illustrated the use of datepicker. Now, we're going to look at a quick file uploader using HTML5 and jQuery, which surprisingly, goes by the name of jquery-file-upload. This plugin makes easy work of file uploads without having an ugly UI, excessive code, or an overly complex user experience for neither the developer nor the user. Below is the basic implementation of the uploader, less the server-side file handler. This code was pulled directory from the project's page linked earlier in this paragraph.

   <form id="fileupload" action="https://jquery-file-upload.appspot.com/" method="POST" enctype="multipart/form-data">
        <noscript><input type="hidden" name="redirect" value="https://blueimp.github.io/jQuery-File-Upload/"></noscript>
        <div class="row fileupload-buttonbar">
            <div class="col-lg-7">
                <span class="btn btn-success fileinput-button">
                    <i class="glyphicon glyphicon-plus"></i>
                    <span>Add files...</span>
                    <input type="file" name="files[]" multiple>
                </span>
                <button type="submit" class="btn btn-primary start">
                    <i class="glyphicon glyphicon-upload"></i>
                    <span>Start upload</span>
                </button>
                <button type="reset" class="btn btn-warning cancel">
                    <i class="glyphicon glyphicon-ban-circle"></i>
                    <span>Cancel upload</span>
                </button>
                <button type="button" class="btn btn-danger delete">
                    <i class="glyphicon glyphicon-trash"></i>
                    <span>Delete</span>
                </button>
                <input type="checkbox" class="toggle">
                <span class="fileupload-process"></span>
            </div>
            <div class="col-lg-5 fileupload-progress fade">
                <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100">
                    <div class="progress-bar progress-bar-success" style="width:0%;"></div>
                </div>
                <div class="progress-extended">&nbsp;</div>
            </div>
        </div>
        <table role="presentation" class="table table-striped"><tbody class="files"></tbody></table>
    </form>

And now for the jQuery -- so simple and elegant, there's no reason not to love this plugin.

$(function () {
    'use strict';

    $('#fileupload').fileupload({
        //xhrFields: {withCredentials: true},
        url: 'assets/plugins/jQuery-File-Upload/server/php/'
    });
    $('#fileupload').fileupload(
        'option',
        'redirect',
        window.location.href.replace(
            /\/[^\/]*$/,
            '/cors/result.html?%s'
        )
    );

    if ($.support.cors) {
            $.ajax({
                url: '//jquery-file-upload.appspot.com/',
                type: 'HEAD'
            }).fail(function () {
                $('<div class="alert alert-danger"/>')
                    .text('Upload server currently unavailable - ' +
                            new Date())
                    .appendTo('#fileupload');
            });
    } else {
        $('#fileupload').addClass('fileupload-processing');
        $.ajax({
            //xhrFields: {withCredentials: true},
            url: $('#fileupload').fileupload('option', 'url'),
            dataType: 'json',
            context: $('#fileupload')[0]
        }).always(function () {
            $(this).removeClass('fileupload-processing');
        }).done(function (result) {
            $(this).fileupload('option', 'done')
                .call(this, null, {result: result});
        });
    }
});

So, the beauty of this plugin is you have to edit very little code and it is incredibly flexible. The maintainers have done well to provide users with a damn near plug-and-play solution to a problem every web developer faces at some point, and common has to readdress. While I could discuss the pros and cons of the many solutions available for this problem, in particular, jQuery-File-Upload presents a great example of the power and flexibility of plugins within the jQuery ecosystem that you can and should take advantage of.

Feature detection - Are you IE or are you not?

The whole reason there are so many frameworks and libraries in this world now is that of compatibility issues. Every developer has a phase where they believe they can solve all of the world's problems by producing this new, exciting framework that functions across every device and browser.... until they realize that their quest is a futile effort. Look at Internet Explorer, for example. Since Microsoft decided the wise decision would be to incorporate that pile into the OS (seriously, who does that?!) we web developers have had a non-stop, pulling teeth, life sucks to be that guy type of battle. For a while, we had to write code specific to each browser, check the user agent on page load, and reference the appropriate resources. Then the major players (not IE) decided to listen to the W3C and adhere to standards. They worked out how a browser should behave across the board. The reason for this: yes, they're in competition with each other, but the users suffer when developers can't anticipate how a certain browser is going to interpret their code. Microsoft still hasn't caught on to this in all their infinite wisdom about user experience.

Aside from my obvious strong distaste for that organization's software, jQuery provides inherent functionality that handles most of what we need in terms of feature detection. There are methods available to handle a vast array of problems, including user agent detection and addressing the lack of certain required features. While jQuery is pretty damn good at this basic stuff, it's still not a bad idea to incorporate a couple additional scripts into any website that relies heavily on not just jQuery, but JavaScript as a whole. Queue normalize.jz and Modernizr.Normalize.js takes JSON returned from APIs and ensures it fits an easy to parse format, which can become problematic for Redux or Flex applications where difficulties arise when parsing nested JSON. Modernizr helps ensure the code you write on your fully updated dev system will function on grandma's Windows 2000 NT box running IE8 (ok, maybe not that far, but it'll help with legacy browsers).

Overall the goal here is to offer a uniform user experience; something we've strived for decades to do and are barely getting on the right track to achieving it. Take a further look at the documentation to find out more about what other, convenient utilities are available in jQuery. Things such as isNumeric() or isArray(), hell, you can even grep() an array. Super cool methods that serve the specific purpose of making jQuery more versatile.

Compatibility methods - play nice with other frameworks and older browsers

While Modernizr provides various avenues of ensuring your beautiful jQuery will work on older browsers, enabling all the CSS3 and HTML5 magic to do its thing, sometimes you need more than jQuery. Sometimes, another framework has a particular function you're rather fond of or handles an issue you're working to overcome more elegantly than you yourself want to re-engineer in jQuery. So, you load up that script then low and behold, console is full of error messages. You've got conflicts flying around like crazy, and your site just isn't loading now.

This is where jQuery has yet another awesome feature. Using one of the many compatibility methods you are able to circumvent many of the issues that can come about when mixing frameworks and libraries together. Since jQuery and the majority of its tools are contained in the jQuery namespace, and most of the global objects are also stored in the jQuery namespace, you shouldn't run into issues. The problem arises when another framework chooses to use $ as a shortcut too. jQuery by default uses this symbol as its shortcut (remember our $.ajax call, the $ is short for jQuery). To alleviate this issue jQuery has an awesome method that needs to be called immediately after jQuery is loaded on the page and before you use any jQuery.

var $j = jQuery.noConflict();

What we just did there is created our own shortcut. This will take care of any conflicts -- the one issue being if you have already written extensive jQuery scripts, you're going to have to go through all of them and replace $ with $j. BUT this takes care of that problem. On to the next.

Multibrowser support

Given the fact that the whole purpose of having a language like JavaScript is to make our pages more dynamic and responsive to user interaction, it's mind boggling that we still deal with certain browsers not adhering to the same rules so many others do. Not to mention there are still tons of legacy systems out there on the internet that may want to take advantage of your new app. Unfortunately, jQuery 2.x broke backwards support for browsers older than IE9. While I personally don't disagree with this move, it does pose a set of problems that you might want to consider depending on who you're targeting. jQuery 1.x supports browsers as old as IE6, but you won't get to take advantage of many of the bugs fixes that were implemented in the 2.x series, as well as various new methods that weren't in the older versions.

Another aspect consider is this is yet another reason to make sure you're doing feature checks on $(document).ready(). For instance, I typically run a check for IE/Mozilla/Chrome browsers. If the visiting user is on anything older than IE10, I display a message saying their browser may not be supported and they should consider upgrading to a modern browser, then give them a gentle nudge towards Chrome. This is typically not intrusive, and doesn't impact their UX significantly, but I feel it's a stance that needs to be taken to ensure good usability, and proper security hygeine for the less informed. Without taking off on a tangent, it is up to us to ensure our users are practicing proper security hygeine. They oftentimes aren't aware that old software is full of vulnerabilities, and while we don't have to educate them on CSRF, Remote File Inclusions, XSS, and a slough of other issues, they do need to at very least be reminded to update their software. Continuing.

Concluding our discussion on jQuery

While this article is titled "DOM Manipulation with jQuery," I took different approach through it. It's moreso an understanding of jQuery article than a how-to manipulate the DOM piece. The reality is by understanding the fundamentals of this beautiful library you can unleash impressive interactions that will delight your users and increase your productivity. In addition, it sets you up to dive deeper into other, higher level abstraction layers such as CoffeeScript. The elegance that jQuery provided us in the early days of Web 2.0 will not be forgotten anytime soon, and I see it staying around as a staple for any seasoned web developer. I remember not wanting to do anything more than absolutely necessary when I first started (back in the days of HTML 4 and XHTML 1.0). JavaScript, and subsequently jQuery have come a long way since those early days, and I hope this piece shed some light on the subject for you if you're considering pursuing it as a viable means to an ends. Please, stop torturing yourself with document.getElementByWhatever and start using $('[CSS_SELECTOR]').doSomething().

Thanks for reading! Be sure to let me know what you think.

Originally posted on my blog at https://www.thomasjost.com/blogs/dom-manipulation-with-jquery

要查看或添加评论,请登录

Thomas J.的更多文章

社区洞察

其他会员也浏览了