Наш канал в telegram

Как сделать поле для ввода текста на svg

Не так давно, работая над web-мордой одного интересного устройства (кстати, если что, я теперь принимаю заказы на создание web-интерфейсов на svg), мне пришлось решать задачу создания полей для ввода текста. В спецификации svg такие поля отсутствуют, в интернете адекватных решений найдено не было, так что пришлось изобретать велосипед самому (на самом деле я конечно делал это не один, посильную помощь оказывал Virtual, за что ему отдельное спасибо). Велосипед в итоге был изобретён и сегодня я хочу рассказать как и на каких граблях при этом пришлось попрыгать и что в конце-концов получилось.

Итак, как я уже сказал, в спецификации svg поля для ввода текста отсутствуют. Самым оптимальным и простым мне показалось решение инжектировать поля ввода из xhml. Делается это с помощью специального тега — foreignObject. Смотрим код примера и как это выглядит:

Код под катом

<svg id="svg_root_pr1"
	xmlns="http://www.w3.org/2000/svg"
	xmlns:xlink="http://www.w3.org/1999/xlink"
	xmlns:ev="http://www.w3.org/2001/xml-events"
	width="100%" height="100%" viewBox="0 0 100 50">
 
<rect id="document_fon_pr1" x="0" y="0" width="100%" height="100%" opacity="0"/>
 
<!-- вставка в SVG-документ нашей картинки -->
<svg id="picture_pr1"
	xmlns="http://www.w3.org/2000/svg"
	xmlns:xlink="http://www.w3.org/1999/xlink"
	xmlns:ev="http://www.w3.org/2001/xml-events"
	x="0" y="0" width="100" height="50">
<!-- Created by Pozianos Yuriy (aka rhf-admin), for http://radiohlam.ru -->
 
<!-- стили -->
<style type="text/css">
	.font
	{	font-family: Arial, Helvetica, sans-serif;
		font-size: 1em;
		font-weight: normal;
	}
</style>
 
<!-- рисунок -->
<rect id="picture_fon_pr1" x="1" y="1" width="98" height="48" rx="5" ry="5" stroke="#999999" fill="#FFFFFF"/>
 
<foreignObject x="10" y="24" width="183" height="23">
	<input xmlns="http://www.w3.org/1999/xhtml" id="text_id_pr1" class="font" maxlength="30" value="default_text"
		style="border: none; background: rgba(255,255,255,0); padding: 0"/>
</foreignObject>
 
</svg>
 
</svg>

[свернуть]

Этот код отлично работает почти во всех браузерах и, казалось бы, что тут ещё изобретать. Но не всё так просто. При попытке прочитать или записать введённый в поле input текст нас ожидают первые грабли. И заключаются они в том, что value — это не атрибут, а свойство объекта input.

То есть, попытки писать и читать value при помощи document.getElementById(‘text_id’).setAttribute(«value»,variable) и variable = document.getElementById(‘text_id’).getAttribute(«value») обречены на провал. Никаких ошибок при этом выдаваться не будет, более того, увидев , что атрибута value у объекта input не существует, скрипт его создаст и будет исправно записывать туда значение указанной переменной, а так же читать его обратно, вот только к отображаемому на экране свойству value объекта input всё это не будет иметь никакого отношения.

Ещё раз повторюсь, value — это свойство, а значит правильный доступ к нему вот такой — document.getElementById(‘text_id’).value

Теперь давайте перепрыгнем на другие грабли, — попробуем расположить какой-нибудь svg-объект над нашим инжектированным объектом input. Скажем, сделаем выпадающее меню, которое при открытии будет перекрывать input. Смотрим код и пример ниже.

Код под катом

<svg id="svg_root_pr2"
	xmlns="http://www.w3.org/2000/svg"
	xmlns:xlink="http://www.w3.org/1999/xlink"
	xmlns:ev="http://www.w3.org/2001/xml-events"
	width="100%" height="100%" viewBox="0 0 100 50">
 
<rect id="document_fon_pr2" x="0" y="0" width="100%" height="100%" opacity="0"/>
 
<!-- вставка в SVG-документ нашей картинки -->
<svg id="picture_pr2"
	xmlns="http://www.w3.org/2000/svg"
	xmlns:xlink="http://www.w3.org/1999/xlink"
	xmlns:ev="http://www.w3.org/2001/xml-events"
	x="0" y="0" width="100" height="50">
<!-- Created by Pozianos Yuriy (aka rhf-admin), for http://radiohlam.ru -->
 
