Category Archives: ajax

Годные js библиотеки (1): jsTree

Written by elwood

За последние месяцы я частенько пользовался различными джаваскриптовыми контролами и плагинами к jQuery. Думаю, имеет смысл поделиться инфой о самых полезных из них. Пока список таков:

jstree

Начнём с jsTree. Библиотека представляет собой плагин к jQuery. С помощью jsTree я делал дерево категорий. Категорий много, все сразу загружать проблематично. Поэтому в первую очередь я разобрался с тем, как настраивать аяксовую подгрузку json-данных. После этого самым важным было научиться определять выбранные элементы. Потом занялся улучшением UI – задал отдельную иконку для представления категорий с заданными правилами (это была админка для задания правил к отдельным категориям, соответственно, категории делились на те, для которых правил нет, и те, для которых они уже заданы). Научился открывать нужные ноды при загрузке дерева автоматически (при этом ajax-запрос на получение дочерних элементов формируется автоматически). Ну и напоследок включил отображение выделенного элемента а-ля OS X, с выделением всей строки полностью. Рассмотрим эти шаги подробнее. Возможно, опытным джаваскриптерам приведённые рецепты и пояснения покажутся чересчур банальными, но лично мне бы это очень помогло.

(more…)

Spring @ResponseBody и UTF-8

Written by elwood

С недавних пор я отказался от хитрых способов для реализации ajax-запросов (таких, как DWR, к примеру), отдав предпочтение обычным спринговым обработчикам, помеченным аннотацией @ResponseBody. Настроил конвертеры для json и xml, добавив их в список конвертеров AnnotationMethodHandlerAdapter. Но, как оказалось, настроил я не всё.

Дело в том, что обычно если мне нужно было получить кусок разметки при помощи ajax, я не выставлял @ResponseBody (поскольку это был “обычный” запрос), а в методе возвращал название view. Spring брал по названию view нужный jsp (хотя у меня не напрямую jsp, а через Tiles, что не суть важно). Однако мне вдруг понадобилось напрямую вернуть String. И как оказалось, конвертер StringHttpMessageConverter упрямо перекодирует возвращаемую мной строку в ISO-8859-1, свою дефолтную кодировку. Причем ни выставленные Accepts и Accept-Charset заголовки, ни указанное в @RequestMapping‘e свойство produce=”text/html;charset=UTF-8″, ни сконфигурированные в конвертере supportedMediaTypes не влияли на результат.

Полез дебагером внутрь конвертера, где выяснил, что действительно, это всё не влияет на определение нужной кодировки. А влияет лишь переданный в конвертер MediaType, который как правило содержал только mime-type, а кодировка была установлена в null. После чего конвертер, видя что ему пришел null, брал свою дефолтную кодировку и применял её к строке. Причем эту дефолтную кодировку нельзя задать ни в конструкторе конвертера, ни с помощью сеттера. Пришлось пойти на крайние меры – сделать форк конвертера в свой класс, добавить возможность задания дефолтной кодировки и заменить стандартный конвертер на свой в конфиге AnnotationMethodHandlerAdapter‘a.

Код:

/**
 * User: igor.kostromin
 * Date: 22.08.12
 * Time: 12:11
 *
 * Differs from StringHttpMessageConverter only in default charset specifying feature.
 */
public final class ConfigurableStringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
 
    private Charset defaultCharset;
 
    public Charset getDefaultCharset() {
        return defaultCharset;
    }
 
	private final List<Charset> availableCharsets;
 
	private boolean writeAcceptCharset = true;
 
	public ConfigurableStringHttpMessageConverter() {
		super(new MediaType("text", "plain", StringHttpMessageConverter.DEFAULT_CHARSET), MediaType.ALL);
        defaultCharset = StringHttpMessageConverter.DEFAULT_CHARSET;
		this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
	}
 
    public ConfigurableStringHttpMessageConverter(String charsetName) {
        super(new MediaType("text", "plain", Charset.forName(charsetName)), MediaType.ALL);
        defaultCharset = Charset.forName(charsetName);
		this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
    }
 
	/**
	 * Indicates whether the {@code Accept-Charset} should be written to any outgoing request.
	 * <p>Default is {@code true}.
	 */
	public void setWriteAcceptCharset(boolean writeAcceptCharset) {
		this.writeAcceptCharset = writeAcceptCharset;
	}
 
	@Override
	public boolean supports(Class<?> clazz) {
		return String.class.equals(clazz);
	}
 
	@Override
	protected String readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException {
		Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
		return FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset));
	}
 
	@Override
	protected Long getContentLength(String s, MediaType contentType) {
		Charset charset = getContentTypeCharset(contentType);
		try {
			return (long) s.getBytes(charset.name()).length;
		}
		catch (UnsupportedEncodingException ex) {
			// should not occur
			throw new InternalError(ex.getMessage());
		}
	}
 
	@Override
	protected void writeInternal(String s, HttpOutputMessage outputMessage) throws IOException {
		if (writeAcceptCharset) {
			outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
		}
		Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
		FileCopyUtils.copy(s, new OutputStreamWriter(outputMessage.getBody(), charset));
	}
 
	/**
	 * Return the list of supported {@link Charset}.
	 *
	 * <p>By default, returns {@link Charset#availableCharsets()}. Can be overridden in subclasses.
	 *
	 * @return the list of accepted charsets
	 */
	protected List<Charset> getAcceptedCharsets() {
		return this.availableCharsets;
	}
 
	private Charset getContentTypeCharset(MediaType contentType) {
		if (contentType != null && contentType.getCharSet() != null) {
			return contentType.getCharSet();
		}
		else {
			return defaultCharset;
		}
	}
}

