Клиентский JavaScript предназначен для того, чтобы превращать статические HTML-документы в интерактивные веб-приложения. Работа с содержимым вебстраниц – главное предназначение JavaScript. Данная глава является одной из наиболее важных в этой книге – здесь рассказывается о том, как это делается.
В главах 13 и 14 говорилось, что каждое окно, вкладка и фрейм веб-браузера
представлено объектом Window
. Каждый объект Window
имеет свойство document
,
ссылающееся на объект Document
. Этот объект Document
и является темой обсуждения
данной главы. Однако объект Document
не является автономным объектом. Он
является центральным объектом обширного API, известного как объектная модель
документа (Document Object Model, DOM), который определяет порядок доступа
к содержимому документа.
Эта глава начинается с описания базовой архитектуры DOM, а затем она расскажет:
В заключительном разделе этой главы рассматриваются различные особенности
документов, включая свойство referrer
, метод write()
и приемы получения текста,
выделенного в документе.
Объектная модель документа (Document Object Model, DOM) – это фундаментальный прикладной программный интерфейс, обеспечивающий возможность работы с содержимым HTML- и XML-документов. Прикладной программный интерфейс (API) модели DOM не особенно сложен, но в нем существует множество архитектурных особенностей, которые вы должны знать
Прежде всего, следует понимать, что вложенные элементы HTML- или
XML-документов представлены в виде дерева объектов DOM. Древовидное представление
HTML-документа содержит узлы, представляющие элементы или теги, такие как
<body>
и <p>
, и узлы, представляющие строки текста. HTML-документ также
может содержать узлы, представляющие HTML-комментарии. Рассмотрим
следующий простой HTML-документ:
<html> <head> <title>Sample Document</title> </head> <body> <h1>An HTML Document</h1> <p>This is a <i>simple</i> document. </html>
DOM-представление этого документа приводится на рис. 15.1.
Тем, кто еще не знаком с древовидными структурами в компьютерном программировании, полезно узнать, что терминология для их описания была заимствована у генеалогических деревьев. Узел, расположенный непосредственно над данным узлом, называется родительским по отношению к данному узлу. Узлы, расположенные на один уровень ниже другого узла, являются дочерними по отношению к данному узлу. Узлы, находящиеся на том же уровне и имеющие того же родителя, называются братьями. Узлы, расположенные на любое число уровней ниже другого узла, являются его потомками. Родительские, прародительские и любые другие узлы, расположенные выше данного узла, являются его предками.Каждый прямоугольник на рис. 15.1 является узлом документа, который
представлен объектом Node
. О свойствах и методах объекта Node
будет рассказываться
в некоторых разделах, следующих ниже, кроме того, описания этих свойств вы
найдете в справочной статье Node
в четвертой части книги. Обратите внимание,
что на рисунке изображено три различных типа узлов.
Document
, который представляет документ целиком.Element
, а Text
.Document
, Element
и Text
–
это подклассы класса Node
, и для них
имеются отдельные справочные статьи в четвертой части книги. Document
и Element
являются двумя самыми важными классами в модели DOM, и большая часть
главы посвящена знакомству с их свойствами и методами.
Рис. 15.1. Древовидное представление HTML-документа
Обратите внимание на формальные отличия между обобщенными типами Document
и Element
и типами HTMLDocument
и HTMLElement
. Тип Document
представляет HTML-
или XML-документ, а класс Element
представляет элемент этого документа.
Подклассы HTMLDocument
и HTMLElement
представляют конкретно HTML-документ и его
элементы. В этой книге часто используются имена обобщенных классов Document
и Element
, даже когда подразумеваются HTML-документы. То же самое относится
и к справочному разделу книги: свойства и методы типов HTMLDocument
и
HTMLElement
описываются в справочных статьях Document
и Element
.
На рис. 15.2 следует также отметить наличие большого количества подтипов
класса HTMLElement
, представляющих конкретные типы HTML-элементов.
Каждый из них определяет JavaScript-свойства, отражающие HTML-атрибуты
конкретного элемента или группы элементов (раздел 15.4.1). Некоторые из этих
специфических классов определяют дополнительные свойства или методы, которые
не являются отражением синтаксиса языка разметки HTML. Более подробно эти
классы и их дополнительные особенности рассматриваются в справочном
разделе книги.
Наконец, обратите внимание, что на рис. 15.2 изображены некоторые типы узлов,
которые нигде до сих пор не упоминались. Узлы Comment
представляют HTML- или
XML-комментарии. Поскольку комментарии являются обычными текстовыми
строками, эти узлы во многом подобны узлам Text
, представляющим
отображаемый текст документа. Тип CharacterData
, обобщенный предок типов Text
и Comment
,
определяет методы, общие для узлов этих двух типов. Узлы типа Attr
представляют XML- или HTML-атрибуты, но они практически никогда не используются,
потому что класс Element
определяет методы, позволяющие интерпретировать
атрибуты, как пары имя/значение, а не как узлы документа. Объект DocumentFragment
(не изображен на рисунке) – это разновидность узлов, которая практически
никогда не встречается в документах: он представляет последовательность узлов, не
имеющих общего родителя. Объекты DocumentFragment
удобно использовать при
выполнении манипуляций над документами, и подробнее об этом типе
рассказывается в разделе 15.6.4. Модель DOM также определяет несколько редко
используемых типов, представляющих, например, объявления типа документа и
инструкции обработки XML.
Рис. 15.2. Неполная иерархия классов узлов документов
Работа большинства клиентских программ на языке JavaScript так или иначе
связана с манипулированием элементами документа. В ходе выполнения эти
программы могут использовать глобальную переменную document
, ссылающуюся на
объект Document
. Однако чтобы выполнить какие-либо манипуляции с
элементами документа программа должна каким-то образом получить или выбрать
объекты Element
, ссылающиеся на эти элементы документа. Модель DOM
определяет несколько способов выборки элементов. Выбрать элемент или элементы
документа можно:
id
;
name
;
Все эти приемы выборки элементов описываются в следующих подразделах.
Все HTML-элементы имеют атрибуты id
. Значение этого атрибута должно быть
уникальным в пределах документа – никакие два элемента в одном и том же
документе не должны иметь одинаковые значения атрибута id
. Выбрать элемент по
уникальному значению атрибута id
можно с помощью метода getElementById()
объекта Document
. Этот метод уже использовался в примерах глав 13 и 14:
var section1 = document.getElementById("section1");
Это самый простой и самый распространенный способ выборки элементов. Если
сценарию необходимо иметь возможность манипулировать каким-то
определенным множеством элементов документа, задайте этим элементам атрибуты
id
и используйте возможность их поиска по их id
. Если потребуется
отыскать более одного элемента по значению атрибута id
, можно
воспользоваться удобной функцией getElements()
, реализация которой приводится в
примере 15.1.
Пример 15.1. Поиск нескольких элементов по значениям атрибута id
/** * Эта функция принимает произвольное количество строковых аргументов. * Каждый аргумент интерпретируется как значение атрибута id элемента * и для каждого из них вызывается метод document.getElementById(). * Она возвращает объект, который отображает значения атрибута id * в соответствующие объекты Element. Если какое-то значение атрибута id * не будет найдено в документе, возбуждает исключение Error. */ function getElements(/*значения атрибутов id...*/) { var elements = {}; // Создать пустое отображение. for(var i = 0; i < arguments.length; i++) { // Для каждого аргумента, var id = arguments[i]; // где аргумент - id элемента, var elt = document.getElementById(id); // отыскать элемент. if (elt == null) // Если не найден, возбудить ошибку. throw new Error("No element with id: " + id); elements[id] = elt; // Отобразить id в элемент } return elements;// Вернуть отображение id в элементы }
В версиях Internet Explorer ниже IE8 метод getElementById()
выполняет поиск
значений атрибутов id
без учета регистра символов и, кроме того, возвращает
элементы, в которых будет найдено совпадение со значением атрибута name
.
HTML-атрибут name
первоначально предназначался для присваивания имен
элементам форм, и значение этого атрибута использовалось, когда выполнялась
отправка данных формы на сервер. Подобно атрибуту id
, атрибут name
присваивает
имя элементу. Однако в отличие от id
значение атрибута name
не обязано быть
уникальным: одно и то же имя могут иметь сразу несколько элементов, что
вполне обычно при использовании в формах радиокнопок и флажков. Кроме того,
в отличие от id
атрибут name
допускается указывать лишь в некоторых HTML-элементах,
включая формы, элементы форм и элементы <iframe>
и <img>
.
Выбрать HTML-элементы, опираясь на значения их атрибутов name
, можно с
помощью метода getElementsByName()
объекта Document
:
var radiobuttons = document.getElementsByName("favorite_color");
Метод getElementsByName()
определяется не классом
Document
, а классом HTMLDocument
,
поэтому он доступен только в HTML-документах и не доступен в
XML-документах. Он возвращает объект NodeList
, который ведет себя как доступный
только для чтения массив объектов Element
. В IE метод getElementsByName()
возвращает
также элементы, значения атрибутов id
которых совпадает с указанным
значением. Чтобы обеспечить совместимость с разными версиями браузеров, необходимо
внимательно подходить к выбору значений атрибутов и не использовать одни и те
же строки в качестве значений атрибутов name
и id
.
Мы видели в разделе 14.7,
что наличие атрибута name
в некоторых
HTML-элементах приводит к автоматическому созданию свойств с этими именами в объекте
Window
. To же относится и к объекту Document
. Наличие атрибута name
в элементе
<form>
, <img>
, <iframe>
, <applet>
,
<embed>
или <object>
(но только в том элементе <object>
,
который не имеет вложенных объектов с альтернативным содержимым)
приводит к созданию свойства в объекте Document
, имя которого совпадает со значением
атрибута (при этом предполагается, что объект документа еще не имеет свойства
с этим именем).
Если существует только один элемент с указанным именем, значением
автоматически созданного свойства документа станет сам элемент. Если таких элементов
несколько, значением свойства будет объект NodeList
, играющий роль массива
элементов. Как было показано в разделе 14.7,
для именованных элементов <iframe>
создаются особые свойства документа: они ссылаются не на объекты Element
, а на
объекты Window
, представляющие фреймы.
Это означает, что некоторые элементы могут быть выбраны по их именам
простым обращением к свойствам объекта Document
:
// Получить ссылку на объект Element для элемента // <form name="shipping_address"> var form = document.shipping_address;
Причины, почему не следует использовать автоматически создаваемые свойства
окна, которые описываются в разделе 14.7,
в равной степени применимы и к
автоматически создаваемым свойствам документа. Если вам потребуется отыскать
именованные элементы, лучше всего это сделать явно с помощью метода
getElementsByName()
.
Метод getElementsByTagName()
объекта Document
позволяет выбрать все HTML- или
XML-элементы указанного типа (или по имени тега). Например, получить
подобный массиву объект, доступный только для чтения, содержащий объекты Element
всех элементов <span>
в документе, можно следующим образом:
var spans = document.getElementsByTagName("span");
Подобно методу getElementsByName()
метод getElementsByTagName()
возвращает объект
NodeList
(подробнее класс NodeList
описывается во врезке в этом же разделе).
Элементы документа включаются в массив NodeList
в том же порядке, в каком они
следуют в документе, т. е. первый элемент <p>
в документе можно выбрать так:
var firstpara = document.getElementsByTagName("p")[0];
Имена HTML-тегов не чувствительны к регистру символов, и когда
getElementsByTagName()
применяется к HTML-документу, он выполняет сравнение с именем
тега без учета регистра символов. Переменная spans
, созданная выше, например,
будет включать также все элементы <span>
, которые записаны как <SPAN>
.
Можно получить NodeList
, содержащий все элементы документа, если передать
методу getElementsByTagName()
шаблонный символ «*
».
Класс Element
тоже определяет метод getElementsByTagName()
. Он
действует точно так же, как и версия метода в классе Document
, но выбирает
только элементы, являющиеся потомками для элемента, относительно которого
вызывается метод. То есть отыскать все элементы <span>
внутри первого элемента
<p>
можно следующим образом:
var firstpara = document.getElementsByTagName("p")[0];
var firstParaSpans = firstpara.getElementsByTagName("span");
По историческим причинам класс HTMLDocument
определяет специальные свойства
для доступа к узлам определенных типов. Свойства images
,
forms
и links
,
например, ссылаются на объекты, которые ведут себя как массивы, доступные только
для чтения, содержащие элементы <img>
, <form>
и <a>
(но только те теги <a>
,
которые имеют атрибут href
). Эти свойства ссылаются на объекты HTMLCollection
,
которые во многом похожи на объекты NodeList
, но дополнительно могут
индексироваться значениями атрибутов id
и name
. Ранее мы узнали, как можно получить
ссылку на именованный элемент <form>
с помощью такого выражения:
document.shipping_address
С помощью свойства document.forms
обращение к форме, имеющей атрибут name
(или id
), можно записать более конкретно:
document.forms.shipping_address;
Объект HTMLDocument
также определяет свойства-синонимы embeds
и plugins
,
являющиеся коллекциями HTMLCollection
элементов <embed>
. Свойство anchors
является нестандартным, но с его помощью можно получить доступ к элементам <a>
,
имеющим атрибут name
, но не имеющим атрибут href
. Свойство scripts
определено
стандартом HTML5 и является коллекцией HTMLCollection
элементов <script>
, но
к моменту написания этих строк оно было реализовано не во всех браузерах.
Кроме того, объект HTMLDocument
определяет два свойства, каждое из которых
ссылается не на коллекцию, а на единственный элемент. Свойство document.body
представляет элемент <body>
HTML-документа, а свойство document.head
– элемент <head>
.
Эти свойства всегда определены в документе: даже если в исходном документе
отсутствуют элементы <head>
и <body>
, браузер создаст их неявно. Свойство
document.Element
объекта Document
ссылается на корневой элемент документа. В
HTML-документах он всегда представляет элемент <html>
.
Методы getElementsByName()
и getElementsByTagName()
возвращают объекты
NodeList
, а такие свойства, как document.images
и document.forms
, являются
объектами HTMLCollection
.
Эти объекты являются объектами, подобными массивам, доступным
только для чтения (раздел 7.11). Они имеют свойство length и могут
индексироваться (только для чтения) подобно настоящим массивам. Содержимое
объекта NodeList
или HTMLCollection
можно обойти с помощью стандартного
цикла, например:
for(var i = 0; i < document.images.length
; i++)// Обойти все изображения
document.images[i].style.display = "none"; // ...и скрыть их.
Для объектов NodeList
и HTMLCollection
нельзя непосредственно вызывать
методы класса Array
, но их можно вызывать косвенно:
var content = Array.prototype.map.call(document.getElementsByTagName("p"), function(e) {return e.innerHTML;});Объекты
HTMLCollection
могут иметь дополнительные именованные
свойства и могут индексироваться не только числами, но и строками.
По историческим причинам оба объекта, NodeList
и HTMLCollection
, могут
также играть роль функций: вызов их с числовым или строковым
аргументом равносилен операции индексирования числом или строкой. Однако
использование этой причудливой особенности может сбивать с толку.
Интерфейсы обоих объектов, NodeList
и HTMLCollection
, проектировались
под другие языки программирования, не такие динамические, как
JavaScript. Оба определяют метод item()
. Он принимает целое число и
возвращает элемент с этим индексом. Однако в программах на языке JavaScript
нет нужды использовать этот метод, так как можно использовать простую
операцию индексирования массива. Аналогично HTMLCollection
определяет
метод namedItem()
, возвращающий значение именованного свойства, но
в программах на языке JavaScript вместо него можно использовать
операции индексирования массива и обращения к свойствам.
Одна из наиболее важных и интересных особенностей объектов NodeList
и HTMLCollection
состоит в том, что они не являются статическими слепками
документа, а продолжают «жить», и списки элементов, которые они
представляют, изменяются по мере изменения документа. Если вызвать метод
getElementsByTagName('div')
для документа, в котором отсутствуют элементы
<div>
, он вернет объект NodeList
, свойство length
которого будет равно 0
.
Если затем вставить в документ новый элемент <div>
, этот элемент
автоматически станет членом коллекции NodeList
, а ее свойство length
станет равно 1
.
Обычно такая динамичность элементов NodeList
и HTMLCollection
бывает
весьма полезна. Однако если добавлять или удалять элементы из
документа в процессе итераций по коллекции NodeList
, потребуется
предварительно создать статическую копию объекта NodeList
:
var snapshot = Array.prototype.slice.call(nodelist, 0);
Значением HTML-атрибута class
является список из нуля или более
идентификаторов, разделенных пробелами. Он дает возможность определять множества
связанных элементов документа: любые элементы, имеющие в атрибуте class
один
и тот же идентификатор, являются частью одного множества. Слово class
зарезервировано в языке JavaScript, поэтому для хранения значения HTML-атрибута
class
в клиентском JavaScript используется свойство className
. Обычно атрибут
class
используется вместе с каскадными таблицами стилей CSS с целью
применить общий стиль отображения ко всем членам множества, и мы еще будем
рассматривать эту тему в главе 16. Однако кроме этого стандарт HTML5 определяет
метод getElementsByClassName()
, позволяющий выбирать множества элементов
документа на основе идентификаторов в их атрибутах class
.
Подобно методу getElementsByTagName()
, метод getElementsByClassName()
может
вызываться и для HTML-документов, и для HTML-элементов, и возвращает
«живой» объект NodeList
, содержащий все потомки документа или элемента,
соответствующие критерию поиска. Метод getElementsByClassName()
принимает
единственный строковый аргумент, но в самой строке может быть указано несколько
идентификаторов, разделенных пробелами. Соответствующими будут считаться
элементы, атрибуты class
которых содержат все указанные идентификаторы.
Порядок следования идентификаторов не имеет значения. Обратите внимание,
что и в атрибуте class
, и в аргументе метода getElementsByClassName()
идентификаторы классов разделяются пробелами, а не запятыми. Ниже приводится
несколько примеров использования метода getElementsByClassName()
:
Современные браузеры отображают HTML-документы в «режиме
совместимости» или в «стандартном режиме» в зависимости от строгости объявления
<!DOCTYPE>
в начале документа. Режим совместимости поддерживается для сохранения
обратной совместимости, и одна из его особенностей состоит в том, что
идентификаторы классов в атрибуте class
и каскадных таблицах стилей CSS
нечувствительны к регистру символов. Метод getElementsByClassName()
следует алгоритму
сопоставления, используемому таблицами стилей. Если документ отображается
в режиме совместимости, метод сравнивает строки без учета регистра символов.
В противном случае сравнение выполняется с учетом регистра символов.
К моменту написания этих строк метод getElementsByClassName()
был реализован
во всех текущих браузерах, за исключением IE8 и более ранних версий. Однако
IE8 поддерживает описываемый в следующем разделе метод querySelectorAll()
,
на основе которого можно реализовать метод getElementsByClassName()
.
Каскадные таблицы стилей CSS имеют очень мощные синтаксические
конструкции, известные как селекторы, позволяющие описывать элементы или
множества элементов документа. Полное описание синтаксиса селекторов CSS выходит
далеко за рамки этой книги¹, однако несколько примеров помогут прояснить их
основы. Элементы можно описать с помощью имени тега и атрибутов id
и class
:
#nav // Элемент с атрибутом id="nav" div // Любой элемент <div> .warning // Любой элемент с идентификатором "warning" в атрибуте class
В более общем случае элементы можно выбирать, опираясь на значения атрибутов:
p[lang="fr"] //: Абзац с текстом на французском языке: <p lang="fr"> *[name="x"] // Любой элемент с атрибутом name="x"
Эти простейшие селекторы можно комбинировать:
span.fatal.error // Любой элемент <span> с классами "fatal" и "error" span[lang="fr"].warning // Любое предупреждение на французском языке
С помощью селекторов можно также определять взаимоотношения между элементами:
#log span // Любой элемент <span>, являющийся потомком элемента с id="log" #log>span // Любой элемент <span>, дочерний относительно элемента с id="log" body>h1:first-child // Первый элемент <h1>, дочерний относительно <body>
Селекторы можно комбинировать для выбора нескольких элементов или множеств элементов:
div, #log// Все элементы <div> плюс элемент с id="log"
Как видите, селекторы CSS позволяют выбирать элементы всеми способами,
описанными выше: по значению атрибута id
и name
, по имени тега и по имени класса.
Наряду со стандартизацией селекторов CSS3 другой стандарт консорциума W3C,
известный как «Selectors API» (API селекторов), определяет методы JavaScript
для получения элементов, соответствующих указанному селектору.
Ключевым в этом API является метод querySelectorAll()
объекта Document
. Он принимает
единственный строковый аргумент с селектором CSS и возвращает объект NodeList
,
представляющий все элементы документа, соответствующие селектору. В
отличие от ранее описанных методов выбора элементов объект NodeList
,
возвращаемый методом querySelectorAll()
, не является «живым»: он хранит элементы,
которые соответствовали селектору на момент вызова метода, и не отражает
последующие изменения в документе. В случае отсутствия элементов,
соответствующих селектору, метод querySelectorAll()
вернет пустой NodeList
. Если методу
querySelectorAll()
передать недопустимую строку, он возбудит исключение.
В дополнение к методу querySelectorAll()
объект документа также определяет
метод querySelector()
, подобный методу querySelectorAll()
, – с тем отличием, что он
возвращает только первый (в порядке следования в документе) соответствующий
элемент или null
, в случае отсутствия соответствующих элементов.
Эти два метода также определяются классом Elements
(и классом DocumentFragment
,
о котором рассказывается в разделе
15.6.4). Когда они вызываются относительно
элемента, поиск соответствия заданному селектору выполняется во всем
документе, а затем результат фильтруется так, чтобы в нем остались только потомки
использованного элемента. Такой подход может показаться противоречащим
здравому смыслу, так как он означает, что строка селектора может включать предков
элемента, для которого выполняется сопоставление.
Обратите внимание, что стандарт CSS определяет псевдоэлементы :first-line
и :first-letter
. В CSS им соответствуют не фактические элементы, а части
текстовых узлов. Они не будут обнаруживать совпадений, если использовать их вместе
с методом querySelectorAll()
или querySelector()
. Кроме того, многие браузеры не
возвращают результатов сопоставления с псевдоклассами :link
и :visited
, потому
что в противном случае это позволило бы получать информацию об истории
посещений страниц пользователем.
Методы querySelector()
и querySelectorAll()
поддерживают все текущие браузеры.
Тем не менее, обратите внимание, что спецификации этих методов не требуют
поддержки селекторов CSS3: производителям браузеров предлагается реализовать
поддержку того же набора селекторов, который поддерживается в каскадных
таблицах стилей. Текущие браузеры, кроме IE, поддерживают селекторы CSS3.
IE7 и 8 поддерживают селекторы CSS2. (Ожидается, что IE9 будет поддерживать
CSS3.)
Метод querySelectorAll()
является идеальным инструментом выбора элементов:
это очень мощный механизм, с помощью которого клиентские программы на
языке JavaScript могут выбирать элементы документа для выполнения операций
над ними. К счастью, селекторы CSS можно использовать даже в браузерах, не
имеющих собственной поддержки метода querySelectorAll()
. Похожий механизм
запросов на основе селекторов в библиотеке jQuery (глава 19) является
центральной парадигмой программирования. Веб-приложения на основе jQuery могут
использовать переносимый, совместимый с разными типами браузеров эквивалент
метода querySelectorAll()
, который называется $()
.
Программный код, выполняющий в библиотеке jQuery сопоставление с селекторами CSS, был реструктурирован и вынесен в самостоятельную библиотеку с именем Sizzle, которая была заимствована фреймворком Dojo и другими клиентскими библиотеками¹. Преимущество использования библиотек, таких как Sizzle (или библиотек, использующих Sizzle), в том, что выбор элементов можно производить даже в старых браузерах, и при этом обеспечивается поддержка базового набора селекторов, которые гарантированно будут работать во всех браузерах.
До того, как модель DOM была стандартизована, в IE4 была реализована
коллекция document.all[]
, представляющая все элементы (кроме текстовых узлов Text
)
в документе. Впоследствии коллекцию document.all[]
заменили стандартные
методы, такие как getElementByld()
и getElementsByTagName()
, и теперь она считается
устаревшей и не должна использоваться. Однако в свое время появление этой
коллекции произвело целую революцию, и даже сейчас все еще можно встретить
сценарии, использующие ее следующими способами:
document.all[0] // Первый элемент документа document.all["navbar"] // Элемент (или элементы) со значением "navbar" // в атрибуте id или name document.all.navbar // To же самое document.all.tags("div") // Все элементы <div> в документе document.all.tags("p")[0] // Первый элемент <p> в документе
После выбора элемента документа иногда бывает необходимо отыскать
структурно связанные части документа (родитель, братья, дочерний элемент). Объект
Document
можно представить как дерево объектов Node
,
как изображено на рис. 15.1.
Тип Node
определяет свойства, позволяющие перемещаться по такому дереву,
которые будут рассматриваться в разделе 15.3.1.
Существует еще один прикладной
интерфейс навигации по документу как дереву объектов Element
. Этот более
новый (и часто более простой в использовании) прикладной интерфейс
рассматривается в разделе 15.3.2.
Объект Document
, его объекты Element
и объекты Text
, представляющие текстовые
фрагменты в документе, – все они являются объектами
Node
. Класс Node
определяет следующие важные свойства:
parentNode
null
для узлов, не имеющих родителя,
таких как Document
. childNodes
NodeList
), обеспечивающий
«живое» представление дочерних узлов. firstChild, lastChild
null
, если данный узел не имеет
дочерних узлов. nextSibling, previousSibling
nodeType
Document
имеют значение
9
в этом свойстве. Узлы
типа Element
– значение 1
. Текстовые узлы типа
Text
– значение 3
. Узлы типа
Comments – значение 8
, и узлы типа
DocumentFragment
– значение 11
. nodeValue
Text
и Comment
. nodeName
Element
, в котором все символы преобразованы в верхний
регистр. С помощью этих свойств класса Node
можно следующими
способами сослаться на второй дочерний узел
первого дочернего узла объекта Document
:
document.childNodes[0].childNodes[1]
document.firstChild.firstChild.nextSibling
Допустим, что рассматриваемый документ имеет следующий вид:
<html><head><title>Test</title></head><body>Hello World!</body></html>
Тогда вторым дочерним узлом первого дочернего узла будет элемент
<body>
. В свойстве nodeType
он содержит значение
1
и в свойстве nodeName
– значение «BODY».
Однако, обратите внимание, что этот прикладной интерфейс чрезвычайно
чувствителен к изменениям в тексте документа. Например, если в этот документ
добавить единственный перевод строки между тегами <html>
и
<head>
, этот символ перевода строки станет первым дочерним
узлом (текстовым узлом Text
) первого дочернего узла, а вторым дочерним
узлом станет элемент <head>
, а не </body>
.
Когда основной интерес представляют сами элементы документа, а не текст в них
(и пробельные символы между ними), гораздо удобнее использовать прикладной
интерфейс, позволяющий интерпретировать документ как дерево объектов
Element
, игнорируя узлы Text
и Comment
,
которые также являются частью документа.
Первой частью этого прикладного интерфейса является свойство children
объектов Element
. Подобно свойству childNodes
его
значением является объект NodeList
.
Однако в отличие от свойства childNodes
список children
содержит только
объекты Element
. Свойство children
–
нестандартное свойство, но оно реализовано во
всех текущих браузерах. В IE это свойство было реализовано уже очень давно,
и большинство других браузеров последовали его примеру. Последним основным
браузером, реализовавшим его, стал Firefox 3.5.
Обратите внимание, что узлы Text
и Comment
не имеют дочерних узлов. Это означает,
что описанное выше свойство Node
. parentNode
никогда не возвращает узлы типа Text
или Comment
. Значением свойства parentNode
любого объекта Element
всегда будет
другой объект Element
или корень дерева – объект
Document
или DocumentFragment
.
Второй частью прикладного интерфейса навигации по элементам документа
являются свойства объекта Element
, аналогичные свойствам доступа к дочерним
и братским узлам объекта Node
:
firstElementChild, lastElementChild
firstChild
и lastChild
, но возвращают дочерние элементы. nextElementSibling, previousElementSibling
nextSibling
и previousSibling
, но возвращают братские
элементы. childElementCount
children.length
. Эти свойства доступа к дочерним и братским элементам стандартизованы и реализованы во всех текущих браузерах, кроме IE¹.
Поскольку прикладной интерфейс навигации по элементам документа реализован не во всех браузерах, вам может потребоваться определить переносимые функции навигации, как в примере 15.2.
Пример 15.2. Переносимые функции навигации по документу
/** * Возвращает ссылку на n-го предка элемента e или null, если нет * такого предка или если этот предок не является элементом Element * (например, Document или DocumentFragment). * Если в аргументе n передать 0, функция вернет сам элемент e. * Если в аргументе n передать 1 (или вообще опустить этот аргумент), * функция вернет родительский элемент. Если в аргументе n * передать 2, функция вернет родителя родительского элемента и т. д. */ function parent(e, n) { if (n === undefined) n = 1; while(n-- && e) e = e.parentNode; if (!e || e.nodeType !== 1) return null; return e; } /** * Возвращает n-й братский элемент элемента e. * Если в аргументе n передать положительное число, функция вернет * следующий n-й братский элемент. * Если в аргументе n передать отрицательное число, функция вернет * предыдущий n-й братский элемент. * Если в аргументе n передать ноль, функция вернет сам элемент e. */ function sibling(e,n) { while(e && n !== 0) { // Если e не определен, просто вернуть его if (n > 0) { // Отыскать следующий братский элемент if (e.nextElementSibling) e = e.nextElementSibling; else { for(e=e.nextSibling; e && e.nodeType !== 1; e=e.nextSibling) /* пустой цикл */ ; } n--; } else { // Отыскать предыдущий братский элемент if (e.previousElementSibing) e = e.previousElementSibling; else { for(e=e.previousSibling; e&&e.nodeType!==1; e=e.previousSibling) /* пустой цикл */ ; } n++; } } return e; } /** * Возвращает n-й дочерний элемент элемента е или null, * если нет такого дочернего элемента. * Если в аргументе n передать отрицательное число, поиск дочернего * элемента будет выполняться с конца. 0 соответствует первому * дочернему элементу, но -1 - последнему, -2 - второму с конца и т. д. */ function child(e, n) { if (e.children) {// Если массив children существует if (n < 0) n += e.children.length;// Преобразовать отрицательное число // в индекс массива if (n < 0) return null;// Если получилось отрицательное число, // значит, нет такого дочернего элемента return e.children[n]; // Вернуть заданный дочерний элемент } // Если элемент e не имеет массива children, начать поиск с первого // дочернего элемента, двигаясь вперед, или начать поиск с последнего // дочернего элемента, двигаясь назад. if (n >= 0) { // n - положительное: двигаться вперед, начиная с первого // Найти первый дочерний элемент элемента e if (e.firstElementChild) e = e.firstElementChild; else { for(e = e.firstChild; e && e.nodeType !== 1; e = e.nextSibling) /* пустой цикл */; } return sibling(e, n); // Вернуть n-го брата первого дочернего элемента } else { // n-отрицательное: двигаться назад, начиная с последнего if (e.lastElementChild) e = e.lastElementChild; else { for(e = e.lastChild; e && e.nodeType !== 1; e=e.previousSibling) /* пустой цикл */; } return sibling(e, n+1); // +1, чтобы преобразовать номер -1 дочернего // в номер 0 братского для последнего } }
Все текущие браузеры (включая IE8 и выше) реализуют модель DOM таким
образом, что типы, подобные типам Element
и HTMLDocument
, являются классами,
такими же как классы String
и Array
.
Element
, HTMLDocument
и Text
, но не поддерживает ее для объектов Node
, Document
, HTMLElement
и всех подтипов
типа HTMLElement
) Они не имеют конструкторов (как
создавать новые объекты Element
, будет показано далее в этой главе), но они
имеют объекты-прототипы, которые вы можете расширять своими методами:
Element.prototype.next = function() { if (this.nextElementSibling) return this.nextElementSibling; var sib = this.nextSibling; while(sib && sib.nodeType !== 1) sib = sib.nextSibling; return sib; }
Функции, представленные в примере 15.2, не были реализованы в виде
методов объекта Element
лишь по той причине, что такая возможность не
поддерживается в IE7.
Однако возможность расширения типов DOM может пригодиться для
реализации особенностей, характерных для IE, в других браузерах. Как
отмечалось выше, нестандартное свойство children
объекта Element
было
впервые реализовано в IE и только потом – в других браузерах. Используя
следующий программный код, можно реализовать это свойство в браузерах,
не поддерживающих его, таких как Firefox 3.0:
// Реализация свойства Element.children в браузерах,
// не поддерживающих его. Обратите внимание, что этот метод
// возвращает статический массив, а не "живой" NodeList
if (!document.documentElement.children) {
Element.prototype.__defineGetter__("children", function() {
var kids = [];
for(var c = this.firstChild; c != null; c = c.nextSibling)
if (c.nodeType === 1) kids.push(c);
return kids;
});
}
Метод __defineGetter__
(упоминавшийся в разделе
6.7.1)
не относится к стандартным, но его вполне можно использовать для обеспечения
переносимости в таком программном коде, как этот.
HTML-элементы состоят из имени тега и множества пар имя/значение,
известных как атрибуты. Например, элемент <a>
, определяющий гиперссылку, в
качестве адреса назначения ссылки использует значение атрибута href
. Значения
атрибутов HTML-элементов доступны в виде свойств объектов HTMLElement
,
представляющих эти элементы. Кроме того, модель DOM определяет и другие механизмы
получения и изменения значений XML-атрибутов и нестандартных
HTML-атрибутов. Подробнее об этом рассказывается в следующих подразделах.
Объекты HTMLElement
, представляющие элементы HTML-документа, определяют
свойства, доступные для чтения/записи, соответствующие HTML-атрибутам
элементов. Объект HTMLElement
определяет свойства для поддержки универсальных
HTTP-атрибутов, таких как id
, title
, lang
и dir
, и даже свойства-обработчики
событий, такие как onclick
. Специализированные подклассы класса Element
определяют атрибуты, характерные для представляемых ими элементов. Например,
узнать URL-адрес изображения можно, обратившись к свойству src
объекта
HTMLElement
, представляющего элемент <img>
:
var image = document.getElementById("myimage"); var imgurl = image.src; // Атрибут src определяет URL-адрес изображения image.id === "myimage" // Потому что поиск элемента выполнялся по id
Аналогично можно устанавливать атрибуты элемента <form>
, определяющие
порядок отправки формы:
var f = document.forms[0]; // Первый элемент <form>
в документе
f.action = "http://www.example.com/submit.php"; // Установить URL отправки
f.method = "POST"; //Тип HTTP-запроса
Имена атрибутов в разметке HTML не чувствительны к регистру символов, в отличие от имен свойств в языке JavaScript. Чтобы преобразовать имя атрибута в имя свойства в языке JavaScript, его нужно записать символами в нижнем регистре. Однако, если имя атрибута состоит из более чем одного слова, первый символ каждого слова, кроме первого, записывается в верхнем регистре, например: def aultChecked и tablndex.
Имена некоторых HTML-атрибутов совпадают с зарезервированными словами
языка JavaScript. Имена свойств, соответствующих таким атрибутам,
начинаются с приставки «htmb. Например, HTML-атрибуту for
(элемента <label>
) в языке
JavaScript соответствует свойство с именем htmlFor
. Очень важный HTML-атрибут
class
, имя которого совпадает с зарезервированным (но не используемым) в
языке JavaScript словом «class», является исключением из этого правила: в
программном коде на языке JavaScript ему соответствует свойство className
. Мы еще
встретимся со свойством className
в главе 16.
Свойства, представляющие HTML-атрибуты, обычно имеют строковые значения.
Если атрибут имеет логическое или числовое значение (например, атрибуты
defаultChecked
и maxLength
элемента <input>
), значением соответствующего свойства
будет логическое или числовое значение, а не строка. Значениями атрибутов
обработчиков событий всегда являются объекты Function
(или null
). Спецификация
HTML5 определяет несколько атрибутов (таких как атрибут form элемента <input>
и родственных ему элементов), которые преобразуются в фактические объекты
Element
. Наконец, значением свойства style
любого HTML-элемента является
объект CSSStyleDeclaration, а не строка. Поближе с этим важным свойством мы
познакомимся в главе 16.
Обратите внимание, что основанный на свойствах прикладной интерфейс
получения доступа к значениям атрибутов не позволяет удалять атрибуты из
элементов. В частности, для этих целей нельзя использовать оператор delete
. Для этой
цели можно использовать прием, который описывается в следующем разделе.
Как описывалось выше, тип HTMLElement
и его подтипы определяют свойства,
соответствующие стандартным атрибутам HTML-элементов. Однако тип Element
определяет дополнительные методы getAttribute()
и setAttribute()
, которые
можно использовать для доступа к нестандартным HTML-атрибутам, а также
обращаться к атрибутам элементов XML-документа:
var image = document.images[0];
var width = parseInt(image.getAttribute("WIDTH"));
image.setAttribute("class", "thumbnail");
Приведенный пример демонстрирует два важных различия между этими
методами и описанным выше прикладным интерфейсом, основанным на свойствах.
Во-первых, эти дополнительные методы интерпретируют значения всех а
трибутов как строки. Метод getAttribute()
никогда
не вернет число, логическое значение или объект. Во-вторых, данные методы
принимают стандартные имена атрибутов, даже если эти имена совпадают с
зарезервированными словами языка JavaScript. Имена атрибутов
HTML-элементов нечувствительны к регистру символов.
Класс Element
также определяет два родственных метода
hasAttribute()
и removeAttribute()
.
Первый из них проверяет присутствие атрибута с указанным именем,
а второй удаляет атрибут. Эти методы особенно удобны при работе с логическими
атрибутами: для этих атрибутов (таких как атрибут disabled
HTML-форм) важно
их наличие или отсутствие в элементе, а не их значения.
Если вам приходится работать с XML-документами, содержащими атрибуты из
других пространств имен, вы можете использовать варианты этих четырех
методов, позволяющие указывать имя пространства имен: getAttributeNS()
,
setAttributeNS()
, hasAttributeNS()
и removeAttributeNS()
. Вместо единственного строкового
аргумента с именем атрибута эти методы принимают два аргумента. В первом
передается URI-идентификатор, определяющий пространство имен. Во втором
аргументе обычно передается неквалифицированное локальное имя атрибута из
этого пространства имен. Исключением является метод setAttributeNS()
,
которому во втором атрибуте необходимо передавать квалифицированное имя атрибута,
включающее идентификатор пространства имен. Более полная информация об
этих методах доступа к атрибутам из других пространств имен приводится в
четвертой части книги.
Нередко бывает желательно добавить в HTML-элементы дополнительные данные,
обычно когда предусматривается возможность выбора этих элементов в
JavaScript-сценариях и выполнения некоторых операций с ними. Иногда это можно
реализовать, добавив специальные идентификаторы в атрибут class
. В ряде случаев,
когда речь заходит о более сложных данных, программисты прибегают к
использованию нестандартных атрибутов. Как отмечалось выше, для чтения и изменения
значений нестандартных атрибутов можно использовать методы getAttribute()
и setAttribute()
. Платой за это будет несоответствие документа стандарту.
Стандарт HTML5 предоставляет решение этой проблемы. В документах,
соответствующих стандарту HTML5, все атрибуты, имена которых состоят только из
символов в нижнем регистре и начинаются с приставки «data-
», считаются
допустимыми. Эти «атрибуты с данными» не оказывают влияния на представление
элементов, в которых присутствуют, и обеспечивают стандартный способ
включения дополнительных данных без нарушения стандартов.
Кроме того, стандарт HTML5 определяет в объекте Element
свойство dataset
. Это
свойство ссылается на объект со свойствами, имена которых
соответствуют именам атрибутов data-
без приставки. То есть свойство dataset.x будет
хранить значение атрибута data-x
. Имена атрибутов с дефисами отображаются
в имена свойств с переменным регистром символов: атрибут data-jquery-test
превратится в свойство dataset.jqueryTest
.
Ниже приводится более конкретный пример. Допустим, что в документе имеется следующий фрагмент разметки:
<span class="sparkline" data-ymin="0" data-ymax="10">
1 1 1 2 2 3 4 5 5 4 3 5 6 7 7 4 2 1
</span>
Sparkline – это маленькое изображение, обычно некоторый график, предназначенное для отображения в потоке текста. Чтобы сгенерировать такое изображение, необходимо извлечь значение атрибута с данными, как показано ниже:
// Предполагается, что в браузере поддерживается метод Array.map(),
// определяемый стандартом ES5 (или реализована его имитация)
var sparklines = document.getElementsByClassName("sparkline");
for(var i = 0; i < sparklines.length; i++) {
var dataset = sparklines[i].dataset;
var ymin = parseFloat(dataset.ymin);
var ymax = parseFloat(dataset.ymax);
var data = sparklines[i].textContent.split(" ").map(parseFloat);
drawSparkline(sparklines[i], ymin, ymax, data); // Еще не реализована
}
На момент написания этих строк свойство dataset
еще не было реализовано в
текущих браузерах, поэтому представленное выше решение можно было бы
реализовать так:
var sparklines = document.getElementsByClassName("sparkline");
for(var i = 0; i < sparklines.length; i++) {
var elt = sparklines[i];
var ymin = parseFloat(elt.getAttribute("data-ymin"));
var ymin = parseFloat(elt.getAttribute("data-ymax"));
var points = elt.getAttribute("data-points");
var data = elt.textContent.split(" ").map(parseFloat);
drawSparkline(elt, ymin, ymax, data); // Еще не реализована
}
Обратите внимание, что свойство dataset
является (или будет, когда будет
реализовано) «живым», двунаправленным интерфейсом к атрибутам data-
элемента.
Изменение или удаление свойства объекта dataset
приводит к изменению или
удалению соответствующего атрибута data-
элемента.
Функция drawSparkline()
в приведенном примере является
вымышленной, однако в примере 21.13
демонстрируется прием вставки внутристрочных диаграмм
(sparklines) подобно тому, как это делается нашем примере, а прорисовка осуществляется
с использованием элемента <canvas>
.
Существует несколько способов работы с атрибутами элементов. Тип Node
определяет свойство attributes
. Это свойство имеет значение null
для всех узлов, не
являющихся объектами Element
. Свойство attributes
объектов Element
является
объектом, подобным массиву, представляющим все атрибуты элемента и доступным только для чтения.
Подобно спискам NodeList
, объект attributes
не является
статической копией. Он может индексироваться числами, что означает возможность
перечисления всех атрибутов элемента, а также именами атрибутов:
Значениями, получаемыми в результате индексирования объекта attributes
,
являются объекты Attr
. Объекты Attr
– это специализированный подтип Node
, но
в действительности никогда не используемые в таком качестве. Свойства name
и value
объектов Attr
возвращают имя и значение атрибута.
Взгляните еще раз на рис. 15.1 и попробуйте ответить на вопрос: какой объект
представляет «содержимое» элемента <p>
. На этот вопрос можно дать три ответа:
<i>
simple</i>
document».
Text
, узел типа Element
, включающий
дочерний узел Text
, и еще один узел типа Text
.
Все три ответа являются верными, и каждый ответ ценен по-своему. В следующих разделах описывается, как работать с представлением в виде разметки HTML, с представлением в виде простого текста и с представлением в виде дерева объектов.
При чтении свойства innerHTML
объекта Element
возвращается содержимое этого
элемента в виде строки разметки. Попытка изменить значение этого свойства
приводит к вызову синтаксического анализатора веб-браузера и замещению
текущего содержимого элемента разобранным представлением новой строки.
(Несмотря на свое название, свойство innerHTML
может использоваться для работы не
только с HTML-, но и с XML-элементами.)
Веб-браузеры прекрасно справляются с синтаксическим анализом разметки
HTML, поэтому операция изменения значения свойства innerHTML
обычно
достаточно эффективна, несмотря на необходимость синтаксического анализа.
Обратите внимание, однако, что многократное добавление фрагментов текста в
свойство innerHTML
с помощью оператора +=
обычно не эффективно,
потому что требует выполнения двух шагов – сериализации и синтаксического
анализа.
Впервые свойство innerHTML
было реализовано в IE4. Несмотря на то, что оно
достаточно давно поддерживается всеми браузерами, это свойство было
стандартизовано только с появлением стандарта HTML5. Спецификация HTML5 требует,
чтобы свойство innerHTML
было реализовано не только в объекте Element
, но и в
объекте Document
, однако этому требованию отвечают пока не все браузеры.
Кроме того, спецификация HTML5 стандартизует свойство с именем outerHTML
.
При обращении к свойству outerHTML
оно возвращает строку разметки HTML или
XML, содержащую открывающий и закрывающий теги элемента, которому
принадлежит это свойство. При записи нового значения в свойство outerHTML
элемента
новое содержимое замещает элемент целиком. Свойство outerHTML
определено
только для узлов типа Element
, оно отсутствует в объекте Document
. К моменту
написания этих строк свойство outerHTML
поддерживалось всеми текущими браузерами,
кроме Firefox (далее в этой главе в примере 15.5
приводится реализация свойства outerHTML
на основе свойства innerHTML
).
Еще одной особенностью, впервые появившейся в IE и стандартизованной
спецификацией HTML5, является метод insertAdjacentHTML()
, дающий возможность
вставить строку с произвольной разметкой HTML, «примыкающую» ("adjacent")
к указанному элементу. Разметка передается методу во втором аргументе, а
точное значение слова «примыкающая» ("adjacent") зависит от значения первого
аргумента. Этот первый аргумент должен быть строкой с одним из значений:
«beforebegin», «afterbegin», «beforeend» или «afterend». Перечисленные значения
определяют позицию вставки так, как изображено на рис. 15.3.
Рис. 15.3. Позиции вставки в вызове метода insertAdjacentHTML()
Метод insertAdjacentHTML()
не поддерживается текущей версией Firefox. Далее
в этой главе будет представлен пример 15.6, демонстрирующий, как можно
реализовать метод insertAdjacentHTML()
с применением свойства innerHTML
и как
можно написать методы вставки разметки HTML, не требующие указывать позицию
вставки с помощью строкового аргумента.
Иногда бывает необходимо получить содержимое элемента в виде простого текста
или вставить простой текст в документ (без необходимости экранировать угловые
скобки и амперсанды, используемые в разметке HTML). Стандартный способ
выполнения этих операций основан на использовании свойства textContent
объекта
Node
:
var para = document.getElementsByTagName("p")[0]; // Первый <p>
в документе
var text = para.textContent; // Текст "This is a simple document."
para.textContent = "Hello World!"; // Изменит содержимое абзаца
Свойство textContent
поддерживается всеми текущими браузерами, кроме IE.
В IE вместо него можно использовать свойство innerText
. Впервые свойство
innerText
было реализовано в IE4 и поддерживается всеми текущими браузерами,
кроме Firefox.
Свойства textContent
и innerText
настолько похожи, что обычно могут быть
взаимозаменяемы при использовании. Однако будьте внимательны и отличайте
пустые элементы (строка "" в языке JavaScript интерпретируется как ложное
значение) от неопределенных свойств:
/** * При вызове с одним аргументом возвращает значение свойства textContent * или innerText элемента. При вызове с двумя аргументами записывает * указанное значение в свойство textContent или innerText элемента. */ function textContent(element, value) { var content = element.textContent; // Свойство textContent определено? if (value === undefined) { // Если аргумент value не указан, if (content !== undefined) return content; // вернуть текущий текст else return element.innerText; } else { // Иначе записать текст if (content !== undefined) element.textContent = value; else element.innerText = value; } }
Свойство textContent
возвращает результат простой конкатенации всех узлов
Text
, являющихся потомками указанного элемента. Свойство innerText
не обладает четко
определенным поведением и имеет несколько отличий от свойства textContent
. innerText
не возвращает содержимое элементов <script>
. Из возвращаемого им текста
удаляются лишние пробелы и предпринимается попытка сохранить табличное
форматирование. Кроме того, для некоторых элементов таблиц, таких как <table>
,
<tbody>
и <tr>
, свойство innerText
доступно только для чтения.
Встроенные элементы <script>
(т. е. без атрибута src) имеют свойство text
,
которое можно использовать для получения их содержимого в виде
текста. Содержимое элементов <script>
никогда не отображается в браузерах,
а HTML-парсеры игнорируют угловые скобки и амперсанды внутри
сценариев. Это делает элемент <script>
идеальным средством встраивания
произвольных текстовых данных, доступных для использования
веб-приложением. Достаточно просто определить в атрибуте type
элемента
какое-либо значение (такое как «text/x-custom-data»), чтобы сообщить, что этот
сценарий не содержит выполняемый программный код на языке JavaScript.
В этом случае интерпретатор JavaScript будет игнорировать сценарий, но
сам элемент будет включен в дерево документа, а содержащиеся в нем
данные можно будет получить с помощью свойства text
.
Еще одним средством доступа к содержимому элемента является список
дочерних узлов, каждый из которых может иметь свое множество дочерних узлов.
Когда речь заходит о содержимом элемента, наибольший интерес обычно
представляют текстовые узлы. При работе с XML-документами необходимо также быть
готовыми встретить узлы CDATASection – подтип класса Text
– представляющие
содержимое разделов CDATA.
Пример 15.3 демонстрирует функцию textContent()
, которая выполняет
рекурсивный обход дочерних элементов и объединяет текст, содержащийся во всех
текстовых узлах-потомках. Чтобы было более понятно, напомню, что свойство
nodeValue
(определяемое типом Node
) хранит содержимое текстового узла.
Пример 15.3. Поиск всех текстовых узлов, потомков указанного элемента
// Возвращает простое текстовое содержимое элемента e, // выполняя рекурсивный обход всех дочерних элементов. // Этот метод действует подобно свойству textContent function textContent(e) { var child, type, s = ""; // s хранит текст всех дочерних узлов for(child = e.firstChild; child != null; child = child.nextSibling) { type = child.nodeType; if (type === 3 || type === 4) // Узлы типов Text и CDATASection s += child.nodeValue; else if (type === 1) // Рекурсивный обход узлов типа Element s += textContent(child); } return s; }
Свойство nodeValue
доступно для чтения и записи, и с его помощью можно
изменять содержимое в отображаемых узлах Text
и CDATASection
. Оба типа, Text
и
CDATASection
, являются подтипами класса CharacterData
, описание которого
приводится в четвертой части книги. Класс CharacterData
определяет свойство data
, которое
хранит тот же текст, что и свойство nodeValue
. Следующая функция преобразует
символы текстового содержимого узлов типа Text
в верхний регистр,
устанавливая значение свойства data
:
// Рекурсивно преобразует символы всех текстовых узлов-потомков // элемента n в верхний регистр, function upcase(n) { // Если n - объект Text или CDATA: if (n.nodeType == 3 || n.nodeType == 4) n.data = n.data.toUpperCase(); // преобразовать в верхний регистр else // Иначе рекурсия по дочерним узлам for(var i = 0; i < n.childNodes.length; i++) upcase(n.childNodes[i]); }
Класс CharacterData
также определяет редко используемые методы добавления
в конец, удаления, вставки и замены текста в узлах Text
или CDATASection
. Кроме
изменения содержимого имеющихся текстовых узлов этот класс позволяет
также вставлять в элементы Element
новые текстовые узлы или замещать
существующие текстовые узлы новыми. Создание, вставка и удаление узлов – тема
следующего раздела.
Мы уже знаем, как получать и изменять содержимое документа, используя
строки с разметкой HTML и с простым текстом. Мы также знаем, как выполнять
обход документа для исследования отдельных узлов Element
и Text
, составляющих
его содержимое. Однако точно так же существует возможность изменения
документа на уровне отдельных узлов. Тип Document
определяет методы создания
объектов Element
и Text
, а тип Node
определяет методы для вставки, удаления и
замены узлов в дереве. Приемы создания и вставки узлов уже были показаны в
примере 13.4, который повторяется ниже:
// Асинхронная загрузка сценария из указанного URL-адреса и его выполнение function loadasync(url) { var head = document.getElementsByTagName("head")[0]; // Отыскать <head> var s = document.createElement("script"); // Создать элемент <script> s.src = url; // Установить его атрибут src head.appendChild(s); // Вставить <script> в <head> }
В следующих подразделах более подробно и с примерами рассказывается о
создании новых узлов, о вставке и удалении узлов, а также об использовании объектов
DocumentFragment
, упрощающих работу с множеством узлов.
Как показано в приведенном примере, создавать новые узлы Element
можно с
помощью метода createElement()
объекта Document
. Этому методу необходимо передать
имя тега: это имя не чувствительно к регистру символов при работе с
HTML-документами и чувствительно при работе с XML-документами.
Для создания текстовых узлов существует аналогичный метод:
var newnode = document.createTextNode("содержимое текстового узла");
Кроме того, объект Document
определяет и другие фабричные методы, такие как
редко используемый метод createComment()
. Один такой метод, createDocumentFragment()
,
мы будем использовать в разделе 15.6.4.
При работе с документами, в которых используются пространства имен XML, можно использовать метод
createElementNS()
, позволяющий указывать URI-идентификатор пространства имен и имя
тега создаваемого объекта Element
.
Еще один способ создания в документе новых узлов заключается в копировании
существующих узлов. Каждый узел имеет метод cloneNode()
, возвращающий
новую копию узла. Если передать ему аргумент со значением true
, он рекурсивно
создаст копии всех потомков, в противном случае будет создана лишь
поверхностная копия. В браузерах, отличных от IE, объект Document
дополнительно
определяет похожий метод с именем importNode()
. Если передать ему узел из другого
документа, он вернет копию, пригодную для вставки в текущий документ. Если
передать ему значение true
во втором аргументе, он рекурсивно импортирует все
узлы-потомки.
После создания нового узла его можно вставить в документ с помощью методов
типа Node
: appendChild()
или insertBefore()
.
Метод appendChild()
вызывается относительно узла Element
,
в который требуется вставить новый узел, и вставляет указанный узел так, что тот
становится последним дочерним узлом (значением свойства lastChild
).
Метод insertBefore()
похож на метод appendChild()
, но он принимает два
аргумента. В первом аргументе указывается вставляемый узел, а во втором – узел, перед
которым должен быть вставлен новый узел. Этот метод вызывается относительно
объекта узла, который станет родителем нового узла, а во втором аргументе
должен передаваться дочерний узел этого родителя. Если во втором аргументе
передать null
, метод insertBefore()
будет вести себя,
как appendChild()
, и вставит узел в конец.
Ниже приводится простая функция вставки узла в позицию с указанным
числовым индексом. Она демонстрирует применение обоих методов, appendChild()
и insertBefore()
:
// Вставляет узел child в узел parent так, // что он становится n-м дочерним узлом function insertAt(parent, child, n) { if (n < 0 || n > parent.childNodes.length) throw new Error("недопустимый индекс"); else if (n == parent.childNodes.length) parent.appendChild(child); else parent.insertBefore(child, parent.childNodes[n]); }
Если метод appendChild()
insertBefore()
используется для вставки узла,
который уже находится в составе документа, этот узел автоматически будет удален из
текущей позиции и вставлен в новую позицию; нет необходимости явно удалять
узел. Пример 15.4 демонстрирует функцию сортировки строк таблицы по
значениям ячеек в указанном столбце. Она не создает новые узлы и для изменения
порядка следования существующих узлов использует метод appendChild()
.
Пример 15.4. Сортировка строк таблицы
// Сортирует строки в первом элементе <tbody> указанной таблицы // по значениям n-й ячейки в каждой строке. Использует функцию сравнения, // если она указана. Иначе сортировка выполняется в алфавитном порядке. function sortrows(table, n, comparator) { var tbody = table.tBodies[0]; // Первый <tbody> возможно, созданный неявно var rows = tbody.getElementsByTagName("tr"); // Все строки в tbody rows = Array.prototype.slice.call(rows,0); // Скопировать в массив // Отсортировать строки по содержимому n-го элементаrows.sort(function(row1,row2) { var cell1 = row1.getElementsByTagName("td")[n]; // n-e ячейки var cell2 = row2.getElementsByTagName("td")[n]; // двух строк var val1 = cell1.textContent || cell1.innerText; // текстовое содерж. var val2 = cell2.textContent || cell2.innerText; // двух ячеек if (comparator) return comparator(val1, val2); // Сравнить! if (val1 < val2) return -1; else if (val1 > val2) return 1; else return 0; }); // Добавить строки в конец tbody в отсортированном порядке. При этом // строки автоматически будут удалены из их текущих позиций, благодаря // чему отпадает необходимость явно удалять их. Если <tbody> содержит // какие-либо другие узлы, отличные от элементов <tr>, эти узлы // "всплывут" наверх. for(var i = 0; i < rows.length; i++) tbody.appendChild(rows[i]); } // Отыскивает в таблице элементы <th> // (предполагается, что в таблице существует только одна строка с ними), // и добавляет в них возможность обработки щелчка мышью, чтобы щелчок // на заголовке столбца вызывал сортировку таблицы по этому столбцу. function makeSortable(table) { var headers = table.getElementsByTagName("th"); for(var i = 0; i < headers.length; i++) { (function(n) { // Чтобы создать локальную область видимости headers[i].onclick = function() { sortrows(table, n); }; }(i)); // Присвоить значение i локальной переменной n } } 15.6.3. Удаление и замена узлов
Метод
removeChild()
удаляет элемент из дерева документа. Но будьте внимательны: этот метод вызывается не относительно узла, который должен быть удален, а (как следует из фрагмента «child» в его имени) относительно родителя удаляемого узла. Этот метод вызывается относительно родителя и принимает в виде аргумента дочерний узел, который требуется удалить. Чтобы удалить узел n из документа, вызов метода должен осуществляться так:n.parentNode.removeChild(n);МетодreplaceChild()
удаляет один дочерний узел и замещает его другим. Этот метод должен вызываться относительно родительского узла. В первом аргументе он принимает новый узел, а во втором – замещаемый узел. Вот пример, как заменить узел n текстовой строкой:n.parentNode.replaceChild(document.createTextNode("[ REDACTED ]"), n);Еще один способ применения метода replaceChild():// Замещает узел n новым элементом <b> // и делает узел n его дочерним элементом, function embolden(n) { // Если вместо узла получена строка, интерпретировать ее // как значение атрибута id элемента if (typeof n == "string") n = document.getElementById(n); var parent = n.parentNode; // Ссылка на родителя элемента n var b = document.createElement("b"); // Создать элемент <b> parent.replaceChild(b, n); // Заменить n элементом <b> b.appendChild(n); // Сделать n дочерним элементом элемента <b> }В разделе 15.5.1 было представлено свойствоouterHTML
элементов и говорилось, что оно не реализовано в текущей версии Firefox. Пример 15.5 демонстрирует, как можно реализовать это свойство в Firefox (и в любом другом браузере, поддерживающем свойствоinnerHTML
, позволяющем расширять объект-прототипElement.prototype
и имеющем методы определения методов доступа к свойствам). Здесь также демонстрируется практическое применение методовremoveChild()
исloneNode()
.Пример 15.5. Реализация свойства
outerHTML
с помощью свойстваinnerHTML
// Реализация свойства outerHTML для браузеров, не поддерживающих его. // Предполагается, что браузер поддерживает свойство innerHTML, // возможность расширения Element.prototype // и позволяет определять методы доступа к свойствам. (function() { // Если свойство outerHTML уже имеется - просто вернуть управление if (document.createElement("div").outerHTML) return; // Возвращает окружающую разметку с содержимым элемента, // на который указывает ссылка this function outerHTMLGetter() { var container = document.createElement("div"); // Фиктивный элемент container.appendChild(this.cloneNode(true)); // Копировать this // в фиктивный элемент return container.innerHTML; // Вернуть содержимое // фиктивного элемента } // Замещает указанным значением value содержимое элемента, на который // указывает ссылка this, вместе с окружающей разметкой HTML function outerHTMLSetter(value) { // Создать фиктивный элемент и сохранить в нем указанное значение var container = document.createElement("div"); container.innerHTML = value; // Переместить все узлы из фиктивного элемента в документ while(container.firstChild) // Продолжать, пока в контейнере // не останется дочерних элементов this.parentNode.insertBefore(container.firstChild, this); // И удалить замещаемый узел this.parentNode.removeChild(this); } // Использовать эти две функции в качестве методов чтения и записи // свойства outerHTML всех объектов Element. // Использовать метод Object.defineProperty, если имеется, // в противном случае использовать __defineGetter__ и __defineSetter__. if (Object.defineProperty) { Object.defineProperty(Element.prototype, "outerHTML", { get: outerHTMLGetter, set: outerHTMLSetter, enumerable: false, configurable: true }); } else { Element.prototype.__defineGetter__("outerHTML", outerHTMLGetter); Element.prototype.__defineSetter__("outerHTML", outerHTMLSetter); } }());15.6.4. Использование объектов DocumentFragment
Объекты
DocumentFragment
– это особая разновидность объектовNode
; они служат временным контейнером для других узлов. Создаются объектыDocumentFragment
следующим образом:var frag = document.createDocumentFragment();Как и узел
Document
, объектыDocumentFragment
являются самостоятельными и не входят в состав какого-либо другого документа. Его свойствоparentNode
всегда возвращает значениеnull
. Однако, как и узлыElement
, объектыDocumentFragment
могут иметь любое количество дочерних элементов, которыми можно управлять с помощью методовappendChild()
,insertBefore()
и т. д.Одна из особенностей объекта
DocumentFragment
состоит в том, что он позволяет манипулировать множеством узлов как единственным узлом: если объектDocumentFragment
передать методуappendChild()
,insertBefore()
илиreplaceChild()
, в документ будут вставлены дочерние элементы фрагмента, а не сам фрагмент (дочерние элементы будут перемещены из фрагмента в документ, а сам фрагмент опустеет и будет готов для повторного использования). Следующая функция использует объектDocumentFragment
для перестановки в обратном порядке дочерних элементов узла:
// Выполняет перестановку дочерних элементов узла n в обратном порядке function reverse(n) { // Создать пустой объект DocumentFragment, который // будет играть роль временного контейнера var f = document.createDocumentFragment(); // Выполнить обход дочерних элементов в обратном порядке и переместить // каждый из них в объект фрагмента. Последний дочерний элемент узла n // станет первым дочерним элементом фрагмента f, и наоборот. // Обратите внимание, что при добавлении дочернего элемента в фрагмент f // он автоматически удаляется из узла n. while(n.lastChild) f.appendChild(n.lastChild); // В заключение переместить сразу все дочерние элементы // из фрагмента f обратно в узел n. n.appendChild(f); }В примере 15.6 представлена реализация метода
insertAdjacentHTML()
(раздел 15.5.1) с применением свойстваinnerHTML
и объектаDocumentFragment
. В нем также определяются функции вставки разметки HTML с более логичными именами, чем неочевидноеinsertAdjacentHTML()
. Вложенная вспомогательная функцияfragment()
является, пожалуй, наиболее интересной частью этого примера: она возвращает объектDocumentFragment
, содержащий разобранное представление указанной ей строки с разметкой HTML.Пример 15.6. Реализация метода
insertAdjacentHTML()
с использованием свойстваinnerHTML
// Этот модуль определяет метод Element.insertAdjacentHTML для браузеров, // не поддерживающих его, а также определяет переносимые функции // вставки HTML, имеющие более логичные имена, чем имя insertAdjacentHTML: // Insert.before(), Insert.after(), Insert.atStart(), Insert.atEnd() var Insert = (function() { // Если элементы имеют собственный метод insertAdjacentHTML, использовать // его в четырех функциях вставки HTML, имеющих более понятные имена if (document.createElement("div").insertAdjacentHTML) { return { before: function(e,h) {e.insertAdjacentHTML("beforebegin",h);}, after: function(e,h) {e.insertAdjacentHTML("afterend",h);}, atStart: function(e,h) {e.insertAdjacentHTML("afterbegin",h);}, atEnd: function(e,h) {e.insertAdjacentHTML("beforeend",h);} }; } // Иначе, в случае отсутствия стандартного метода insertAdjacentHTML, // реализовать те же самые четыре функции вставки и затем использовать их // в определении метода insertAdjacentHTML. // Сначала необходимо определить вспомогательный метод, который принимает // строку с разметкой HTML и возвращает объект DocumentFragment, // содержащий разобранное представление этой разметки. function fragment(html) { var elt = document.createElement("div"); // Пустой элемент var frag = document.createDocumentFragment(); // Пустой фрагмент elt.innerHTML = html; // Содержимое элемента while(elt.firstChild) // Переместить все узлы frag.appendChild(elt.firstChild); // из elt в frag return frag; // И вернуть frag } var Insert = { before: function(elt, html) { elt.parentNode.insertBefore(fragment(html), elt); }, after: function(elt, html) { elt.parentNode.insertBefore(fragment(html),elt.nextSibling); }, atStart: function(elt, html) { elt.insertBefore(fragment(html), elt.firstChild); }, atEnd: function(elt, html) { elt.appendChild(fragment(html)); } }; // Реализация метода insertAdjacentHTML на основе приведенных выше функций Element.prototype.insertAdjacentHTML = function(pos, html) { switch(pos.toLowerCase()) { case "beforebegin": return Insert.before(this, html); case "afterend": return Insert.after(this, html); case "afterbegin": return Insert.atStart(this, html); case "beforeend": return Insert.atEnd(this, html); } }; return Insert; // Вернуть четыре функции вставки }());15.7. Пример: создание оглавления
Пример 15.7 показывает, как динамически создавать оглавление документа. Он демонстрирует многие концепции работы с документом, описанные в разделах выше: выбор элементов, навигация по документу, установка атрибутов элементов, установка свойства
innerHTML
и создание новых узлов и вставку их в документ. Пример снабжен большим количеством комментариев, которые призваны облегчить понимание программного кода.Пример 15.7. Автоматическое создание оглавления документа
/** * TOC.js: создает оглавление документа. * * Этот модуль регистрирует анонимную функцию, которая вызывается * автоматически по окончании загрузки документа. Эта функция сначала * отыскивает элемент документа с атрибутом id = "TOC". Если такой элемент * отсутствует, функция создает его, помещая в начало документа. * * Затем функция отыскивает все элементы с <h1> no <h6>, интерпретируя их * как заголовки разделов, и создает оглавление внутри элемента TOC. Функция * добавляет номера разделов в каждый заголовок и обертывает заголовки * именованными якорными элементами, благодаря чему оглавление может * ссылаться на них. Якорным элементам даются имена, начинающиеся * с приставки "TOC", поэтому вам следует избегать использовать * эту приставку в своей разметке HTML. * * Оформление элементов оглавления можно реализовать с помощью CSS. Все * элементы имеют класс "TOCEntry". Кроме того, каждый элемент оглавления * имеет класс, соответствующий уровню заголовка раздела. Для заголовков, * оформленных тегом <h1>, создаются элементы оглавления с классом * "TOCLevel1",для заголовков <h2> - с классом "TOCLevel2" и т.д. * Номера разделов, вставляемые в заголовки, получают класс "TOCSectNum". * * Этот модуль можно использовать с каскадными таблицами стилей, такими как: * * #TOC { border: solid black 1px; margin: 10px; padding: 10px; } * .TOCEntry { font-family: sans-serif; } * .TOCEntry a { text-decoration: none; } * .TOCLevel1 { font-size: 16pt; font-weight: bold; } * .TOCLevel2 { font-size: 12pt; margin-left: .5in; } * .TOCSectNum:after { content: ": "; } * * Последнее определение генерирует двоеточие и пробел после номера раздела. * Чтобы скрыть номера разделов, можно использовать следующий стиль: * * .TOCSectNum { display: none } * * Этот модуль использует вспомогательную функцию onLoad(). **/ // Анонимная функция, определяющая локальную область видимости: onLoad(function() { // Отыскать контейнерный элемент для оглавления. // Если такой элемент отсутствует, создать его в начале документа, var toc = document.getElementById("TOC"); if (!toc) { toc = document.createElement("div"); toc.id = "TOC"; document.body.insertBefore(toc, document.body.firstChild); } // Отыскать все элементы заголовков разделов var headings; if (document.querySelectorAll) // Возможно есть более простой путь? headings = document.querySelectorAll("h1,h2,h3,h4,h5,h6"); else // Иначе отыскать заголовки более сложным способом headings = findHeadings(document.body, []); // Выполняет рекурсивный обход тела документа в поисках заголовков function findHeadings(root, sects) { for(var c = root.firstChild; c != null; c = c.nextSibling) { if (c.nodeType !== 1) continue; if (c.tagName.length == 2 && c.tagName.charAt(0) == "H") sects.push(c); else findHeadings(c, sects); } return sects; } // Инициализировать массив, хранящий номера разделов, var sectionNumbers = [0,0,0,0,0,0]; // Выполнить цикл по найденным элементам заголовков. for(var h = 0; h < headings.length; h++) { var heading = headings[h]; // Пропустить заголовки, находящиеся в контейнере оглавления, if (heading.parentNode == toc) continue; // Определить уровень заголовка. var level = parseInt(heading.tagName.charAt(1)); if (isNaN(level) || level < 1 || level > 6) continue; // Увеличить номер раздела для этого уровня и установить // номера разделов более низкого уровня равными нулю. sectionNumbers[level-1]++; for(var i = level; i < 6; i++) sectionNumbers[i] = 0; // Объединить номера разделов всех уровней, // чтобы получился номер вида 2.3.1. var sectionNumber = sectionNumbers.slice(0,level).join(".") // Добавить номер раздела в заголовок. Номер помещается // в элемент<span>
, чтобы его можно было стилизовать с помощью CSS. var span = document.createElement("span"); span.className = "TOCSectNum"; span.innerHTML = sectionNumber; heading.insertBefore(span, heading.firstChild); // Обернуть заголовок якорным элементом, чтобы можно было // сконструировать ссылку на него. var anchor = document.createElement("a"); anchor.name = "TOC"+sectionNumber; heading.parentNode.insertBefore(anchor, heading); anchor.appendChild(heading); // Создать ссылку на этот раздел. var link = document.createElement("a"); link.href = "#TOC" + sectionNumber; // Адрес назначения ссылки link.innerHTML = heading.innerHTML; // Текст ссылки совпадает // с текстом заголовка // Поместить ссылку в элемент div, чтобы обеспечить возможность // стилизации в зависимости от уровня, var entry = document.createElement("div"); entry.className = "TOCEntry TOCLevel" + level; entry.appendChild(link); // И добавить элемент div в контейнер оглавления. toc.appendChild(entry); } });15.8. Геометрия документа и элементов и прокрутка
До сих пор в этой главе мы рассматривали документы как некие абстрактные деревья элементов и текстовых узлов. Но когда браузер отображает документ в своем окне, он создает визуальное представление документа, в котором каждый элемент имеет определенную позицию и размеры. Часто веб-приложения могут интерпретировать документы как деревья элементов и никак не заботиться о том, как эти элементы отображаются на экране. Однако иногда бывает необходимо определить точные геометрические характеристики элемента. Например, в главе 16 мы увидим, что элемент можно поместить в определенную позицию с помощью CSS. Если вам потребуется использовать CSS для динамического позиционирования элемента (такого как всплывающая подсказка или сноска) рядом с элементом, который позиционируется браузером, вам необходимо иметь возможность определять положение этого элемента.
В этом разделе рассказывается, как можно переходить от абстрактной, древовидной модели документа к геометрическому, основанному на системе координат визуальному представлению документа в окне браузера, и обратно. Свойства и методы, описываемые здесь, реализованы в браузерах достаточно давно (хотя есть некоторые, которые до недавнего времени присутствовали только в IE, а некоторые не были реализованы в IE до появления версии IE9). К моменту написания этих строк они проходили процесс стандартизации консорциумом W3C в виде стандарта «CSSOM-View Module» (https://www.w3.org/TR/cssom-view-1/).
15.8.1. Координаты документа и видимой области
Позиция элемента измеряется в пикселах. Координата
X
растет слева направо, а координатаY
– сверху вниз. Однако существуют две точки, которые мы можем считать началом координат: координатыX
иY
элемента могут отсчитываться относительно верхнего левого угла документа или относительно верхнего левого угла видимой области. Для окон верхнего уровня и вкладок «видимой областью» является часть окна браузера, в которой фактически отображается содержимое документа: в нее не входит обрамление окна (меню, панели инструментов, вкладки). Для документов, отображаемых во фреймах, видимой областью является элемент<iframe>
, определяющий фрейм. В любом случае, когда речь заходит о позиции элемента, необходимо знать, какая система координат используется – относительно начала документа или относительно начала видимой области. (Координаты видимой области видимости иногда называют оконными координатами.)Если документ меньше видимой области или если он еще не прокручивался, верхний левый угол документа находится в верхнем левом углу видимой области, и начала систем координат документа и видимой области совпадают. Однако в общем случае, чтобы перейти от одной системы координат к другой, необходимо добавлять или вычитать смещения прокрутки. Например, если координата
Y
элемента имеет значение 200 пикселов в системе координат документа и пользователь прокрутил документ вниз на 75, то в системе координат видимой области координатаY
элемента будет иметь значение 125 пикселов. Аналогично, если координатаX
элемента имеет значение 400 в системе координат видимой области и пользователь прокрутил документ по горизонтали на 200 пикселов, то в системе координат документа координатаX
элемента будет иметь значение 600.Система координат документа является более фундаментальной, чем система координат видимой области, и на нее не оказывает влияния величина прокрутки. Однако в клиентских сценариях довольно часто используются координаты видимой области. Система координат документа используется при позиционировании элементов с помощью CSS (глава 16). Но проще всего получить координаты элемента в системе координат видимой области (раздел 15.8.2). Аналогично, когда регистрируется функция-обработчик событий от мыши, координаты указателя мыши передаются ей в системе координат видимой области.
Чтобы перейти от одной системы координат к другой, необходимо иметь возможность определять позиции полос прокрутки окна браузера. Во всех браузерах, кроме IE версии 8 и ниже, эти значения можно узнать с помощью свойств
pageXOffset
иpageYOffset
объектаWindow
. Кроме того, в IE (и во всех современных браузерах) позиции полос прокрутки можно узнать с помощью свойствscrollLeft
иscrollTop
. Проблема состоит в том, что в обычном случае позиции полос прокрутки следует читать из свойств корневого элемента (document.documentElement
) документа, а в режиме совместимости (раздел 13.4.4) необходимо обращаться к элементу<body>
(document.body
) документа. Пример 15.8 показывает, как определять позиции полос прокрутки переносимым образом.Пример 15.8. Получение позиций полос прокрутки окна
// Возвращает текущие позиции полос прокрутки в виде свойств x и y объекта function getScrollOffsets(w) { // Использовать указанное окно или текущее, // если функция вызвана без аргумента w = w || window; // Следующий способ работает во всех браузерах, кроме IE версии 8 и ниже if (w.pageXOffset != null) return {x: w.pageXOffset, y:w.pageYOffset}; // Для IE (и других браузеров) в стандартном режиме var d = w.document; if (document.compatMode == "CSS1Compat") return {x:d.documentElement.scrollLeft, y:d.documentElement.scrollTop}; // Для браузеров в режиме совместимости return { x: d.body.scrollLeft, y: d.body.scrollTop }; }Иногда бывает удобно иметь возможность определять размеры видимой области, например, чтобы определить, какая часть документа видима в настоящий момент. Как и в случае со смещениями прокрутки, самый простой способ узнать размеры видимой области не работает в IE версии 8 и ниже, а прием, который работает в IE, зависит от режима, в котором отображается документ. Пример 15.9 показывает, как переносимым способом определять размер видимой области. Обратите внимание на сходство программного кода этого примера с программным кодом в примере 15.8.
Пример 15.9. Получение размеров видимой области документа
// Возвращает размеры видимой области в виде свойств w и h объекта function getViewportSize(w) { // Использовать указанное окно или текущее, w = w || window; //если функция вызвана без аргумента // Следующий способ работает во всех браузерах, кроме IE версии 8 и ниже if (w.innerWidth != null) return {w: w.innerWidth, h:w.innerHeight}; // Для IE (и других браузеров) в стандартном режиме var d = w.document; if (document.compatMode == "CSS1Compat") return { w: d.documentElement.clientWidth, h: d.documentElement.clientHeight }; // Для браузеров в режиме совместимости return { w: d.body.clientWidth, h: d.body.clientWidth }; }В этих двух примерах использовались свойства
scrollLeft
,scrollTop
,clientWidth
и clientHeight. Мы встретимся с этими свойствами еще раз в разделе 15.8.5.15.8.2. Определение геометрии элемента
Самый простой способ определить размеры и координаты элемента – обратиться к его методу
getBoundingClientRect()
. Этот метод впервые появился в IE5 и в настоящее время реализован во всех текущих браузерах. Он не принимает аргументов и возвращает объект со свойствамиleft
,right
,top
иbottom
. Свойстваleft
иtop
возвращают координатыX
иY
верхнего левого угла элемента, а свойстваright
иbottom
возвращают координаты правого нижнего угла.Этот метод возвращает позицию элемента в системе координат видимой области. (Слово «Client» в имени метода
getBoundingClientRect()
косвенно указывает на клиентскую область веб-браузера, т. е. на окно и видимую область в нем.) Чтобы перейти к координатам относительно начала документа, которые не изменяются после прокрутки окна браузера пользователем, нужно добавить смещения прокрутки:
var box = e.getBoundingClientRect(); // Координаты в видимой области var offsets = getScrollOffsets(); // Вспомогат. функция, объявленная выше var x = box.left + offsets.x; // Перейти к координатам документа var y = box.top + offsets.y;Кроме того, во многих браузерах (и в стандарте W3C) объект, возвращаемый методом
getBoundingClientRect()
, имеет свойстваwidth
иheight
, но оригинальная реализация в IE не поддерживает их. Для совместимости ширину и высоту элемента можно вычислять, как показано ниже:var box = e.getBoundingClientRect(); var w = box.width || (box.right - box.left); var h = box.height || (box.bottom - box.top);В главе 16 вы узнаете, что содержимое элемента окружается необязательной пустой областью, которая называется отступом (padding). Отступы окружаются необязательной рамкой (border), а рамка окружается необязательными полями (margins). Координаты, возвращаемые методом
getBoundingClientRect()
, включают рамку и отступы элемента, но не включают поля.Если слово «Client» в имени метода
getBoundingClientRect()
определяет систему координат возвращаемого прямоугольника, то о чем свидетельствует слово «Bounding» (ограничивающий)? Блочные элементы, такие как изображения, абзацы и элементы<div>
, всегда отображаются браузерами в прямоугольных областях. Однако строчные элементы, такие как<span>
,<code>
и<b>
, могут занимать несколько строк и таким образом состоять из нескольких прямоугольных областей. Например, представьте некоторый курсивный текст (отмеченный тегами<i>
и</i>
), разбитый на две строки. Область, занимаемая этим текстом, состоит из прямоугольника в правой части первой строки и прямоугольника в левой части второй строки (в предположении, что текст записывается слева направо). Если передать методуgetBoundingClientRect()
строчный элемент, он вернет геометрию «ограничивающего прямоугольника» (bounding rectangle), содержащего все отдельные прямоугольные области. Для элемента<i>
, взятого в качестве приведенного выше примера, ограничивающий прямоугольник будет включать обе строки целиком.Для определения координат и размеров отдельных прямоугольников, занимаемых строчными элементами, можно воспользоваться методом
getClientRects()
, который возвращает объект, подобный массиву, доступный только для чтения, чьи элементы представляют объекты прямоугольных областей, подобные тем, что возвращаются методомgetBoundingClientRect()
.Мы уже знаем, что методы модели DOM, такие как
getElementsByTagName(),
возвращают «живые» результаты, изменяющиеся синхронно с изменением документа. Объекты прямоугольных областей (и списки объектов прямоугольных областей), возвращаемые методамиgetBoundingClientRect()
иgetClientRects()
не являются «живыми». Они хранят статические сведения о визуальном представлении документа на момент вызова. Они не обновляются, если пользователь прокрутит документ или изменит размеры окна браузера.
15.8.3. Определение элемента в указанной точке
Метод
getBoundingClientRect()
позволяет узнать текущую позицию элемента в видимой области. Но иногда бывает необходимо решить обратную задачу – узнать, какой элемент находится в заданной точке внутри видимой области. Сделать это можно с помощью методаelementFromPoint()
объектаDocument
. Он принимает координаты X и Y (относительно начала координат видимой области, а не документа) и возвращает объектElement
, находящийся в этой позиции. На момент написания этих строк алгоритм выбора элемента не был строго определен, но суть реализации метода сводится к тому, что он должен возвращать самый внутренний и самый верхний (в смысле CSS-атрибутаz-index
, который описывается в разделе 16.2.1.1) элемент, находящийся в этой точке. Если передать ему координаты точки, находящейся за пределами видимой области, методelementFromPoint()
вернетnull
, даже если после преобразования координат в систему координат документа получится вполне допустимая точка.Метод
elementFromPoint()
выглядит весьма практичным, и наиболее очевидный случай его использования – определение элемента, находящегося под указателем мыши по его координатам. Однако, как будет показано в главе 17, объект события от мыши уже содержит эту информацию в своем свойствеtarget
. Именно поэтому на практике методelementFromPoint()
почти не используется.
15.8.4. Прокрутка
В примере 15.8 демонстрировалось, как определять позиции полос прокрутки окна браузера. Чтобы заставить браузер прокрутить документ, можно присваивать значение используемым в этом примере свойствам
scrollLeft
иscrollTop
, но существует более простой путь, поддерживаемый с самых ранних дней развития языка JavaScript. МетодscrollTo()
объектаWindow
(и его синонимscroll()
) принимает координатыX
иY
точки (относительно начала координат документа) и устанавливает их в качестве величин смещения полос прокрутки. То есть он прокручивает окно так, что точка с указанными координатами оказывается в верхнем левом углу видимой области. Если указать точку, расположенную слишком близко к нижней или к правой границе документа, браузер попытается поместить эту точку как можно ближе к верхнему левому углу видимой области, но не сможет обеспечить точное их совпадение. Следующий пример прокручивает окно браузера так, что видимой оказывается самая нижняя часть документа:
// Получить высоту документа и видимой области. // Метод offsetHeight описывается ниже. var documentHeight = document.documentElement.offsetHeight; var viewportHeight = window.innerHeight; // Или использовать описанный // выше getViewportSize(), // И прокрутить окно так, чтобы переместить // последнюю "страницу" в видимую область window.scrollTo(0, documentHeight - viewportHeight);Метод
scrollBy()
объектаWindow
похож на методыscroll()
иscrollTo()
, но их аргументы определяют относительное смещение и добавляются к текущим позициям полос прокрутки. Тем, кто умеет быстро читать, мог бы понравиться следующий букмарклет (раздел 13.2.5.1):
// Прокручивает документ на 10 пикселов каждые 200 мсек. // Учтите, что этот букмарклет нельзя отключить после запуска! JavaScript:void setInterval(function() {scrollBy(0,10)}, 200); javascript:void setInterval(function() {scrollBy(0,10)}, 200);Часто требуется прокрутить документ не до определенных числовых координат, а до элемента в документе, который нужно сделать его видимым. В этом случае можно определить координаты элeмeнтa cпoмoщью мeтoдa
getBoundingClientRect()
, преобразовать их в координаты относительно начала документа и передать их методуscrollTo()
, но гораздо проще воспользоваться методомscrollIntoView()
требуемого HTML-элемента. Этот метод гарантирует, что элемент, относительно которого он будет вызван, окажется в видимой области. По умолчанию он старается прокрутить документ так, чтобы верхняя граница элемента оказалась как можно ближе к верхней границе видимой области. Если в единственном аргументе передать методу значениеfalse
, он попытается прокрутить документ так, чтобы нижняя граница элемента совпала с нижней границей видимой области. Кроме того, браузер выполнит прокрутку по горизонтали, если это потребуется, чтобы сделать элемент видимым.Своим поведением метод
scrollIntoView()
напоминает свойствоwindow.location.hash
, когда ему присваивается имя якорного элемента (элемента<a name="">
).15.8.5. Подробнее о размерах, позициях и переполнении элементов
Метод
getBoundingClientRect()
поддерживается всеми текущими браузерами, но если требуется обеспечить поддержку браузеров более старых версий, этот метод использовать нельзя и для определения размеров и позиций элементов следует применять более старые приемы. Размеры элементов определяются достаточно просто: доступные только для чтения свойстваoffsetWidth
иoffsetHeight
любого HTML- элемента возвращают его размеры в пикселах. Возвращаемые размеры включают рамку элемента и отступы, но не включают поля, окружающие рамку снаружи.Все HTML-элементы имеют свойства
offsetLeft
иoffsetTop
, возвращающие их координатыX
иY
. Для многих элементов эти координаты откладываются относительно начала документа и непосредственно определяют позицию элемента. Но для потомков позиционируемых элементов и некоторых других элементов, таких как ячейки таблиц, эти свойства возвращают координаты относительно элемента-предка, а не документа. СвойствоoffsetParent
определяет, относительно какого элемента исчисляются значения этих свойств. ЕслиoffsetParent
имеет значениеnull
, свойства содержат координаты относительно начала документа. Таким образом, в общем случае для определения позиции элемента е с помощью его свойствoffsetLeft
иoffsetTop
требуется выполнить цикл:
Выполняя обход цепочки, образуемой ссылками
offsetParent
, и накапливая смещения, эта функция вычисляет координаты указанного элемента относительно начала документа (напомню, что методgetBoundingClientRect()
, напротив, возвращает координаты относительно начала видимой области). Однако на этом тему позиционирования элементов нельзя считать исчерпанной – функцияgetElementPosition()
не всегда вычисляет правильные значения, и ниже будет показано, как исправить эту ошибку.В дополнение к множеству свойств
offset
все элементы документа определяют еще две группы свойств; имена свойств в первой группе начинаются с приставкиclient
, а во второй группе – с приставкиscroll
. To есть каждый HTML-элемент имеет все свойства, перечисленные ниже:Чтобы понять разницу между группами свойств
client
иscroll
, необходимо уяснить, что объем содержимого HTML-элемента может не умещаться в прямоугольную область, выделенную для этого содержимого, и поэтому отдельные элементы могут иметь собственные полосы прокрутки (смотрите описание CSS-атрибутаoverflow
в разделе 16.2.6). Область отображения содержимого – это видимая область, подобная видимой области в окне браузера, и когда содержимое элемента не умещается в видимой области, необходимо принимать в учет позиции полос прокрутки элемента.Свойства
clientWidth
иclientHeight
похожи на свойстваoffsetWidth
иoffsetHeight
, за исключением того, что они включают только область содержимого и отступы и не включают размер рамки. Кроме того, если браузер добавляет между рамкой и отступами полосы прокрутки, то свойстваclientWidth
иclientHeight
не включают ширину полос прокрутки в возвращаемые значения. Обратите внимание, что для строчных элементов, таких как<i>
,<code>
и<span>
, свойстваclientWidth
иclientHeight
всегда возвращают0
.Свойства
clientWidth
иclientHeight
использовались в методеgetViewportSize()
из примера 15.9. В том случае, когда эти свойства применяются к корневому элементу документа (или телу элемента в режиме совместимости), они возвращают те же значения, что и свойстваinnerWidth
иinnerHeight
окна.Свойства
clientLeft
иclientTop
не имеют большой практической ценности: они возвращают расстояние по горизонтали и вертикали между внешней границей отступов элемента и внешней границей его рамки. Обычно эти значения просто определяют ширину левой и верхней рамки. Однако если элемент имеет полосы прокрутки и если браузер помещает эти полосы прокрутки вдоль левого или верхнего края (что весьма необычно), значения свойствclientLeft
иclientTop
также будут включать ширину полос прокрутки. Для строчных элементов свойстваclientLeft
иclientTop
всегда возвращают0
.Свойства
scrollWidth
иscrollHeight
определяют размер области содержимого элемента, плюс его отступы, плюс ширину и высоту области содержимого, выходящую за видимую область. Когда содержимое целиком умещается в видимой области, значения этих свойств совпадают со значениями свойствclientWidth
иclientHeight
. В противном случае они включают ширину и высоту области содержимого, выходящую за видимую область, и возвращают значения, превосходящие значения свойствclientWidth
иclientHeight
.Наконец, свойства
scrollLeft
иscrollTop
определяют позиции полос прокрутки элемента. Мы использовали эти свойства корневого элемента документа в методеgetScrollOffsets()
(пример 15.8), но они также присутствуют в любом другом элементе. Обратите внимание, что свойстваscrollLeft
иscrollTop
доступны для записи и им можно присваивать значения, чтобы прокрутить содержимое элемента. (HTML-элементы не имеют методаscrollTo()
, как объектWindow
.)Когда документ содержит прокручиваемые элементы, содержимое которых не умещается в видимой области, объявленный выше метод
getElementPosition()
дает некорректные результаты из-за того, что он не учитывает позиции полос прокрутки. Ниже приводится исправленная версия, которая вычитает позиции полос прокрутки из накопленных смещений, т. е. преобразует координаты относительно начала документа в координаты относительно видимой области:
function getElementPos(elt) { var x = 0, y = 0; // Цикл, накапливающий смещения for(var e = elt; e != null; e = e.offsetParent) { x += e.offsetLeft; y += e.offsetTop; } // Еще раз обойти все элементы-предки, чтобы вычесть смещения прокрутки. // Он также вычтет позиции главных полос прокрутки и преобразует // значения в координаты видимой области. for(var e=elt.parentNode; e != null && e.nodeType == 1; e=e.parentNode) { x -= e.scrollLeft; y -= e.scrollTop; } return {x:x, y:y}; }В современных браузерах этот метод
getElementPos()
возвращает те же координаты, что и методgetBoundingClientRect()
(но он менее эффективен). Теоретически такая функция, какgetElementPos()
, могла бы использоваться в браузерах, не поддерживающих методgetBoundingClientRect()
. Однако браузеры, не поддерживающиеgetBoundingClientRect()
, имеют массу несовместимостей в механизме позиционирования элементов, и поэтому в них такая простая функция оказывается ненадежной. Клиентские библиотеки, такие как jQuery, включают функции вычисления позиций элементов, которые дополняют этот простой алгоритм множеством проверок, учитывающих несовместимости между браузерами. Если вам потребуется определять позиции элементов в браузерах, не поддерживающихgetBoundingClientRect()
, то для этих целей лучше использовать библиотеку, например, jQuery.15.9. HTML-формы
HTML-элемент
<form>
и различные элементы ввода, такие как<input>
,<select>
и<button>
, занимают видное место в разработке клиентских сценариев. Эти HTML-элементы появились в самом начале развития Всемирной паутины, еще до появления языка JavaScript. Формы HTML – это механизм веб-приложений первого поколения, не требующий применения JavaScript. Ввод пользователя собирается в элементах форм; затем форма отправляется на сервер; сервер обрабатывает ввод и генерирует новую HTML-страницу (обычно с новыми элементами форм) для отображения на стороне клиента.Элементы HTML-форм по-прежнему остаются великолепным инструментом получения данных от пользователя, даже когда данные формы целиком обрабатываются JavaScript-сценарием на стороне клиента и не должны отправляться на сервер. С точки зрения программиста, разрабатывающего серверные сценарии, форма оказывается совершенно бесполезной, если в ней отсутствует кнопка отправки формы. Однако с точки зрения программиста, разрабатывающего клиентские сценарии, кнопка отправки вообще не нужна (хотя все еще может использоваться). Серверные программы опираются на механизм отправки форм – они обрабатывают данные порциями, объем которых определяется формой, – и это ограничивает их интерактивные возможности. Клиентские программы опираются на механизм событий – они могут откликаться на события, возникающие в отдельных элементах форм, – и это позволяет обеспечить более высокую степень интерактивности. Например, клиентская программа может проверять ввод пользователя по мере нажатия клавиш. Или откликаться на выбор флажка, разрешая доступ к набору параметров, которые имеют смысл, только когда флажок отмечен.
В следующих подразделах описывается, как реализовать подобные операции при работе с HTML-формами. Формы конструируются из HTML-элементов, как и любые другие части HTML-документа, и ими можно управлять с помощью методов модели DOM, описанных выше в этой главе. Элементы формы стали первыми элементами, для которых было предусмотрено программное управление на самых ранних этапах развития клиентского JavaScript, поэтому они поддерживают некоторые дополнительные функции, предшествовавшие появлению DOM.
Обратите внимание, что в этом разделе описываются не сам язык разметки HTML, а приемы управления HTML-формами. Здесь предполагается, что вы уже имеете некоторое знакомство с HTML-элементами (
<input>
,<textarea>
,<select>
, и т. д), используемыми для создания форм. Тем не менее в табл. 15.1 для справки приводится список наиболее часто используемых элементов форм. Дополнительные сведения о функциях для работы с формами и элементами форм можно узнать в четвертой части книги, в справочных статьях Form, Input, Option, Select и TextArea.Таблица 15.1. Элементы HTML-форм
HTML-элемент Свойство type
Обработчик событий Описание и события <input type="button">
или<button type="button">
" button
"onclick
Кнопка <input type="checkbox">
" checkbox
"onchange
Переключаемый флажок; с иным поведением, чем у радиокнопки <input type="file">
" file
"onchange
Поле ввода имени файла для выгрузки на сервер; свойство value
доступно только для чтения<input type="hidden">
" hidden
"none Данные, отправляемые вместе с формой, но не видимые пользователю <option>
none none Единственный пункт объекта Select; обработчик событий подключается к объекту Select, а не к объектам Option
<input type="password">
" password
"onchange
Поле ввода пароля – скрывает вводимые символы <input type="radio">
" radio
"onchange
Переключатель с поведением радиокнопки – в каждый конкретный момент времени может быть выбран только один переключатель <input type="reset">
или<button type="reset">
" reset
"onclick
Кнопка, сбрасывающая форму в исходное состояние <select>
" select-one
"onchange
Список или раскрывающееся меню, в котором можно выбрать только один пункт (смотрите также <option>) <select multiple>
" select-multiple
"onchange
Список, в котором можно выбрать сразу несколько пунктов (смотрите также <option>) <input type="submit">
или<button type="submit">
" submit
"onclick
Кнопка, инициирующая отправку формы <input type="text">
" text
"onchange
Однострочное текстовое поле ввода; по умолчанию в элементе <input> атрибут type отсутствует или не опознается <textarea>
" textarea
"onchange
Многострочное текстовое поле ввода 15.9.1. Выбор форм и элементов форм
Формы и элементы, содержащиеся в них, можно выбрать с помощью стандартных методов, таких как
getElementById()
иgetElementsByTagName():
:var fields=document.getElementById("address").getElementsByTagName("input");В браузерах, поддерживающих
querySelectorAll()
, можно выбрать все радиокнопки или все элементы с одинаковыми именами, присутствующие в форме, как показано ниже:
// Все радиокнопки в форме с атрибутом id="shipping" document.querySelectorAll(\'#shipping input[type="radio"]'); // Все радиокнопки с атрибутом "method" в форме с атрибутом id="shipping" document.querySelectorAll(\'#shipping input[type="radio"][name="method"]');Однако, как описывалось в разделах 14.7, 15.2.2 и 15.2.3, элемент
<form>
с установленным атрибутомname
илиid
можно также выбрать другими способами. Элемент<form>
с атрибутомname="address"
можно выбрать любым из следующих способов:
window.address // Ненадежный: старайтесь не использовать document.address // Может применяться только к формам с атрибутом name document.forms.address // Явное обращение к форме с атрибутом name или id document.forms[n] // Ненадежный: n - порядковый номер формыВ разделе 15.2.3 говорилось, что свойство
document.forms
ссылается на объектHTML-Collection
, позволяющий выбирать элементы<form>
по их порядковым номерам, по значению атрибутаid
илиname
. ОбъектыForm
сами по себе действуют подобно объектамHTMLCollection
, хранящим элементы форм, и могут индексироваться именами или числами. Если первый элемент формы с атрибутомname="address"
имеет атрибутname="street"
, на него можно сослаться с помощью любого из следующих выражений:
document.forms.address[0] document.forms.address.street document.address.street // только для name="address", но не id="address"Если необходимо явно указать, что выполняется обращение к элементу формы, вместо объекта формы можно индексировать его свойство
elements
:
document.forms.address.elements[0] document.forms.address.elements.streetДля выбора конкретных элементов документа предпочтительнее использовать атрибут
id
. Однако при отправке форм атрибутname
играет особую роль и чаще используется с самими формами, чем с элементами форм. Обычно при работе с группами флажков и обязательно при работе с группами радиокнопок для атрибутаname
используется одно и то же значение. Напомню, что, когда объектHTMLCollection
индексируется именем и существует сразу несколько элементов, использующих одно и то же имя, возвращаемым значением является объект, подобный массиву, содержащий все элементы с указанным именем. Взгляните на следующую форму, содержащую радиокнопки для выбора метода доставки товара:
<form name="shipping"> <fieldset><legend>Shipping Method</legend> <label><input type="radio" name="method" value="1st">Первым классом</label> <label><input type="radio" name="method" value="2day">За 2 дня самолетом</label> <label><input type="radio" name="method" value="overnite">В течение ночи</label> </fieldset> </form>Сослаться на массив радиокнопок в этой форме можно следующим образом:
var methods = document.forms.shipping.elements.method;
Обратите внимание, что элементы
<form>
имеют HTML-атрибут и соответствующее ему свойство с именем «method», поэтому в данном случае необходимо использовать свойствоelements
формы вместо прямого обращения к свойствуmethod
. Чтобы определить, какой метод доставки выбрал пользователь, необходимо обойти элементы формы в массиве и проверить свойствоchecked
каждого из них:
var shipping_method; for(var i = 0; i < methods.length; i++) if (methods[i].checked) shipping_method = methods[i].value;Co свойствами элементов форм, такими как
checked
иvalue
, мы поближе познакомимся в следующем разделе.15.9.2. Свойства форм и их элементов
Наиболее интересным для нас свойством объекта
Form
является массивelements[]
, описанный выше. Остальные свойства объектаForm
менее важны. Свойстваaction
,encoding
,method
иtarget
непосредственно соответствуют атрибутамaction
,encoding
,method
иtarget
элемента<form>
. Все эти свойства и атрибуты используются для управления отправкой данных формы на веб-сервер и отображением результатов. Клиентский сценарий на языке JavaScript может устанавливать значения этих свойств, но это имеет смысл, только когда форма действительно отправляется серверной программе.До появления JavaScript отправка форм выполнялась с помощью специальной кнопки
Submit
, а сброс значений элементов формы в значения по умолчанию с помощью специальной кнопкиReset
. В языке JavaScript тем же целям служат два метода,submit()
иreset()
, объектаForm
. Методsubmit()
объектаForm
отправляет форму, а методreset()
сбрасывает элементы формы в исходное состояние.У всех (или у большинства) элементов форм есть общие свойства, перечисленные далее. Кроме того, у некоторых элементов есть специальные свойства, которые будут описаны ниже, когда мы будем рассматривать элементы форм различных типов по отдельности.
type
Доступная только для чтения строка, идентифицирующая тип элемента формы. Для элементов форм, определяемых с помощью тега<input>
, это свойство просто хранит значение атрибутаtype
. Другие элементы форм (такие как<textarea>
и<select>
) также определяют свойствоtype
, благодаря чему его можно использовать в сценарии для идентификации элементов, подобно тому, как идентифицируются различные типы элементов<input>
. Значения этого свойства для каждого типа элементов форм перечислены во втором столбце табл. 15.1.form
Доступная только для чтения ссылка на объектForm
, в котором содержится этот элемент, илиnull
, если элемент не находится внутри элемента<form>
.name
Доступная только для чтения строка, указанная в HTML-атрибутеname
.value
Доступная для чтения и записи строка, определяющая «значение», содержащееся в элементе формы или представляемое им. Эта строка отсылается на веб-сервер при передаче формы и только иногда представляет интерес для JavaScript-программ. Для элементовText
иTextarea
это свойство содержит введенный пользователем текст. Для кнопок, создаваемых с помощью тега<input>
(но не для кнопок, создаваемых с помощью тега<button>
), это свойство определяет отображаемый на кнопке текст. Свойствоvalue
для элементов переключателей (радиокнопок) и флажков не редактируется и никак не представляется пользователю. Это просто строка, устанавливаемая HTML-атрибутомvalue
. Эта строка предназначена для отправки веб-серверу, и ее можно использовать для передачи дополнительных данных. Свойствоvalue
будет обсуждаться далее в этой главе, когда мы будем рассматривать различные категории элементов формы.15.9.3. Обработчики событий форм
Каждый элемент
Form
имеет обработчик событияonsubmit
, возникающего в момент отправки формы, и обработчик событияonreset
, возникающего в момент сброса формы в исходное состояние. Обработчикonsubmit
вызывается непосредственно перед отправкой формы. Он может отменить отправку, вернув значениеfalse
. Это дает JavaScript-программам возможность проверить ввод пользователя и избежать отправки неполных или ошибочных данных серверной программе. Обратите внимание, что обработчикonsubmit
вызывается только в случае щелчка мышью на кнопкеSubmit
. Вызов методаsubmit()
формы не приводит к вызову обработчикаonsubmit
.Обработчик событий on reset похож на обработчик
onsubmit
. Он вызывается непосредственно перед сбросом формы в исходное состояние и может предотвратить сброс элементов формы, вернув значениеfalse
. Кнопки Reset редко используются в формах, но если у вас имеется такая кнопка, возможно, у вас появится желание запросить у пользователя подтверждение, прежде чем выполнить сброс:<form... onreset="return confirm('Вы действительно хотите сбросить все и начать сначала?')"> ... <button type="reset">Очистить поля ввода и начать сначала</button> </form>Подобно обработчику
onsubmit
, обработчик on reset вызывается только в случае щелчка мышью на кнопке Reset. Вызов методаreset()
формы не приводит к вызову обработчикаonreset
.Элементы форм обычно возбуждают событие
click
илиchange
, когда пользователь взаимодействует с ними, и вы можете реализовать обработку этих событий, определив обработчикonclick
илиonchange
. В третьем столбце таблицы 15.1 для каждого элемента формы указан основной обработчик событий. Вообще говоря, элементы форм, являющиеся кнопками, возбуждают событиеclick
в момент активации (даже когда активация производится посредством нажатия клавиши на клавиатуре, а не щелчком мышью). Другие элементы форм возбуждают событиеchange
, когда пользователь изменяет содержимое, представляемое элементом. Это происходит, когда пользователь вводит текст в текстовое поле или выбирает элемент раскрывающегося списка. Обратите внимание, что это событие возбуждается не каждый раз, когда пользователь нажимает клавишу, находясь в текстовом поле ввода. Оно возбуждается, только когда пользователь изменит значение элемента и перенесет фокус ввода в другой элемент. То есть этот обработчик событий вызывается по завершении ввода. Радиокнопки и флажки являются кнопками, хранящими информацию о своем состоянии, и все они возбуждают событияclick
иchange
; из них событиеchange
имеет большее практическое значение.Элементы форм также возбуждают событие focus, когда они получают фокус ввода, и событие
blur
, когда теряют его.Важно знать, что внутри обработчика события ключевое слово
this
всегда ссылается на элемент документа, вызвавший данное событие (подробнее об этом будет рассказываться в главе 17). Во всех элементах форм имеется свойствоform
, ссылающееся на форму, в которой содержится элемент, поэтому обработчики событий элемента формы всегда могут обратиться к объектуForm
, как кthis.form
. Сделав еще один шаг, мы можем сказать, что обработчик событий для одной формы может ссылаться на соседний элемент формы, имеющий имяx
, какthis.form.x
.15.9.4. Кнопки
Кнопки являются одними из наиболее часто используемых элементов форм, т. к. они предоставляют понятный визуальный способ вызова пользователем какого-либо запрограммированного сценарием действия. Элемент кнопки не имеет собственного поведения, предлагаемого по умолчанию, и не представляет никакой пользы без обработчика события
onclick
. Кнопки, определяемые с помощью элементов<input>
, отображают простой текст, содержащийся в их свойствеvalue
.Кнопки, определяемые с помощью элементов
<button>
, отображают содержимое элемента.Обратите внимание, что гиперссылки предоставляют такой же обработчик событий
onclick
, как и кнопки. Когда действие, выполняемое обработчикомonclick
, можно классифицировать как «переход по ссылке», используйте ссылку. В противном случае используйте кнопку.Элементы
Submit
иReset
очень похожи на элементы-кнопки, но имеют связанные с ними действия, предлагаемые по умолчанию (передача или очистка формы). Если обработчик событийonclick
возвращаетfalse
, стандартное действие этих кнопок, предусмотренное по умолчанию, не выполняется. Обработчикonclick
элементаSubmit
можно использовать для проверки введенных значений, но обычно это делается в обработчикеonsubmit
самой формы.В четвертой части книги нет описания элемента
Button
. Описание всех элементов- кнопок, включая описание элементов, создаваемых с помощью тега<button>
, вы найдете в разделе, посвященном элементуInput
.15.9.5. Переключатели и флажки
Флажки и радиокнопки имеют два визуально различимых состояния: они могут быть либо установлены, либо сброшены. Пользователь может изменить состояние такого элемента, щелкнув на нем. Радиокнопки обычно объединяются в группы связанных элементов, имеющих одинаковые значения HTML-атрибута
name
. При установке одной из радиокнопок предыдущая установленная в группе радиокнопка сбрасывается. Флажки тоже часто объединяются в группы с общим значением атрибутаname
, и когда вы обращаетесь к ним по имени, необходимо помнить, что вы получаете в ответ объект, подобный массиву, а не отдельный элемент.И флажки, и переключатели имеют свойство
checked
. Это доступное для чтения и записи логическое значение определяет, отмечен ли элемент в данный момент. СвойствоdefaultChecked
представляет собой доступное только для чтения логическое значение, содержащее значение HTML-атрибутаchecked
; оно определяет, должен ли элемент отмечаться, когда страница загружается в первый раз.Флажки и радиокнопки сами не отображают какой-либо текст и обычно выводятся вместе с прилегающим к ним HTML-текстом (или со связанным тегом
<label>
). Это значит, что установка свойстваvalue
элемента флажка или радиокнопки не изменяет внешнего вида элемента. Свойствоvalue
можно установить, но это изменит лишь строку, отсылаемую на веб-сервер при передаче данных формы.Когда пользователь щелкает на флажке или радиокнопке, элемент вызывает свой обработчик
onclick
. Если состояние флажка или радиокнопки изменяется в результате щелчка мышью, они также вызывают обработчик событийonchange
. (При этом радиокнопки, изменяющие состояние в результате щелчка на другой радиокнопке, не вызывают обработчикonchange
.)15.9.6. Текстовые поля ввода
Однострочные текстовые поля ввода
Text
применяются в HTML-формах и JavaScript-программах, пожалуй, чаще других. Они позволяют пользователю ввести короткий однострочный текст. Свойствоvalue
представляет текст, введенный пользователем. Установив это свойство, можно явно задать выводимый текст.Определяемый стандартом HTML5 атрибут
placeholder
позволяет указать строку приглашения к вводу, которая будет отображаться в поле ввода до того момента, пока пользователь не введет какой-нибудь текст:Дата прибытия: <input type="text" name="arrival" placeholder="yyyy-mm-dd">Обработчик событий
onchange
текстового поля ввода вызывается, когда пользователь вводит новый текст или редактирует существующий, а затем указывает, что он завершил редактирование, убрав фокус ввода из текстового поля.Элемент
Textarea
(многострочное текстовое поле ввода) очень похож на элементText
за исключением того, что разрешает пользователю ввести (а JavaScript-программе вывести) многострочный текст. Многострочное текстовое поле создается тегом<textarea>
, синтаксис которого существенно отличается от синтаксиса тега<input>
, используемого для создания однострочного текстового поля. (Подробнее об этом см. в разделе с описанием элементаTextarea
в четвертой части книги.) Тем не менее, эти два типа элементов ведут себя очень похожим образом. Свойствоvalue
и обработчик событий onchange элементаTextarea
можно использовать точно так же, как в случае с элементомText
.Элемент
<input type="password">
– это модификация однострочного текстового поля ввода, в котором вместо вводимого пользователем текста отображаются символы звездочек. Как можно заключить из имени элемента, его можно использовать, чтобы дать пользователю возможность вводить пароли, не беспокоясь о том, что другие прочитают их через плечо. Следует понимать, что элементPassword
защищает введенные пользователем данные от любопытных глаз, но при отправке данных формы эти данные никак не шифруются (если только отправка не выполняется по безопасному HTTPS-соединению) и при передаче по сети могут быть перехвачены.Наконец, элемент
<input type="file">
предназначен для ввода пользователем имени файла, который должен быть выгружен на веб-сервер. По существу, это однострочное текстовое поле, совмещенное со встроенной кнопкой, выводящей диалог выбора файла. У элемента выбора файла, как и у однострочного текстового поля, есть обработчик событий onchange. Однако, в отличие от текстового поля ввода, свойствоvalue
элемента выбора файла доступно только для чтения. Это не дает злонамеренным JavaScript-программам обмануть пользователя, выгрузив файл, не предназначенный для отправки на сервер.Различные текстовые элементы ввода определяют обработчики событий
onkeypress
,onkeydown
иonkeyup
. Можно вернутьfalse
из обработчиков событийonkeypress
илиonkeydown
, чтобы запретить обработку нажатой пользователем клавиши. Это может быть полезно, например, когда требуется заставить пользователя вводить только цифры. Этот прием демонстрируется в примере 17.6.15.9.7. Элементы Select и Option
Элемент
Select
представляет собой набор вариантов (представленных элементамиOption
), которые могут быть выбраны пользователем. Браузеры обычно отображают элементыSelect
в виде раскрывающихся меню, но, если указать в атрибуте size значение больше чем 1, они будут отображать их в виде списков (возможно, с полосами прокрутки). ЭлементSelect
может работать двумя сильно различающимися способами, а выбор того или иного способа определяется значением свойстваtype
. Если в теге<select>
определен атрибутmultiple
, пользователь сможет выбрать несколько вариантов, а свойствоtype
объектаSelect
будет иметь значение «select-multiple
». В противном случае, если атрибутmultiple
отсутствует, может быть вы: бран только один вариант и свойствоtype
будет иметь значение «select-one».В некотором смысле элемент с возможностью множественного выбора похож на набор флажков, а элемент без такой возможности – на набор радиокнопок. Однако варианты выбора, отображаемые элементом
Select
, не являются кнопками-переключателями: они определяются с помощью тега<option>
. ЭлементSelect
определяет свойствоoptions
, которое является объектом, подобным массиву, хранящим объектыOption
.Когда пользователь выбирает тот или иной вариант или отменяет выбор, элемент
Select
вызывает свой обработчик событий onchange. Для элементовSelect
с возможностью выбора единственного варианта, доступное только для чтения свойствоselectedIndex
определяет выбранный в данный момент вариант. Для элементовSelect
с возможностью множественного выбора одного свойстваselectedIndex
недостаточно для представления полного набора выбранных вариантов. В этом случае для определения выбранных вариантов следует в цикле перебрать элементы массиваoptions[]
и проверить значения свойства selected каждого объектаOption
.Кроме свойства selected у каждого объекта
Option
есть свойствоtext
, задающее строку текста, которая отображается в элементеSelect
для данного варианта. Используя это свойство, можно изменить видимый пользователем текст. Свойствоvalue
представляет доступную для чтения и записи строку текста, который отсылается на веб-сервер при передаче данных формы. Даже если вы пишете исключительно клиентскую программу и ваши формы никуда не отправляются, свойствоvalue
(или соответствующий ей HTML-атрибутvalue
) можно использовать для хранения данных, которые потребуются после выбора пользователем определенного варианта. Обратите внимание, что элементOption
не определяет связанных с формой обработчиков событий; используйте вместо этого обработчик onchange соответствующего элементаSelect
.Помимо задания свойства
text
объектовOption
имеются способы динамического изменения выводимых в элементеSelect
вариантов с помощью особых возможностей свойстваoptions
, которые ведут свое существование с самого начала появления поддержки клиентских сценариев. Можно обрезать массив элементовOption
, установив свойствоoptions.length
равным требуемому количеству вариантов, или удалить все объектыOption
, установив значение свойстваoptions.length
равным нулю. Можно удалять отдельные объектыOption
из элементаSelect
, присваивая элементам массиваoptions[]
значениеnull
. В этом случае удаляются соответствующие объектыOption
, а все элементы, расположенные в массивеoptions[]
правее, автоматически сдвигаются влево, заполняя опустевшее место.Чтобы добавить в элемент
Select
новый вариант, можно создать его с помощью конструктораOption()
и добавить в конец массиваoptions[]
, как показано ниже:
// Создать новый объект Option var zaire = new Option("Zaire", // Свойство text "zaire", // Свойство value false, // Свойство defaultSelected false); // Свойство selected // Отобразить вариант в элементе Select, // добавив его в конец массива options: var countries = document.address.country; // Получить объект Select countries.options[countries.options.length] = zaire;Имейте в виду, что эти специальные возможности элемента
Select
пришли к нам из прошлых времен. Вставку и удаление вариантов можно реализовать более очевидным способом, воспользовавшись стандартными методамиDocument.createElement()
,Node.insertBefore()
,Node.removeChild()
и другими.15.10. Другие особенности документов
Эта глава начиналась с утверждения, что она является одной из наиболее важных в книге. Она также по необходимости оказалась одной из наиболее длинных. Этот раздел завершает главу описанием некоторых особенностей объекта
Document
.15.10.1. Свойства объекта Document
В этой главе уже были представлены некоторые свойства объекта
Document
, такие какbody
,documentElement
иforms
, ссылающиеся на специальные элементы документа. Кроме них документы определяют еще несколько свойств, представляющих интерес:cookie
Специальное свойство, позволяющее JavaScript-программам читать и писать cookie-файлы. Это свойство рассматривается в главе 20.domain
Свойство, которое позволяет доверяющим друг другу веб-серверам, принадлежащим одному домену, ослаблять связанные с политикой общего происхождения ограничения на взаимодействие между их веб-страницами (подробности см. в разделе 13.6.2.1).lastModified
Строка, содержащая дату последнего изменения документа.location
Это свойство ссылается на тот же объектLocation
, что и свойствоlocation
объектаWindow
.referrer
URL-адрес документа, содержащего ссылку (если таковая существует), которая привела браузер к текущему документу. Это свойство имеет то же значение, что и HTTP-заголовокReferer
, но записывается с двумя буквами r.title
Текст между тегами<title>
и</title>
данного документа.URL
Свойство URL документа является строкой, доступной только для чтения, а не объектомLocation
. Значение этого свойства совпадает с начальным значением свойстваlocation.href
, но, в отличие от объектаLocation
, не является динамическим. Если пользователь выполнит переход, указав новый идентификатор фрагмента внутри документа, то свойствоlocation.href
изменится, а свойствоdocument.URL
-нет.Из всех этих свойств наибольший интерес представляет свойство
referrer
: оно содержит URL-адрес документа, содержащего ссылку, которая привела пользователя к текущему документу. Это свойство можно было бы использовать по примеру следующего кода:
if (document.referrer.indexOf("http://www.google.com/search?") == 0) { var args = document.referrer.substring(ref.indexOf("?")+1).split("&"); for(var i = 0; i < args.length; i++) { if (args[i].substring(0,2) == "q=") { document.write("<p>Добро пожаловать, пользователь Google. "); document.write("Вы искали: " + unescape(args[i].substring(2)).replace(\'+\', \' \ break; } } }Метод
document.write()
, использованный в этом примере, является темой следующего раздела.15.10.2. Метод document.write()
Метод
document.write()
был одним из первых методов, реализованных еще в веб-браузере Netscape 2. Он появился до создания модели DOM и представлял единственный способ отображения динамически изменяемого текста в документе. В современных сценариях надобность в этом методе отпала, но вы наверняка встретите его в существующем программном коде.Метод
document.write()
объединяет свои строковые аргументы и вставляет получившуюся строку в документ, в точку вызова метода. По завершении выполнения сценария браузер выполнит синтаксический анализ сгенерированного вывода и отобразит его. Например, следующий фрагмент использует методwrite()
для динамического вывода информации в статический HTML-документ:*/ //-->
<script> document.write("<p>Заголовок документа: " + document.title); document.write("<br>URL: " + document.URL); document.write("<br>Ссылающийся на него документ: " + document.referrer); document.write("<br>Изменен: " + document.lastModified); document.write("<br>Открыт: " + new Date()); </script>Важно понимать, что метод
write()
можно использовать для вывода разметки HTML только в процессе синтаксического анализа документа. То есть вызывать методdocument.write()
из программного кода верхнего уровня в теге <script> можно только в случае, если выполнение сценария является частью процесса анализа документа. Если поместить вызовdocument.write()
в определение функции и затем вызвать эту функцию из обработчика события, результат окажется неожиданным – этот вызов уничтожит текущий документ и все содержащиеся в нем сценарии! (Почему это происходит, будет описано чуть ниже.) По тем же причинам нельзя использоватьdocument.write()
в сценариях с установленными атрибутамиdefer
илиasync
.Пример 13.3 в главе 13 использует метод
document.write()
указанным способом/ чтобы сгенерировать более сложный вывод.Метод
write()
можно использовать для создания полностью новых документов в других окнах и фреймах (однако при работе с другими окнами и фреймами необходимо позаботиться о том, чтобы не нарушать политику общего происхождения). Первый вызов методаwrite()
другого документа полностью уничтожит имеющееся содержимое этого документа. Методwrite()
можно вызывать многократно, тем самым добавляя новое содержимое в документ. Содержимое, передаваемое методуwrite()
, может буферизоваться (и не отображаться), пока не будет выполнен завершающий вызов метода close() объекта документа, сообщающий парсеру, что работа с документом окончена и он может выполнить разбор документа и отобразить его.Следует отметить, что объект
Document
поддерживает также методwriteln()
, который идентичен методуwrite()
за исключением того, что он добавляет символ перевода строки после вывода своих аргументов. Это может пригодиться, например, при выводе форматированного текста внутри элемента <pre>.Метод
document.write()
редко используется в современных сценариях: свойствоinnerHTML
и другие приемы, поддерживаемые моделью DOM, обеспечивают более удачные способы добавления содержимого в документ. С другой стороны, некоторые алгоритмы лучше укладываются в схему потокового ввода/вывода, реализуемую методомwrite()
. Если вы создаете сценарий, который динамически генерирует и выводит текст в процессе своего выполнения, вам, возможно, будет интересно ознакомиться с примером 15.10, в котором свойствоinnerHTML
указанного элемента обертывается простыми методамиwrite()
иclose()
.Пример 15.10. Интерфейс потоков ввода-вывода к свойству
innerHTML
// Определить простейший интерфейс "потоков ввода/вывода" // для свойства innerHTML элемента. function ElementStream(elt) { if (typeof elt === "string") elt = document.getElementById(elt); this.elt = elt; this.buffer = ""; } // Объединяет все аргументы и добавляет в буфер ElementStream.prototype.write = function() { this.buffer += Array.prototype.join.call(arguments, ""); }; // To же, что и write(), но добавляет символ перевода строки ElementStream.prototype.writeln = function() { this.buffer += Array.prototype.join.call(arguments, "") + "\n"; }; // Переносит содержимое буфера в элемент и очищает буфер. ElementStream.prototype.close = function() { this.elt.innerHTML = this.buffer; this.buffer = ""; };15.10.3. Получение выделенного текста
Иногда удобно иметь возможность определять, какой участок текста документа выделен пользователем. Сделать это можно, как показано ниже:
function getSelectedText() { if (window.getSelection) // Функция API, определяемая стандартом HTML5 return window.getSelection().toString(); else if (document.selection) // Прием, характерный для IE. return document.selection.createRange().text; }Стандартный метод
window.getSelection()
возвращает объектSelection
, описывающий текущий выделенный текст, как последовательность одного или более объектовRange
. ОбъектыSelection
иRange
определяют чрезвычайно сложный прикладной интерфейс, который практически не используется и не описывается в этой книге. Наиболее важной и широко реализованной (везде, кроме IE) особенностью объектаSelection
является его методtoString()
, который возвращает простое текстовое содержимое выделенной области.Браузер IE определяет иной прикладной интерфейс, который не описывается в этой книге. Метод
document.Selection
возвращает объект, представляющий выделенную область. МетодcreateRange()
этого объекта возвращает реализованный только в IE объектTextRange
, свойствоtext
которого содержит выделенныйПрием, подобный приведенному в примере выше, может, в частности, пригодиться в букмарклетах (раздел 13.2.5.1) для организации поиска выделенного текста в поисковых системах или на сайте. Так, следующая HTML-ссылка пытается отыскать текущий выделенный фрагмент текста в Википедии. Если поместить в закладку эту ссылку и URL-адрес со спецификатором javascript:, закладка превратится в букмарклет:
<a href="javascript: var q; if (window.getSelection) q = window.getSelection().toString(); else if (document.selection) q = document.selection.createRange().text; void window.open(\'http://en.wikipedia.org/wiki/\' + q);"> Поиск выделенного текста в Википедии </a>В приведенном выше примере, выбирающем выделенный текст, есть одна проблема, связанная с несовместимостью. Метод
getSelection()
объектаWindow
не возвращает выделенный текст, если он находится внутри элемента<input>
или<textarea>
: он возвращает только тот текст, который выделен в теле самого документа. В то же время свойствоdocument.selection
, поддерживаемое браузером IE, возвращает текст, выделенный в любом месте в документе.Чтобы получить текст, выделенный в текстовом поле ввода или в элементе
<textarea>
, можно использовать следующее решение:elt.value.substring(elt.selectionStart, elt.selectionEnd);Свойства selectionStart и selectionEnd не поддерживаются в версиях 1Е8 и ниже.
15.10.4. Редактируемое содержимое
Мы уже познакомились с элементами форм, включая текстовые поля ввода и элементы
textarea
, которые дают пользователям возможность вводить и редактировать простой текст. Вслед за IE все текущие веб-браузеры стали поддерживать простые средства редактирования разметки HTML. Вы можете увидеть их в действии на страницах (например, на страницах блогов, где можно оставлять комментарии), встраивающих редактор форматированного текста с панелью инструментов, содержащей кнопки для выбора стиля отображения шрифта (полужирный, курсив), настройки выравнивания и вставки изображений и ссылок.Существует два способа включения поддержки возможности редактирования. Можно установить HTML-атрибут
contenteditable
любого тега или установить JavaScript-свойствоcontenteditable
соответствующего элементаElement
, содержимое которого разрешается редактировать. Когда пользователь щелкнет на содержимом внутри этого элемента, появится текстовый курсор, и пользователь сможет вводить текст с клавиатуры. Ниже представлен HTML-элемент, создающий область редактирования:<div id="editor" contenteditable> Click to edit </div>Браузеры могут поддерживать автоматическую проверку орфографии для полей форм и элементов с атрибутом
contenteditable
. В браузерах, поддерживающих такую проверку, она может быть включена или выключена по умолчанию. Чтобы явно включить ее, следует добавить атрибутspellcheck
. А чтобы явно запретить – добавить атрибутspellcheck=false
(если, например, в элементе<textarea>
предполагается выводить программный код или другой текст с идентификаторами, отсутствующими в словаре).Точно так же можно сделать редактируемым весь документ, записав в свойство
designMode
объектаDocument
строку «on». (Чтобы снова сделать документ доступным только для чтения, достаточно записать в это свойство строку «off».) СвойствоdesignMode
не имеет соответствующего ему HTML-атрибута. Можно сделать документ доступным для редактирования, поместив его в элемент<iframe>
, как показано ниже (обратите внимание, что здесь используется функцияonLoad()
из примера 13.5):*/ //-->
<iframe id="editor" src="about:blank"></iframe> // Пустой фрейм <script> onLoad(function() { // После загрузки, var editor = document.getElementById("editor"); // найти фрейм документа editor.contentDocument.designMode = "on"; // и включить режим }); // редактирования. </script>Все текущие браузеры поддерживают свойства
contenteditable
иdesignMode
. Однако они оказываются плохо совместимыми, когда дело доходит до фактического редактирования. Все браузеры позволяют вставлять и удалять текст и перемещать текстовый курсор с помощью клавиатуры и мыши. Во всех браузерах нажатие клавиши Enter выполняет переход на новую строку, но разные браузеры создают в результате разную разметку. Некоторые начинают новый абзац, другие просто вставляют элемент<br/>
.Некоторые браузеры позволяют использовать горячие комбинации клавиш, такие как Ctrl-B, чтобы изменить шрифт выделенного текста на полужирный. В других браузерах (таких как Firefox) стандартные для текстовых процессоров комбинации, такие как Ctrl-B и Ctrl-I, выполняют другие операции, имеющие отношение к самому браузеру, а не к текстовому редактору.
Браузеры определяют множество команд редактирования текста, для большинства из которых не предусмотрены горячие комбинации клавиш. Чтобы выполнить эти команды, необходимо использовать метод
execCommand()
объектаDocument
(Обратите внимание, что это метод объектаDocument
, а не элемента с атрибутомcontenteditable
. Если документ содержит более одного редактируемого элемента, команда применяется к тому из них, в котором в текущий момент находится текстовый курсор.) Команды, выполняемые методомexecCommand()
, определяются строками, такими как «bold», «subscript», «justifycenter» или «insertimage». Имя команды передается методуexecCommand()
в первом аргументе. Некоторые команды требуют дополнительное значение. Например, команда «createlink» требует указать URL для гиперссылки. Теоретически, если во втором аргументе передать методуexecCommand()
значениеtrue
, браузер автоматически запросит у пользователя ввести необходимое значение. Однако для большей совместимости вам необходимо самим запрашивать у пользователя требуемые данные, передаваяfalse
во втором аргументе и требуемое значение – в третьем аргументе.Ниже приводятся две функции, которые реализуют редактирование с помощью метода execCommand():
function bold() { document.execCommand("bold", false, null); } function link() { var url = prompt("Введите адрес гиперссылки"); if (url) document.execCommand("createlink", false, url); }Команды, выполняемые методом
execCommand()
, обычно запускаются кнопками на панели инструментов. Качественный пользовательский интерфейс должен запрещать доступ к кнопкам, если вызываемые ими команды недоступны. Чтобы определить, поддерживается ли некоторая команда браузером, можно передать ее имя методуdocument.queryCommandSupported()
. Вызовом методаdocument.queryCommandEnabled()
можно узнать, доступна ли команда в настоящее время. (Команда, которая выполняет некоторые действия с выделенным текстом, например, может быть недоступна, пока не будет выделен фрагмент текста.) Некоторые команды, такие как «bold» и «italic», могут иметь логическое состояние «включено» или «выключено» в зависимости от наличия выделенного фрагмента текста или местоположения текстового курсора. Как правило, эти команды представлены на панели инструментов кнопками-переключателями. Для определения текущего состояния таких команд можно использовать методdocument.queryCommandState()
. Наконец, некоторые команды, такие как «fontname», ассоциируются с некоторым значением (именем семейства шрифтов). Узнать это значение можно с помощью методаdocument.queryCommandValue()
. Если в текущем выделенном фрагменте используются шрифты двух разных семейств, значение «fontname» будет неопределенным. Для проверки этого случая можно использовать методdocument.queryCommandIndeterm()
.Различные браузеры реализуют различные наборы команд редактирования. Некоторые команды, такие как «bold», «italic», «createlink», «undo» и «redo», поддерживаются всеми браузерами¹. На момент написания этих строк проект стандарта HTML5 определял команды, перечисленные ниже. Однако, поскольку они реализованы пока не во всех браузерах, здесь не будет даваться сколько-нибудь подробное их описание:
bold insertLineBreak selectAll createLink insertOrderedList subscript delete insertUnorderedList superscript formatBlock insertParagraph undo forwardDelete insertText unlink insertlmage italic unselect insertHTML redoЕсли вашему веб-приложению потребуется обеспечить возможность редактирования форматированного текста, то вам, вероятно, лучше обратить внимание на уже реализованные решения, учитывающие различия между браузерами. В Интернете можно найти множество готовых компонентов редакторов². Следует отметить, что функция редактирования, встроенная в браузеры, обладает достаточно широкими возможностями, чтобы дать пользователю возможность вводить небольшие объемы форматированного текста, но она оказывается недостаточно мощной для создания серьезных документов. Обратите внимание, в частности, что разметка HTML, генерируемая этими редакторами, весьма далека от идеала.
После того как пользователь отредактирует содержимое элемента с атрибутом
contenteditable
, можно воспользоваться свойствомinnerHTML
, чтобы получить разметку HTML отредактированного содержимого. Что дальше делать с полученным отформатированным текстом, полностью зависит от вас. Его можно сохранить в скрытом поле формы и отправить вместе с формой на сервер. Непосредственную отправку отредактированного текста на сервер можно выполнить с помощью приемов, описываемых в главе 18. Можно также сохранить результаты редактирования локально, с помощью механизмов, описываемых в главе 20.¹ Список поддерживаемых команд можно найти по адресу http://www.quirksmode.org/dom/execCommand.html.
² Фреймворки YUI и Dojo включают такие компоненты редакторов. Список других решений можно найти на странице http://en.wikipedia.org/wiki/Online_rich-text_editor