<!-- стили -->
<style type="text/css">
	.font
	{	font-family: Arial, Helvetica, sans-serif;
		font-size: 1em;
		font-weight: normal;
	}
</style>
 
<!-- рисунок -->
<rect id="picture_fon_pr2" x="1" y="1" width="98" height="48" rx="5" ry="5" stroke="#999999" fill="#FFFFFF"/>
 
<rect id="button_pr2" x="20" y="8" width="60" height="17" rx="5" ry="5" stroke="#999999" fill="#CCCCCC"/>
<text class="font" fill="black" y="22"><tspan x="50" text-anchor="middle">Menu</tspan></text>
<rect x="20" y="8" width="62" height="17" rx="5" ry="5" opacity="0"
	onmouseover="button_hover('button_pr2')" onmouseout="button_normal('button_pr2')"
	onmousedown="button_active('button_pr2')" onmouseup="button_hover('button_pr2')" onclick="menu('menu_pr2')"/>
 
<foreignObject x="10" y="24" width="183" height="23">
	<input xmlns="http://www.w3.org/1999/xhtml" id="text_id_pr2" class="font" maxlength="30" value="default_text"
		style="border: none; background: rgba(255,255,255,0); padding: 0"/>
</foreignObject>
 
<g id="menu_pr2" display="none">
	<rect id="sel1_pr2" x="1" y="25" width="48" height="17" rx="5" ry="5" stroke="#999999" fill="#CCCCCC"/>
	<text class="font" fill="black" y="39"><tspan x="25" text-anchor="middle">sel1</tspan></text>
	<rect x="1" y="25" width="48" height="17" rx="5" ry="5" opacity="0"
		onmouseover="button_hover('sel1_pr2')" onmouseout="button_normal('sel1_pr2')"
		onmousedown="button_active('sel1_pr2')" onmouseup="button_hover('sel1_pr2')"/>
 
	<rect id="sel2_pr2" x="51" y="25" width="48" height="17" rx="5" ry="5" stroke="#999999" fill="#CCCCCC"/>
	<text class="font" fill="black" y="39"><tspan x="75" text-anchor="middle">sel2</tspan></text>
	<rect x="51" y="25" width="48" height="17" rx="5" ry="5" opacity="0"
		onmouseover="button_hover('sel2_pr2')" onmouseout="button_normal('sel2_pr2')"
		onmousedown="button_active('sel2_pr2')" onmouseup="button_hover('sel2_pr2')"/>
</g>
 
</svg>
 
<!-- скрипты -->
<script type="text/javascript">
<![CDATA[
	function menu(who)
	{	var state = document.getElementById(who).getAttribute("display");	//получаем текущее состояние
		if(state=="none")			//если элемент не отображается
		{	document.getElementById(who).setAttribute("display","yes");	}	//включаем отображение меню
		else
		{	document.getElementById(who).setAttribute("display","none");}	//выключаем отображение меню
	}
 
	// button animate
	function button_normal(who)
	{	document.getElementById(who).setAttribute("fill","#CCCCCC");	}
	function button_hover(who)
	{	document.getElementById(who).setAttribute("fill","#999999");	}
	function button_active(who)
	{	document.getElementById(who).setAttribute("fill","#666666");	}
]]>
</script>
 
</svg>

[свернуть]
Menu sel1 sel2

Как видите, судя по коду, выпадающее меню должно располагаться над инжектированным полем ввода. Однако легко убедиться, что на самом деле этого не происходит (покликайте на кнопке Menu). Вместо этого объект input «просвечивает» через svg-элементы и оказывается на переднем плане.

Здесь всё оказалось немного сложнее. Как выяснилось, перекрыть поле input не удаётся вообще никакими svg-элементами. Тогда было решено, что раз нельзя это поле перекрыть, то его нужно стереть. В svg для этого есть специальный атрибут — display, который при установке значения в none просто не отрисовывает соответствующий объект на экране вместе со всеми дочерними объектами (выше мы им уже пользовались для скрытия пунктов выпадающего меню).

То есть, идея следующая, — будем показывать на экране поле input не постоянно, а только тогда, когда нам это действительно нужно (когда реально нужно отредактировать текст). Всё остальное время, будем показывать на месте поля input обычный svg-текст, содержимое которого совпадает с содержимым поля input (а само поле input будем с экрана стирать).

