diff --git a/1-js/2-first-steps/8-operators/article.md b/1-js/2-first-steps/8-operators/article.md index dcd0d700..e0f20193 100644 --- a/1-js/2-first-steps/8-operators/article.md +++ b/1-js/2-first-steps/8-operators/article.md @@ -112,7 +112,7 @@ var oranges = "3"; alert( +apples + +oranges); // 5, число, оба операнда предварительно преобразованы в числа ``` -С точки зрения математики такое изобилие плюсов может показаться странном. С точки зрения программирования -- никаких разночтений: сначала выполнятся унарные плюсы, приведут строки к числам, а затем -- бинарный `'+'` их сложит. +С точки зрения математики такое изобилие плюсов может показаться странным. С точки зрения программирования -- никаких разночтений: сначала выполнятся унарные плюсы, приведут строки к числам, а затем -- бинарный `'+'` их сложит. Почему унарные плюсы выполнились до бинарного сложения? Как мы сейчас увидим, дело в их приоритете. diff --git a/1-js/3-writing-js/2-coding-style/code-style.svg b/1-js/3-writing-js/2-coding-style/code-style.svg index 64fa7c07..71e1ce4a 100644 --- a/1-js/3-writing-js/2-coding-style/code-style.svg +++ b/1-js/3-writing-js/2-coding-style/code-style.svg @@ -1,8 +1,8 @@ - + code-style.svg - Created with bin/sketchtool. + Created with Sketch. diff --git a/1-js/3-writing-js/3-write-unmain-code/article.md b/1-js/3-writing-js/3-write-unmain-code/article.md index 06fa507c..a9c417ac 100644 --- a/1-js/3-writing-js/3-write-unmain-code/article.md +++ b/1-js/3-writing-js/3-write-unmain-code/article.md @@ -78,12 +78,9 @@ div.append(''); В итоге, после вызова получается два независимых `div'а`: первый содержит `img` (этот неявный клон никуда не присвоен), а второй -- наш `span`. -Злая магия? Плохой феншуй? - -Ничего подобного, просто избирательное следование соглашениям. Вызов `wrap` -- неявно клонирует элемент. - -Такой сюрприз, бесспорно, стоит многих часов отладки. +Объяснения не очень понятны? Написано что-то странное? Это просто разум, привыкший, что соглашения уважаются, не допускает мысли, что вызов `wrap` -- неявно клонирует элемент. Ведь другие jQuery-методы, кроме `clone` этого не делают. +Как говорил [Учитель](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D1%84%D1%83%D1%86%D0%B8%D0%B9): "В древности люди учились для того, чтобы совершенствовать себя. Нынче учатся для того, чтобы удивить других". ## Краткость -- сестра таланта! diff --git a/1-js/5-functions-closures/1-global-object/4-function-and-variable/solution.md b/1-js/5-functions-closures/1-global-object/4-function-and-variable/solution.md deleted file mode 100644 index 33fe8cf2..00000000 --- a/1-js/5-functions-closures/1-global-object/4-function-and-variable/solution.md +++ /dev/null @@ -1,19 +0,0 @@ -Ответ: `5`. - -```js -//+ run untrusted -var a = 5; - -function a() { } - -alert(a); -``` - -Чтобы понять, почему -- разберём внимательно как работает этот код. -
    -
  1. До начала выполнения создаётся переменная `a` и функция `a`. Стандарт написан так, что функция создаётся первой и переменная ее не перезаписывает. То есть, функция имеет приоритет. Но в данном случае это совершенно неважно, потому что... -
  2. -
  3. ...После инициализации, когда код начинает выполняться -- срабатывает присваивание `a = 5`, перезаписывая `a`, и уже не важно, что там лежало.
  4. -
  5. Объявление `Function Declaration` на стадии выполнения игнорируется (уже обработано).
  6. -
  7. В результате `alert(a)` выводит 5.
  8. -