Использование:

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="messageConverters">
            <util:list>
                <bean class="ru.dz.mvk.util.ConfigurableStringHttpMessageConverter">
                    <constructor-arg index="0" value="UTF-8"/>
                </bean>
                <bean class="org.springframework.http.converter.FormHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter"/>
                <bean class="org.springframework.http.converter.BufferedImageHttpMessageConverter"/>
                <!-- json converter (for application/json multimedia type) -->
                <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
                <!-- xml converter (for application/xhtml+xml)-->
                <bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
                    <property name="marshaller" ref="xstreamMarshaller"/>
                    <property name="unmarshaller" ref="xstreamMarshaller"/>
                </bean>
            </util:list>
        </property>
</bean>

Ajax – динамически загружаемый javascript

Written by elwood

Хочу поделиться маленьким велосипедом. Частенько нам нужно загрузить динамически кусок html-разметки (например, какой-нибудь div) и вставить его в DOM страницы. Иногда этот кусок разметки содержит javascript в виде некоторых обработчиков событий. Но если загружать его в ajax запросе, то javascript выполнен не будет (и функции, которые вы определили, тоже не будут зарегистрированы). Для того, чтобы заставить это работать, нужно либо вынести весь js-код в отдельный js-файл и указать его на главной странице заблаговременно, либо ограничивать себя использованием только inline-javascript (это когда мы пишем <input type=”button” onclick=”someJsCode()”/>. В моем случае оба варианта были неудобны, а самым удобным был именно обычный javascript в загружаемых кусочках разметки (потому что IDE при редактировании этих частей лучше понимает контекст – не возникает варнингов о “неизвестных” функциях, айдишниках элементов, переменных итд”. Поэтому я сочинил функцию, которая принимает в качестве аргумента загруженную разметку, выдирает оттуда скрипты (содержимое тегов <script>), склеивает их в один элемент <script> и добавляет в document.body. После этого javascript-функции, определенные в загруженных скриптах регистрируются в пространстве имен, и нормально работают. Причем, если при следующем ajax-запросе необходимо будет поменять реализацию некоторых функций, ничего дополнительно делать не нужно – js-интерпретатор затрет старые функции (по имени) новыми.

Плюс к этому, так как код скриптов нам больше не нужен, и будет только мешаться при отладке, другая функция убирает все script-теги и возвращает разметку уже без скриптов. Её можно вставить в innerHTML элемента, куда мы хотели запихнуть динамический контент.

Код функции:

/**
 * Concatenates all javascript code in html into one big script and
 * creates appends script element with this code to document body.
 * @param html Markup with embedded scripts retrieved using ajax.
 *
 * TODO : mb additional work required for scripts with specified src
 */
function processEmbeddedScripts(html) {
    var tempDiv = document.createElement('div');
    tempDiv.innerHTML = html;
    var scriptNodesList = tempDiv.getElementsByTagName('script');
    if (scriptNodesList.length > 0) {
        var wholeScript = '';
        for (var i = 0; i < scriptNodesList.length; i++) {
            var scriptCode = scriptNodesList.item(i);
            wholeScript = wholeScript + scriptCode.innerHTML + ' ';
        }
        var scriptElement = document.createElement('script');
        scriptElement.type = 'text/javascript';
        scriptElement.text = wholeScript;
        document.body.appendChild(scriptElement);
    }
}
 
/**
 * Removes all embedded scripts from specified piece of html markup
 * and returns the shrinked markup.
 */
function excludeEmbeddedScripts(html) {
    return html.replace(/<script(.|\s)*?\/script>/g, '');
}

Использование:

// получаем html, очищенный от скриптов
var htmlWithoutScripts = excludeEmbeddedScripts(rawHtml);
// добавляем этот html в DOM
document.getElementById('mydiv').innerHTML = htmlWithoutScripts;
// и после того, как мы имеем уже полностью собранный DOM, достаем скрипты и регистрируем их
// в документе. при этом скрипты будут выполнены браузером.
processEmbeddedScripts(rawHtml);

Возможно (см TODO), еще придется доработать этот велосипед для скриптов с указанным src (их нужно пропихивать в document.body без text, но с указанным src).

PS: При повторных выполнениях этого кода старые функции затираются новым кодом (это корректное поведение).

PS-2: В некоторых браузерах подобных подход тяжело отлаживать, поскольку добавленные таким образом скрипты браузер в инструментах разработчика либо не отображает полностью, либо не отображает до тех пор, пока не случится брейпоинт в этих кусочках кода. Поэтому приходится этот метод использовать редко, предпочитая классически выносить весь js-код в отдельные файлы.