Сразу замечу, что скрывать поле ввода, делая его невидимым с помощью свойства visible или атрибута opacity смысла не имеет, — оно всё равно будет располагаться над всеми остальными элементами, просто вы не будете видеть, что туда вводите (невидимость же, ага).

Итак, осталось только решить, как определять, когда пользователю нужно отредактировать текст, а когда нет (то есть когда нужно менять местами на экране поле input и svg-текст).

Я предлагаю следующий вариант. Если пользователь пытается кликнуть по тексту мышью (событие onclick объекта svg-текст) — значит нужно переключаться в режим редактирования (скрывать svg-текст и показывать поле input), если поле ввода потеряло фокус (событие onblur объекта input) — нужно переключаться в режим отображения svg-текста (и скрывать поле input).

Сразу скажу, что тут есть ещё несколько видов граблей.

Первые связаны с тем, что для того, чтобы потерять фокус — нужно сначала получить фокус. А если мы при клике по svg-тексту будем просто выключать svg-текст и включать xhtml поле input, то это вовсе не означает, что оно сразу получит фокус. Не получит. Фокус перейдёт на это поле только после того, как вы по нему кликните, а пока вы его просто включили.

Следовательно и потерять фокус поле input сможет не раньше, чем вы по нему кликните и передадите ему таким образом фокус. Помните, как в «Простоквашино», — Чтобы продать что-нибудь ненужное, — нужно сначала купить что-нибудь ненужное, а у нас денег нет!

Думаю не нужно долго объяснять к чему это может привести. Если вы, к примеру, кликнули по svg-тексту (и включили таким образом отображение поля input), а затем передумали и кликнули по выпадающему меню, — поле input не сможет снова поменяться местами с svg-текстом (оно же не теряло фокус) и опять будет просвечивать через меню.

Чтобы избежать возникновения подобной коллизии, — нужно принудительно устанавливать фокус на поле input в той же функции, которая делает это поле видимым. Делается это вызовом метода focus() для объекта input.

Вторые грабли связаны с переменными размерами svg-текста, зависящими от размера и количества символов в этом тексте. То есть, если мы в поле input введём пустое значение (сотрём в нём весь текст) и потом это пустое значение перезапишется в svg-текст, то этот svg-текст будет иметь нулевую ширину. Ну и как потом по нему кликнуть, чтобы снова перейти в режим редактирования?

Для решения этой проблемы можно сделать над svg-текстом прозрачную накладку фиксированной ширины (равной размеру поля ввода) и выполнять переход в режим редактирования после клика по этой накладке, а не по svg-тексту. Естественно, эту накладку нужно включать и выключать вместе с svg-текстом.

Ниже приведён пример полностью рабочего кода и того, как это будет выглядеть на экране.

Код под катом

<svg id="svg_root_pr3"
	xmlns="http://www.w3.org/2000/svg"
	xmlns:xlink="http://www.w3.org/1999/xlink"
	xmlns:ev="http://www.w3.org/2001/xml-events"
	width="100%" height="100%" viewBox="0 0 100 50">
 
<rect id="document_fon_pr3" x="0" y="0" width="100%" height="100%" opacity="0"/>
 
<!-- вставка в SVG-документ нашей картинки -->
<svg id="picture_pr3"
	xmlns="http://www.w3.org/2000/svg"
	xmlns:xlink="http://www.w3.org/1999/xlink"
	xmlns:ev="http://www.w3.org/2001/xml-events"
	x="0" y="0" width="100" height="50">
<!-- Created by Pozianos Yuriy (aka rhf-admin), for http://radiohlam.ru -->
 
<!-- стили -->
<style type="text/css">
	.font
	{	font-family: Arial, Helvetica, sans-serif;
		font-size: 1em;
		font-weight: normal;
	}
</style>
 
<!-- рисунок -->
<rect id="picture_fon_pr3" x="1" y="1" width="98" height="48" rx="5" ry="5" stroke="#999999" fill="#FFFFFF"/>
 
<rect id="button_pr3" x="20" y="8" width="60" height="17" rx="5" ry="5" stroke="#999999" fill="#CCCCCC"/>
<text class="font" fill="black" y="22"><tspan x="50" text-anchor="middle">Menu</tspan></text>
<rect x="20" y="8" width="62" height="17" rx="5" ry="5" opacity="0"
	onmouseover="button_hover('button_pr3')" onmouseout="button_normal('button_pr3')"
	onmousedown="button_active('button_pr3')" onmouseup="button_hover('button_pr3')" onclick="menu('menu_pr3')"/>
 