diff --git a/1-js/5-functions-closures/1-global-object/4-function-and-variable/task.md b/1-js/5-functions-closures/1-global-object/4-function-and-variable/task.md deleted file mode 100644 index ee8502d0..00000000 --- a/1-js/5-functions-closures/1-global-object/4-function-and-variable/task.md +++ /dev/null @@ -1,15 +0,0 @@ -# Функция и переменная - -[importance 3] - -Каков будет результат кода? Почему? - -```js -var a = 5; - -function a() { } - -alert(a); -``` - -P.S. Это задание -- учебное, на понимание процесса инициализации и выполнения. В реальной жизни мы, конечно же, не будем называть переменную и функцию одинаково. diff --git a/2-ui/1-document/12-multi-insert/article.md b/2-ui/1-document/12-multi-insert/article.md index e30bc70f..53acc250 100644 --- a/2-ui/1-document/12-multi-insert/article.md +++ b/2-ui/1-document/12-multi-insert/article.md @@ -213,7 +213,7 @@ ul.appendChild(fragment); // вместо фрагмента вставятс - + diff --git a/2-ui/2-events-and-interfaces/4-event-bubbling/article.md b/2-ui/2-events-and-interfaces/4-event-bubbling/article.md index 3c74a2a3..4d490766 100644 --- a/2-ui/2-events-and-interfaces/4-event-bubbling/article.md +++ b/2-ui/2-events-and-interfaces/4-event-bubbling/article.md @@ -69,9 +69,7 @@
  • `event.target` будет содержать ссылку на конкретный элемент внутри формы, самый вложенный, на котором произошёл клик.
  • -[online] [codetabs height=220 src="bubble-target"] -[/online] Возможна и ситуация, когда `event.target` и `this` -- один и тот же элемент, например если в форме нет других тегов и клик был на самом элементе `
    `. @@ -148,11 +146,15 @@ Стадия цели, обозначенная на рисунке цифрой `(2)`, особо не обрабатывается, так как обработчики, назначаемые обоими этими способами, срабатывают также на целевом элементе. +[smart header="Есть события, которые не всплывают, но которые можно перехватить"] +Бывают события, которые можно поймать только на стадии перехвата, а на стадии всплытия -- нельзя.. + +Например, таково событие фокусировки на элементе [onfocus](/focus-blur). Конечно, это большая редкость, такое исключение существует по историческим причинам. +[/smart] + ## Примеры -В примере ниже на `form, div, p` стоят те же обработчики, что и раньше, но на этот раз -- на стадии погружения. - -Чтобы увидеть перехват в действии, кликните на элементе `P`: +В примере ниже на ``, `
    `, `

    ` стоят те же обработчики, что и раньше, но на этот раз -- на стадии погружения. Чтобы увидеть перехват в действии, кликните в нём на элементе `

    `: [codetabs height=220 src="capture"] @@ -180,20 +182,14 @@ for(var i=0; i`, чтобы увидеть порядок прохода события: [codetabs height=220 src="both"] -Должно быть `FORM` -> `DIV` -> `P` -> `P` -> `DIV` -> `FORM`. Заметим, что элемент `P` участвует в обоих стадиях. +Должно быть `FORM` -> `DIV` -> `P` -> `P` -> `DIV` -> `FORM`. Заметим, что элемент `

    ` участвует в обоих стадиях. Как видно из примера, один и тот же обработчик можно назначить на разные стадии. При этом номер текущей стадии он, при необходимости, может получить из свойства `event.eventPhase` (=1, если погружение, =3, если всплытие). -[smart header="Есть события, которые не всплывают, но которые можно перехватить"] -Бывают события, которые можно поймать только на стадии перехвата, а на стадии всплытия -- нельзя.. - -Например, таково событие фокусировки на элементе [onfocus](/focus-blur). Конечно, это большая редкость, такое исключение существует по историческим причинам. -[/smart] - ## Отличия IE8- diff --git a/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.view/index.html b/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.view/index.html index 530a64aa..bdeec1b6 100755 --- a/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.view/index.html +++ b/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.view/index.html @@ -27,7 +27,7 @@ Shift+Клик добавляет промежуток от последнего diff --git a/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/source.view/index.html b/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/source.view/index.html index b9607c6a..e9d21138 100755 --- a/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/source.view/index.html +++ b/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/source.view/index.html @@ -26,22 +26,8 @@ Shift+Клик добавляет промежуток от последнего diff --git a/2-ui/3-event-details/5-drag-and-drop-objects/dragDemo.view/index.html b/2-ui/3-event-details/5-drag-and-drop-objects/dragDemo.view/index.html index 29767e28..0a85b121 100644 --- a/2-ui/3-event-details/5-drag-and-drop-objects/dragDemo.view/index.html +++ b/2-ui/3-event-details/5-drag-and-drop-objects/dragDemo.view/index.html @@ -2,7 +2,7 @@ - + - @@ -27,38 +27,39 @@ function Voter(options) { var elem = options.elem; - var voteElem = elem.find('.vote'); + var voteElem = elem.querySelector('.vote'); - elem.on('click', '.down', onDownClick) - .on('click', '.up', onUpClick) - .on('mousedown selectstart', false); + elem.onclick = function(event) { + // сам обработчик не меняет голос, он вызывает функцию + if (event.target.closest('.down')) { + voteDecrease(); + } else if (event.target.closest('.up')) { + voteIncrease(); + } + } + + elem.onmousedown = function() { + return false; + }; // ----------- методы ------------- - function onDownClick() { - voteDecrease(); // сам обработчик не меняет голос, он вызывает функцию - } - - function onUpClick() { - voteIncrease(); - } - function voteDecrease() { - voteElem.html( +voteElem.html()-1 ); + voteElem.innerHTML = +voteElem.innerHTML - 1; } function voteIncrease() { - voteElem.html( +voteElem.html()+1 ); + voteElem.innerHTML = +voteElem.innerHTML + 1; } this.setVote = function(vote) { - voteElem.html( +vote ); + voteElem.innerHTML = +vote; }; } var voter = new Voter({ - elem: $('#voter') + elem: document.getElementById('voter') }); voter.setVote(1); diff --git a/2-ui/5-widgets/2-widgets-structure/4-voter/source.view/index.html b/2-ui/5-widgets/2-widgets-structure/4-voter/source.view/index.html index 297b8c47..3fb7c8f3 100755 --- a/2-ui/5-widgets/2-widgets-structure/4-voter/source.view/index.html +++ b/2-ui/5-widgets/2-widgets-structure/4-voter/source.view/index.html @@ -2,6 +2,7 @@ + - @@ -29,7 +29,7 @@ function Voter(options) { } var voter = new Voter({ - elem: $('#voter') + elem: document.getElementById('voter') }); voter.setVote(1); diff --git a/2-ui/5-widgets/2-widgets-structure/4-voter/task.md b/2-ui/5-widgets/2-widgets-structure/4-voter/task.md index da06bd5f..490f0bed 100644 --- a/2-ui/5-widgets/2-widgets-structure/4-voter/task.md +++ b/2-ui/5-widgets/2-widgets-structure/4-voter/task.md @@ -15,7 +15,7 @@ По клику на `+` и `—` число должно увеличиваться или уменьшаться. -**Публичный метод `voter.setVote(vote)` должен устанавливать текущее число -- значение голоса.** +Публичный метод `voter.setVote(vote)` должен устанавливать текущее число -- значение голоса. Все остальные методы и свойства пусть будут приватными. diff --git a/2-ui/5-widgets/2-widgets-structure/5-voter-proto/solution.view/index.html b/2-ui/5-widgets/2-widgets-structure/5-voter-proto/solution.view/index.html index 3aa85a15..0f24a0f5 100755 --- a/2-ui/5-widgets/2-widgets-structure/5-voter-proto/solution.view/index.html +++ b/2-ui/5-widgets/2-widgets-structure/5-voter-proto/solution.view/index.html @@ -3,8 +3,8 @@ - - + + @@ -17,7 +17,7 @@ diff --git a/2-ui/5-widgets/2-widgets-structure/5-voter-proto/solution.view/voter.js b/2-ui/5-widgets/2-widgets-structure/5-voter-proto/solution.view/voter.js index 80f3096b..1341b1b5 100755 --- a/2-ui/5-widgets/2-widgets-structure/5-voter-proto/solution.view/voter.js +++ b/2-ui/5-widgets/2-widgets-structure/5-voter-proto/solution.view/voter.js @@ -1,22 +1,33 @@ function Voter(options) { var elem = this._elem = options.elem; - this._voteElem = elem.find('.vote'); + this._voteElem = elem.querySelector('.vote'); - elem.on('mousedown selectstart', false); + elem.onmousedown = function() { + return false; + }; - elem.on('click', '.down', this._onDownClick.bind(this)); - elem.on('click', '.up', this._onUpClick.bind(this)); + elem.onclick = this._onClick.bind(this); } -Voter.prototype._onDownClick = function() { - this._voteElem.html( +this._voteElem.html() - 1 ); +Voter.prototype._onClick = function(event) { + if (event.target.closest('.down')) { + this._voteDecrease(); + } else if (event.target.closest('.up')) { + this._voteIncrease(); + } }; -Voter.prototype._onUpClick = function() { - this._voteElem.html( +this._voteElem.html() + 1 ); + +Voter.prototype._voteIncrease = function() { + this._voteElem.innerHTML = +this._voteElem.innerHTML + 1; +}; + +Voter.prototype._voteDecrease = function() { + this._voteElem.innerHTML = +this._voteElem.innerHTML - 1; }; Voter.prototype.setVote = function(vote) { - this._voteElem.html(vote); + this._voteElem.innerHTML = +vote; }; + diff --git a/2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.md b/2-ui/5-widgets/2-widgets-structure/6-voter-add-step/solution.md similarity index 100% rename from 2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.md rename to 2-ui/5-widgets/2-widgets-structure/6-voter-add-step/solution.md diff --git a/2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.view/index.html b/2-ui/5-widgets/2-widgets-structure/6-voter-add-step/solution.view/index.html old mode 100755 new mode 100644 similarity index 71% rename from 2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.view/index.html rename to 2-ui/5-widgets/2-widgets-structure/6-voter-add-step/solution.view/index.html index 86a48b29..49f27232 --- a/2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.view/index.html +++ b/2-ui/5-widgets/2-widgets-structure/6-voter-add-step/solution.view/index.html @@ -3,8 +3,8 @@ - - + + @@ -18,7 +18,7 @@ - - - - - -

    - - 0 - + -
    - - - - - - diff --git a/2-ui/5-widgets/2-widgets-structure/6-voter-colored/solution.view/style.css b/2-ui/5-widgets/2-widgets-structure/6-voter-colored/solution.view/style.css deleted file mode 100755 index 16870974..00000000 --- a/2-ui/5-widgets/2-widgets-structure/6-voter-colored/solution.view/style.css +++ /dev/null @@ -1,18 +0,0 @@ - -.voter { - font-family: Consolas, "Lucida Console", monospace; - font-size: 18px; -} -.up, .down { - cursor: pointer; - color: blue; - font-weight: bold; -} - -.positive { - color: green; -} - -.negative { - color: red; -} \ No newline at end of file diff --git a/2-ui/5-widgets/2-widgets-structure/6-voter-colored/solution.view/voter.js b/2-ui/5-widgets/2-widgets-structure/6-voter-colored/solution.view/voter.js deleted file mode 100755 index db936fb1..00000000 --- a/2-ui/5-widgets/2-widgets-structure/6-voter-colored/solution.view/voter.js +++ /dev/null @@ -1,28 +0,0 @@ - -function Voter(options) { - var elem = this._elem = options.elem; - this._voteElem = elem.find('.vote'); - this._vote = 0; - - elem.on('mousedown selectstart', false); - - elem.on('click', '.down', this._onDownClick.bind(this)); - elem.on('click', '.up', this._onUpClick.bind(this)); -} - -Voter.prototype._onDownClick = function() { - this.setVote(this._vote - 1); -}; - -Voter.prototype._onUpClick = function() { - this.setVote(this._vote + 1); -}; - -Voter.prototype._renderVote = function() { - this._voteElem.html(this._vote); -}; - -Voter.prototype.setVote = function(vote) { - this._vote = vote; - this._renderVote(); -}; diff --git a/2-ui/5-widgets/2-widgets-structure/6-voter-colored/task.md b/2-ui/5-widgets/2-widgets-structure/6-voter-colored/task.md deleted file mode 100644 index 6c8156ba..00000000 --- a/2-ui/5-widgets/2-widgets-structure/6-voter-colored/task.md +++ /dev/null @@ -1,17 +0,0 @@ -# Добавить цвет в голосовалку - -[importance 5] - -Создайте функцию-конструктор `ColoredVoter`, которая наследует от голосовалки, созданной в задаче [](/task/voter-proto) и отображает положительные значения зелёным, а отрицательные -- красным. - -Результат работы `new ColoredVoter`: (проголосуйте, чтобы увидеть): -[iframe border=1 src="solution"] - -Решение задачи состоит из двух этапов: -
      -
    1. Внести изменения в `Voter`, вынести логику отображения голоса в защищенный метод `_renderVote`, чтобы его можно было отнаследовать. При необходимости добавьте другие методы и свойства. Делайте такой код, который будет удобно расширять.
    2. -
    3. Отнаследовать и переопределить `_renderVote` в `ColoredVoter`.
    4. -
    - - -В качестве исходного кода используйте решение задачи [](/task/voter-proto). diff --git a/2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.view/step-voter.js b/2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.view/step-voter.js deleted file mode 100755 index b474294b..00000000 --- a/2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.view/step-voter.js +++ /dev/null @@ -1,13 +0,0 @@ -function StepVoter(options) { - Voter.apply(this, arguments); - this._step = options.step || 1; -} -StepVoter.prototype = Object.create(Voter.prototype); - -StepVoter.prototype._increase = function() { - this.setVote(this._vote + this._step); -}; - -StepVoter.prototype._decrease = function() { - this.setVote(this._vote - this._step); -}; \ No newline at end of file diff --git a/2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.view/voter.js b/2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.view/voter.js deleted file mode 100755 index 0d78d91f..00000000 --- a/2-ui/5-widgets/2-widgets-structure/7-voter-add-doublevote/solution.view/voter.js +++ /dev/null @@ -1,36 +0,0 @@ - -function Voter(options) { - var elem = this._elem = options.elem; - this._voteElem = elem.find('.vote'); - this._vote = 0; - - elem.on('mousedown selectstart', false); - - elem.on('click', '.down', this._onDownClick.bind(this)); - elem.on('click', '.up', this._onUpClick.bind(this)); -} - -Voter.prototype._onDownClick = function() { - this._decrease(); -}; - -Voter.prototype._onUpClick = function() { - this._increase(); -}; - -Voter.prototype._renderVote = function() { - this._voteElem.html(this._vote); -}; - -Voter.prototype.setVote = function(vote) { - this._vote = vote; - this._renderVote(); -}; - -Voter.prototype._increase = function() { - this.setVote(this._vote + 1); -}; - -Voter.prototype._decrease = function() { - this.setVote(this._vote - 1); -}; diff --git a/2-ui/5-widgets/2-widgets-structure/article.md b/2-ui/5-widgets/2-widgets-structure/article.md index 21b8d904..68ee24dd 100644 --- a/2-ui/5-widgets/2-widgets-structure/article.md +++ b/2-ui/5-widgets/2-widgets-structure/article.md @@ -4,7 +4,7 @@ Здесь мы сосредоточимся на графических компонентах, которые также называют "виджетами". -В браузерах есть встроенные виджеты, например `` и другие элементы, о которых мы даже и не думаем, как они работают. Просто работают: принимают значение, вызывают события... +В браузерах есть встроенные виджеты, например `` и другие элементы, о которых мы даже и не думаем, "как они работают". Они "просто работают": показывают значение, вызывают события... Наша задача -- сделать то же самое на уровне выше. Мы будем создавать объекты, которые генерируют меню, диалог или другие компоненты интерфейса, и дают возможность удобно работать с ними. @@ -27,38 +27,44 @@ Далее она может дополняться, изменяться, но в начале -- она такая. -Обратим внимание на важные соглашения: +Обратим внимание на важные соглашения виджета:
    Вся разметка заключена в корневой элемент `
    Это очень удобно: вынул этот элемент из DOM -- нет меню, вставил в другое место -- переместил меню. Кроме того, можно удобно искать подэлементы.
    -
    В разметке -- только классы.
    +
    Внутри корневого элемента -- только классы, не `id`.
    Документ вполне может содержать много различных меню. Они не должны конфликтовать между собой, поэтому для разметки везде используются классы. Исключение -- корневой элемент. В данном случае мы предполагаем, что данное конкретное "меню сладостей" в документе только одно, поэтому даём ему `id`.
    -Для работы с разметкой будем создавать объект `new Menu` и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики. + +# Класс виджета + +Для работы с разметкой будем создавать объект `new Menu` и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики: ```js function Menu(options) { var elem = options.elem; - elem.on('mousedown selectstart', false); + elem.onmousedown = function() { return false; } + + elem.onclick = function(event) { + if (event.target.closest('.title')) { + elem.classList.toggle('open'); + } + }; - elem.on('click', '.title', function() { - elem.toggleClass('open'); - }); } // использование var menu = new Menu({ - elem: $('#sweets-menu') + elem: document.getElementById('sweets-menu') }); ``` Меню: -[codetabs src="menu-1"] +[codetabs src="menu"] Это, конечно, только первый шаг, но уже здесь видны некоторые важные соглашения в коде. @@ -69,25 +75,38 @@ var menu = new Menu({
    Вместо того, чтобы найти элемент и поставить обработчик на него: ```js -var titleElem = elem.find('.title'); +var titleElem = elem.querySelector('.title'); -titleElem.on('click', function() { - elem.toggleClass('open'); +titleElem.onclick = function() { + elem.classList.toggle('open'); } ``` -...Мы пишем так: +...Мы ставим обработчик на корневой `elem` и используем делегирование: ```js -elem.on('click', '.title', function() { - elem.toggleClass('open'); -}); +elem.onclick = function(event) { + if (event.target.closest('.title')) { + elem.classList.toggle('open'); + } +}; ``` -Это ускоряет инициализацию, так как не надо искать элементы, и даёт возможность в любой момент менять DOM внутри, в том числе через `innerHTML`, без необходимости переставлять обработчика. +Это ускоряет инициализацию, так как не надо искать элементы, и даёт возможность в любой момент менять DOM внутри, в том числе через `innerHTML`, без необходимости переставлять обработчик.
    +В этот код лучше добавить дополнительную проверку на то, что найденный `.title` находится внутри `elem`: + +```js +elem.onclick = function(event) { + var closestTitle = event.target.closest('.title'); + if (closestTitle && elem.contains(closestTitle)) { + elem.classList.toggle('open'); + } +}; +``` + ## Публичные методы Уважающий себя компонент обычно имеет публичные методы, которые позволяют управлять им снаружи. @@ -95,12 +114,12 @@ elem.on('click', '.title', function() { Рассмотрим повнимательнее этот фрагмент: ```js -elem.on('click', '.title', function() { - elem.toggleClass('open'); -}); +if (event.target.closest('.title')) { + elem.classList.toggle('open'); +} ``` -Здесь в обработчике события сразу код работы с элементами. Пока одна строка -- всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: "А что же, всё-таки, такое делается при клике?" +Здесь в обработчике события сразу код работы с элементом. Пока одна строка -- всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: "А что же, всё-таки, такое делается при клике?" Для улучшения читаемости выделим обработчик в отдельную функцию `toggle`, которая к тому же станет полезным публичным методом: @@ -108,49 +127,42 @@ elem.on('click', '.title', function() { function Menu(options) { var elem = options.elem; - elem.on('mousedown selectstart', false); + elem.onmousedown = function() { return false; } -*!* - elem.on('click', '.title', onTitleClick); - - function onTitleClick(e) { - toggle(); - } + elem.onclick = function(event) { + if (event.target.closest('.title')) { + toggle(); + } + }; function toggle() { - elem.toggleClass('open'); - }; -*/!* + elem.classList.toggle('open'); + } this.toggle = toggle; } ``` -Здесь и сам обработчик события тоже вынесен в отдельную функцию `onTitleClick`. - -Наши бонусы: -
      -
    1. Во-первых, стало проще найти и расширить обработчик события в коде -- имя `onTitleClick` найти и запомнить.
    2. -
    3. Во-вторых, код стал лучше читаться.
    4. -
    5. Во-третьих, `toggle` теперь -- отдельная функция, доступная извне.
    6. -
    - -Пример использования публичного метода: +Теперь метод `toggle` можно использовать и снаружи: ```js var menu = new Menu(...); menu.toggle(); ``` -## Генерация DOM-дерева +## Генерация DOM-элемента -До этого момента меню "оживляло" уже существующий HTML. Но в более сложном интерфейсе нужно уметь сгенерировать меню "на лету", по данным. +До этого момента меню "оживляло" уже существующий HTML. -Для этого добавим меню три метода: +Но далеко не всегда в HTML уже есть готовая разметка. В сложных интерфейсах намного чаще её нет, а есть данные, на основе которых компонент генерирует разметку. + +В случае меню, данные -- это набор пунктов меню, которые передаются конструктору. + +Для генерации DOM добавим меню три метода:
      -
    • `render()` -- генерирует корневой DOM-элемент и заголовок меню, приватный.
    • -
    • `renderItems()` -- генерирует DOM для списка опций (`
    • `), приватный.
    • -
    • `getElem()` -- возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный.
    • +
    • `render()` -- генерирует корневой DOM-элемент и заголовок меню.
    • +
    • `renderItems()` -- генерирует DOM для списка опций `ul/li`.
    • +
    • `getElem()` -- возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный метод.
    Функция генерации корневого элемента с заголовком `render` отделена от генерации списка `renderItems`. Почему -- будет видно чуть далее. @@ -173,14 +185,14 @@ var menu = new Menu({ }); *!* -// получить DOM-элемент меню +// получить сгенерированный DOM-элемент меню */!* var elem = menu.getElem(); *!* // вставить меню в нужное место страницы */!* -$('#sweets-menu-holder').append( elem ); +document.body.appendChild(elem); ``` Код `Menu` с новыми методами: @@ -195,68 +207,52 @@ function Menu(options) { } function render() { - elem = $(''); - elem.append( $('', { class: "title", text: options.title })) + elem = document.createElement('div'); + elem.className = "menu"; - elem.on('mousedown selectstart', false); + var titleElem = document.createElement('span'); + elem.appendChild(titleElem); + titleElem.className = "title"; + titleElem.textContent = options.title; + + elem.onmousedown = function() { + return false; + }; + + elem.onclick = function(event) { + if (event.target.closest('.title')) { + toggle(); + } + } - elem.on('click', '.title', onTitleClick); } function renderItems() { var items = options.items || []; - var list = $('
      '); - $.each(items, function(i, item) { - list.append( $('
    • ').text(item) ); - }) - list.appendTo(elem); + var list = document.createElement('ul'); + items.forEach(function(item) { + var li = document.createElement('li'); + li.textContent = item; + list.appendChild(li); + }); + elem.appendChild(list); } - // ... -} -``` - -**Важнейший принцип, который здесь использован -- ленивость.** - -Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда `new Menu` создаётся, то переменная `elem` лишь объявляется. DOM-дерево будет сгенерировано только при вызове `getElem()`. - -Более того! Пока меню закрыто -- достаточно заголовка. Кроме того, возможно, посетитель вообще никогда не раскроет это меню, так зачем генерировать список раньше времени? - -**Фаза инициализации очень чувствительна к производительности, так как при загрузке страницы со сложным интерфейсом создаётся много всего. А мы хотим, чтобы он начал работать как можно быстрее.** - -Если изначально подходить к оптимизации на этой фазе "спустя рукава", то потом поправить может быть сложно. Всё-таки, инициализация -- это фундамент, начало работы виджета. Конечно, лучше без фанатизма. Бывают ситуации, когда по коду гораздо удобнее что-то сделать сразу, поэтому нужен взвешенный подход. Чем крупнее участок работы и чем больше шансов его вообще избежать -- тем больше доводов его отложить. - -Ниже -- код меню с методами `open`, `close` и `toggle`, которые подразумевают ленивую генерацию DOM: - -```js -function Menu(options) { - var elem; - - function getElem() { /* см выше */ } - - function render() { /* см выше */ } - - function renderItems() { /* см выше */ } - - function onTitleClick(e) { /* см выше */ } - -*!* function open() { - if (!elem.find('ul').length) { + if (!elem.querySelector('ul')) { renderItems(); } - elem.addClass('open'); + elem.classList.add('open'); }; function close() { - elem.removeClass('open'); + elem.classList.remove('open'); }; function toggle() { - if (elem.hasClass('open')) close(); + if (elem.classList.contains('open')) close(); else open(); }; -*/!* this.getElem = getElem; this.toggle = toggle; @@ -265,23 +261,49 @@ function Menu(options) { } ``` -Основные изменения -- теперь метод `toggle` не просто меняет класс. Этого недостаточно, ведь, чтобы открыть меню, нужно для начала отрендерить его опции. Поэтому добавлено два метода `open` и `close`, которые также полезны и для внешнего интерфейса. +Отметим некоторые особенности этого кода. + +
      +
      Обработчики отделяются от реальных действий.
      +
      В обработчике `onclick` мы "ловим" событие и выясняем, что именно произошло. Возможно, нужно проверить `event.target`, координаты, клавиши-модификаторы, и т.п. Это всё можно делать здесь же. + +Выяснив, что нужно сделать, обработчик `onclick` не делает это сам, а вызывает для этого соответствующий метод. Этот метод уже не знает ничего о событии, он просто делает что-то с виджетом. Его можно вызвать и отдельно, не из обработчика. + +Здесь есть ряд важных плюсов: +
        +
      • Обработчик `onclick` не "распухает" чрезмерно.
      • +
      • Код гораздо лучше читается.
      • +
      • Метод можно повторно использовать, в том числе и сделать публичным, как в коде выше.
      • +
      +
      +
      Генерация DOM, по возможности, должна быть "ленивой".
      +
      Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда `new Menu` создаётся, то переменная `elem` лишь объявляется. DOM-дерево будет сгенерировано только при вызове `getElem()` функцией `render()`. + +Более того! Пока меню закрыто -- достаточно заголовка. Кроме того, возможно, посетитель вообще никогда не раскроет это меню, так зачем генерировать список раньше времени? А при первом открытиии `open()` вызовет функцию `renderItems()`, которая специально для этого выделена отдельно от `render()`. + +**Фаза инициализации очень чувствительна к производительности, так как обычно в сложном интерфейсе создаётся много всего.** + +Если изначально подходить к оптимизации на этой фазе "спустя рукава", то потом поправить долгий старт может быть сложно. Тем более, что инициализация -- это фундамент, начало работы виджета, её оптимизация в будущем может потребовать сильных изменений кода. + +Конечно, здесь, как и везде в оптимизации -- без фанатизма. Бывают ситуации, когда гораздо удобнее что-то сделать сразу. Если это один элемент, то оптимизация здесь ни к чему. А если большой фрагмент DOM, который, как в случае с меню, прямо сейчас не нужен -- то лучше отложить. +
      +
      В действии: -[codetabs src="menu-3-elem" height="200"] +[codetabs src="menu-dom" height="200"] ## Итого -Мы начали создавать компонент "с чистого листа", пока без дополнительных библиотек, но они скоро понадобятся. +Мы начали создавать компонент "с чистого листа", пока без дополнительных библиотек. Основные принципы:
        -
      • В конструктор передаётся объект аргументов `options`, а не список аргументов -- для удобства дополнения и расширения виджета.
      • +
      • Виджет -- это объект, который либо контролирует готовое дерево DOM, либо создаёт своё.
      • +
      • В конструктор виджета передаётся объект аргументов `options`.
      • +
      • Виджет при необходимости создаёт элемент или "оживляет" готовый. Внутре в разметке не используются `id`.
      • Обработчики назначаются через делегирование -- для производительности и упрощения виджета.
      • -
      • Не экономим буквы ценой понятности -- действие и/или обработчик заслуживают быть отдельными функциями.
      • -
      • Будем ленивыми -- если существенный участок работы можно отложить до реального задействования виджета -- откладываем его.
      • +
      • Обработчики событий вызывают соответствующий метод, не пытаются делать всё сами.
      • +
      • При инициализации, если существенный участок работы можно отложить до реального задействования виджета -- откладываем его.
      -Далее мы продолжим работать со разметкой виджета. - diff --git a/2-ui/5-widgets/2-widgets-structure/menu-1.view/menu.js b/2-ui/5-widgets/2-widgets-structure/menu-1.view/menu.js deleted file mode 100755 index a9bb71ae..00000000 --- a/2-ui/5-widgets/2-widgets-structure/menu-1.view/menu.js +++ /dev/null @@ -1,11 +0,0 @@ -function Menu(options) { - var elem = options.elem; - - // отмена выделения при клике на меню - elem.on('mousedown selectstart', false); - - elem.on('click', '.title', function() { - elem.toggleClass('open'); - }); - -} diff --git a/2-ui/5-widgets/2-widgets-structure/menu-1.view/style.css b/2-ui/5-widgets/2-widgets-structure/menu-1.view/style.css deleted file mode 100755 index b9859671..00000000 --- a/2-ui/5-widgets/2-widgets-structure/menu-1.view/style.css +++ /dev/null @@ -1,20 +0,0 @@ - -.menu ul { - display: none; - margin: 0; -} - -.menu .title { - font-weight: bold; - cursor: pointer; - background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat; - padding-left: 18px; -} - -.menu.open ul { - display: block; -} - -.menu.open .title { - background-image: url(https://js.cx/clipart/arrow-down.png); -} \ No newline at end of file diff --git a/2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/menu.js b/2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/menu.js deleted file mode 100755 index d1c4851c..00000000 --- a/2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/menu.js +++ /dev/null @@ -1,51 +0,0 @@ -function Menu(options) { - var elem; - - function getElem() { - if (!elem) render(); - return elem; - } - - function render() { - elem = $(''); - elem.append( $('', { class: "title", text: options.title })) - - elem.on('mousedown selectstart', false); - - elem.on('click', '.title', onTitleClick); - } - - function renderItems() { - var items = options.items || []; - var list = $('
        '); - $.each(items, function(i, item) { - list.append( $('
      • ').text(item) ); - }) - list.appendTo(elem); - } - - function onTitleClick(e) { - toggle(); - } - - function open() { - if (!elem.find('ul').length) { - renderItems(); - } - elem.addClass('open'); - }; - - function close() { - elem.removeClass('open'); - }; - - function toggle() { - if (elem.hasClass('open')) close(); - else open(); - }; - - this.getElem = getElem; - this.toggle = toggle; - this.close = close; - this.open = open; -} \ No newline at end of file diff --git a/2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/style.css b/2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/style.css deleted file mode 100755 index 50c407a2..00000000 --- a/2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/style.css +++ /dev/null @@ -1,20 +0,0 @@ - -.menu ul { - display: none; - margin: 0; -} - -.menu .title { - font-weight: bold; - cursor: pointer; - background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat; - padding-left: 18px; -} - -.menu.open ul { - display: block; -} - -.menu.open .title { - background-image: url(https://js.cx/clipart/arrow-down.png); -} diff --git a/2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/index.html b/2-ui/5-widgets/2-widgets-structure/menu-dom.view/index.html old mode 100755 new mode 100644 similarity index 77% rename from 2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/index.html rename to 2-ui/5-widgets/2-widgets-structure/menu-dom.view/index.html index 95e460f7..4fabe959 --- a/2-ui/5-widgets/2-widgets-structure/menu-3-elem.view/index.html +++ b/2-ui/5-widgets/2-widgets-structure/menu-dom.view/index.html @@ -3,7 +3,8 @@ - + + @@ -12,9 +13,6 @@ -
        - - diff --git a/2-ui/5-widgets/2-widgets-structure/menu-dom.view/menu.js b/2-ui/5-widgets/2-widgets-structure/menu-dom.view/menu.js new file mode 100644 index 00000000..99b3fd21 --- /dev/null +++ b/2-ui/5-widgets/2-widgets-structure/menu-dom.view/menu.js @@ -0,0 +1,61 @@ +function Menu(options) { + var elem; + + function getElem() { + if (!elem) render(); + return elem; + } + + function render() { + elem = document.createElement('div'); + elem.className = "menu"; + + var titleElem = document.createElement('span'); + elem.appendChild(titleElem); + titleElem.className = "title"; + titleElem.textContent = options.title; + + elem.onmousedown = function() { + return false; + }; + + elem.onclick = function(event) { + if (event.target.closest('.title')) { + toggle(); + } + } + + } + + function renderItems() { + var items = options.items || []; + var list = document.createElement('ul'); + items.forEach(function(item) { + var li = document.createElement('li'); + li.textContent = item; + list.appendChild(li); + }); + elem.appendChild(list); + } + + function open() { + if (!elem.querySelector('ul')) { + renderItems(); + } + elem.classList.add('open'); + }; + + function close() { + elem.classList.remove('open'); + }; + + function toggle() { + if (elem.classList.contains('open')) close(); + else open(); + }; + + this.getElem = getElem; + this.toggle = toggle; + this.close = close; + this.open = open; +} \ No newline at end of file diff --git a/2-ui/5-widgets/2-widgets-structure/menu-dom.view/style.css b/2-ui/5-widgets/2-widgets-structure/menu-dom.view/style.css new file mode 100644 index 00000000..73515917 --- /dev/null +++ b/2-ui/5-widgets/2-widgets-structure/menu-dom.view/style.css @@ -0,0 +1,24 @@ + +.menu ul { + display: none; + margin: 0; +} + +.menu .title { + font-weight: bold; + cursor: pointer; +} + +.menu .title:before { + content: '▶'; + padding-right: 6px; + color: green; +} + +.menu.open ul { + display: block; +} + +.menu.open .title:before { + content: '▼'; +} \ No newline at end of file diff --git a/2-ui/5-widgets/2-widgets-structure/menu-1.view/index.html b/2-ui/5-widgets/2-widgets-structure/menu.view/index.html old mode 100755 new mode 100644 similarity index 71% rename from 2-ui/5-widgets/2-widgets-structure/menu-1.view/index.html rename to 2-ui/5-widgets/2-widgets-structure/menu.view/index.html index 658af91a..e1e779d7 --- a/2-ui/5-widgets/2-widgets-structure/menu-1.view/index.html +++ b/2-ui/5-widgets/2-widgets-structure/menu.view/index.html @@ -3,7 +3,7 @@ - + @@ -20,8 +20,8 @@
    diff --git a/2-ui/5-widgets/2-widgets-structure/menu.view/menu.js b/2-ui/5-widgets/2-widgets-structure/menu.view/menu.js new file mode 100644 index 00000000..f705ef41 --- /dev/null +++ b/2-ui/5-widgets/2-widgets-structure/menu.view/menu.js @@ -0,0 +1,12 @@ +function Menu(options) { + var elem = options.elem; + + elem.onmousedown = function() { return false; } + + elem.onclick = function(event) { + if (event.target.closest('.title')) { + elem.classList.toggle('open'); + } + }; + +} diff --git a/2-ui/5-widgets/2-widgets-structure/menu.view/style.css b/2-ui/5-widgets/2-widgets-structure/menu.view/style.css new file mode 100644 index 00000000..73515917 --- /dev/null +++ b/2-ui/5-widgets/2-widgets-structure/menu.view/style.css @@ -0,0 +1,24 @@ + +.menu ul { + display: none; + margin: 0; +} + +.menu .title { + font-weight: bold; + cursor: pointer; +} + +.menu .title:before { + content: '▶'; + padding-right: 6px; + color: green; +} + +.menu.open ul { + display: block; +} + +.menu.open .title:before { + content: '▼'; +} \ No newline at end of file diff --git a/figures.sketch b/figures.sketch index d6ac3b59..03f08b83 100644 Binary files a/figures.sketch and b/figures.sketch differ