<text id="edit_pr3_s" display="yes" class="font" fill="black" x="10" y="40">default_text</text>
<rect id="edit_pr3_p" display="yes" x="10" y="26" onclick="edit_html('edit_pr3')" width="85" height="17" rx="5" ry="5" opacity="0"/>
<g id="edit_pr3" display="none">	<!-- контейнер для xhtml-объекта input -->
	<foreignObject x="10" y="24" width="100" height="23">
		<input xmlns="http://www.w3.org/1999/xhtml" id="edit_pr3_h" onblur="edit_svg('edit_pr3')" class="font" maxlength="12" value=""
			style="border: none; background: rgba(255,255,255,0); padding: 0"/>
	</foreignObject>
</g>
 
<g id="menu_pr3" display="none">
	<rect id="picture_fon_pr3" x="1" y="1" width="98" height="48" rx="5" ry="5" opacity="0" onclick="menu('menu_pr3')"/>
 
	<rect id="sel1_pr3" x="1" y="25" width="48" height="17" rx="5" ry="5" stroke="#999999" fill="#CCCCCC"/>
	<text class="font" fill="black" y="39"><tspan x="25" text-anchor="middle">sel1</tspan></text>
	<rect x="1" y="25" width="48" height="17" rx="5" ry="5" opacity="0"
		onmouseover="button_hover('sel1_pr3')" onmouseout="button_normal('sel1_pr3')"
		onmousedown="button_active('sel1_pr3')" onmouseup="button_hover('sel1_pr3')"/>
 
	<rect id="sel2_pr3" x="51" y="25" width="48" height="17" rx="5" ry="5" stroke="#999999" fill="#CCCCCC"/>
	<text class="font" fill="black" y="39"><tspan x="75" text-anchor="middle">sel2</tspan></text>
	<rect x="51" y="25" width="48" height="17" rx="5" ry="5" opacity="0"
		onmouseover="button_hover('sel2_pr3')" onmouseout="button_normal('sel2_pr3')"
		onmousedown="button_active('sel2_pr3')" onmouseup="button_hover('sel2_pr3')"/>
</g>
 
</svg>
 
<!-- скрипты -->
<script type="text/javascript">
<![CDATA[
	function menu(who)
	{	var state = document.getElementById(who).getAttribute("display");	//получаем текущее состояние
		if(state=="none")			//если элемент не отображается
		{	document.getElementById(who).setAttribute("display","yes");	}	//включаем отображение меню
		else
		{	document.getElementById(who).setAttribute("display","none");}	//выключаем отображение меню
	}
 
	// button animate
	function button_normal(who)
	{	document.getElementById(who).setAttribute("fill","#CCCCCC");	}
	function button_hover(who)
	{	document.getElementById(who).setAttribute("fill","#999999");	}
	function button_active(who)
	{	document.getElementById(who).setAttribute("fill","#666666");	}
 
	//switch edit field
	function edit_html(who)				// включаем html, выключаем svg
	{	var edit = document.getElementById(who+'_s').firstChild.data;		//получаем содержимое svg-текста
		document.getElementById(who+'_h').value=edit;						//записываем полученное значение в html-текст
		document.getElementById(who).setAttribute("display","yes");			//включаем html-текст
		document.getElementById(who+'_s').setAttribute("display","none");	//выключаем svg-текст
		document.getElementById(who+'_p').setAttribute("display","none");	//выключаем накладку на svg-текст
		document.getElementById(who+'_h').focus();							//устанавливаем фокус на html-текст
		document.getElementById(who+'_h').selectionStart=edit.length;		//записываем курсор в конец строки
	}
	function edit_svg(who)				// включаем svg, выключаем html
	{	var edit = document.getElementById(who+'_h').value;					//получаем значение поля value html-текста
		document.getElementById(who+'_s').firstChild.data = edit;			//записываем полученное значение в svg-текст
		document.getElementById(who+'_s').setAttribute("display","yes");	//включаем svg-текст
		document.getElementById(who+'_p').setAttribute("display","yes");	//включаем накладку на svg-текст
		document.getElementById(who).setAttribute("display","none");		//выключаем html-текст
	}
]]>
</script>
 
</svg>

[свернуть]
Menu default_text sel1 sel2

Вот и всё, надеюсь эта информация кому-нибудь пригодится. Если просто захотите обсудить те или иные аспекты работы с svg — жду на форуме или в комментариях. Тема достаточно обширная и интересная.

Добавить комментарий