Букмарклеты
Всем привет, сегодня речь пойдет про использование букмарклета, или закладки для браузера.
Кто не знает, это такая штука, которую можно добавить в закладки (да, я сегодня дебютирую в роли Капитана Очевидность :) и, при нажатии на нее, произвести какой-нибудь эффект.
Примером может служить герой сегодняшней заметки, который расположен по адресу http://ulizko. com/demo/allthat/. Инструкция по применению:
- Перетащите ссылку «link» на панель закладок или щелкните по ней правой кнопкой мыши и выберите пункт меню «добавить в избранное».
- Зайдите на какой-нибудь сайт, вроде http://twitter. com, и нажмите на эту закладку (ну или на избранное).
Появится окошко, в которое можно ввести данные. Вообще, предполагается, что это будет интерфейс добавления желаний в вишлисты (предварительно созданные на каком-то сайте), настроить триггеры оповещений, и прочее. Есть даже какая-то валидация начального уровня. И налажен обмен данными с сервером — то есть, на любом домене к вам приходит список ваших вишлистов, а ваше новое желание с любого домена долетит на крыльях любви к вишлисту и уютно устроится в его объятьях1.
Но. Мы сегодня не об этом, а о том, как делать такие штуки в принципе.
Прежде чем перейти непосредственно к разбору кода, хотелось бы ответить на вопрос (который мне никто не задавал :), а именно, "Какие возможности дает букмарклет?". Правильный ответ — любые. Так как мы получаем возможность подгрузить любой скрипт, мы можем сделать с клиентской страничкой все, что угодно. Например — сделать «выносной» виджет, в котором на любой страничке можно будет добавить запись в блокнот или таскменджер. Или вообще сделать весь таскменеджер выносным. Что тоже важно, они будут работать практически везде — это не плагины к firefoxу и не виджеты к opera. Букмарклетам не важно (ну, почти :), какая у вас ОС или браузер. В общем, есть простор для фантазии.
Итак, как же делать эти самые букмарклеты?
Очень просто: надо создать на страничке элемент anchor с атрибутом href, содержащим javascript-код. Если перевести на русский, то надо сделать вот такую ссылку, адрес которой, по большому счету, и будет букмарклетом:
<a href="javascript:alert('I am bookmarklet'); void 0;">Bookmarklet</a>
Демо:
Демо вышеописанного кодаДля того, чтобы javascript код в адресе ссылки заработал, надо добавить перед ним слово javascript:. По умному это называется «указание псевдопротокола javascript». Еще одна важная деталь — если ваш код вернет какое-то значение, то браузер воспримет его в качестве адреса, по которому нужно перейти, и уйдет с текущей страницы. Чтобы избежать этого, не возвращайте значения, то есть допишите в конец скрипта void 0;, либо оберните весь код в анонимную функцию, невозвращающую значения — (function(){... ваш код мог бы быть здесь...})().
В любом случае, все эти вопросы подробно рассмотрены у Ильи Кантора в его заметке Букмарклеты и правила их написания, к которой я вас и отсылаю за подробностями.
Единственную вещь, которую нам еще нужно знать — это то, что все браузеры ограничивают максимальную длину кода букмарклета. И, подобно тому, как скорость каравана равна скорости самого медленного верблюда, так и максимальный размер кроссбраузерного букмарклета равен ограничению, наложенному IE 6 SP2, то есть, 488 символам.
Таким образом, вряд ли мы сможем закодить какую-то комплексную логику в неполных пятистах символах, так что чаще всего букмарклеты просто создают новый тэг script, в который уже сгружают код приложения.
Так поступил и я. Вот код моего букмарклета в человекоадаптированном виде:
1 (function () {
2 // создаем новую внутреннюю переменную a (лучше в данном случае использовать короткие идентификаторы)
3 // и сразу же добавляем свой объект в глобальный объект window, и записываем в него данные, которые уникальны
4 // для каждого пользователя (ведь они сгенерированы сервером для пользователя перед тем, как он добавил этот букмарклет к себе)
5 var a = window.allThat = {
6 userId : '123345456',
7 server : 'http://mysite.com/',
8 script : document.createElement('script'), // создадим и запомним тэг скрипт,
9 // который сгрузит нам код нашего приложения - мы его потом удалим, если пользователь нажмет кнопку "закрыть"
10 css : document.createElement('link')
11 },
12 /* динамически создаем элементы: */
13 h = document.getElementsByTagName('head')[0];
14 a.css.rel = 'stylesheet';
15 a.css.href = a.server + 'css/bookmarklet.2.css';
16 h.appendChild(a.css);
17 a.script.src = a.server + 'js/bookmarklet.7.js';
18 h.appendChild(a.script);
19 h=null;
20 })();
Потом подгружается непосредственно код самого окошка. Думаю, он может представлять некий интерес сам по себе, так что и его я сюда запощу (все комментарии идут на английском, так как заказчик американец):
1 (function () {
2 var Dom = {
3 get : function (el) {
4 return (el && el.nodeType) ? el : document.getElementById(el);
5 },
6 addListener : function (el, type, fn) {
7 if (document.body.addEventListener) {
8 return function (el, type, fn) {
9 el.addEventListener(type, fn, false);
10 };
11 } else if (document.body.attachEvent) {
12 return function (el, type, fn) {
13 el.attachEvent('on' + type, fn);
14 };
15 } else {
16 return function (el, type, fn) {
17 el['on' + type] = fn;
18 };
19 }
20 }(),
21 removeListener : function (el, type, fn) {
22 if (document.body.removeEventListener){
23 return function (el, type, fn) {
24 el.removeEventListener(type, fn, false);
25 };
26 } else if (document.body.detachEvent) {
27 return function (el, type, fn) {
28 el.detachEvent('on' + type, fn);
29 };
30 } else {
31 return function (el, type, fn) {
32 el['on' + type] = function () { return true; };
33 };
34 }
35 }(),
36 hide : function (el) {
37 el.style.display = 'none';
38 },
39 show : function (el) {
40 el.style.display = '';
41 }
42 },
43
44 allThat = window.allThat;
45
46 allThat.Bookmarklet = function () {
47 // where to send user data and where from take wishlists list
48 var wishlistsLocation = allThat.server + 'wishlists/',
49 sendTo = allThat.server + 'wishes/',
50
51 // bookmarklet window html code
52 innerHTML = '<div id="allthat-wish"><div class="allthat-saving" id="allthat-throbber">saving...</div><div id="allthat-logo"><span>AllThat</span></div><button title="close" id="allthat-close"><span>close</span></button><h1><span>Add To Wishlist</span></h1><form action=""><div class="allthat-field"><label for="allthat-product">Product Name:</label><br /><input type="text" name="product" value="(Ex: Black iPhone Adapter)" class="allthat-sample-value" id="allthat-product" /></div> <div class="allthat-field"><label for="allthat-wishlist">Add to List:</label><br /><select name="wishlist" id="allthat-wishlist"></select></div><fieldset id="allthat-low-price"> <h2>Low Price Alerts!</h2> <div class="allthat-field" id="allthat-range"><label for="allthat-minprice">How much would you like to pay?</label><br /><input type="text" name="minprice" value="(min)" class="allthat-sample-value" id="allthat-minprice" />to<input type="text" name="maxprice" value="(max)" class="allthat-sample-value" id="allthat-maxprice" /></div><h3>Alert me via:</h3><fieldset id="allthat-alerts"><input type="checkbox" name="email" id="allthat-email" /><label for="allthat-email" id="allthat-email-label">Email</label><br /><input type="checkbox" name="sms" id="allthat-sms" /><label for="allthat-sms" id="allthat-sms-label">SMS</label><br /><input type="checkbox" name="twitter" id="allthat-twitter" /><label for="allthat-twitter" id="allthat-twitter-label">Twitter</label><br /><select name="frequency" id="allthat-frequency"><option value="0" selected="selected">-- Alert Frequency --</option><option value="1">Daily</option><option value="7">Weekly</option><option value="30">Monthly</option></select></fieldset></fieldset><button title="Add" id="allthat-add"><span>Add</span></button></form><div id="allthat-errors" style="color:red;"/></div>',
53
54 // dom elements:
55 container = document.createElement('div'),
56 errorDiv,
57 alertFrequencyDropdown,
58 wishlistsDropdown,
59 wishlistsWrapper,
60 savingThrobber,
61 closeButton,
62 titleInput,
63 minPriceInput,
64 maxPriceInput,
65 sendButton,
66 alerts = {},
67 scripts = [],
68
69 // input default values:
70 titleDefaultValue = '(Ex: Black iPhone Adapter)',
71 minPriceDefaultValue = '(min)',
72 maxPriceDefaultValue = '(max)',
73
74 // errors array - used for validation
75 errors = [],
76 errorMessages = {
77 titleEmpty : 'Please enter product name',
78 wishlistNotSelected : 'Please chose list',
79 frequencyNonSelected : 'Please chose alert frequency'
80 };
81
82 // append bookmarklet window html to the target page
83 function createTemplate () {
84 container.innerHTML = innerHTML;
85 document.body.appendChild(container);
86 };
87 // initialize javascript references to the Dom elements
88 function initializeDomElementsReferences () {
89 errorDiv = Dom.get('allthat-errors');
90 alertFrequencyDropdown = Dom.get('allthat-frequency');
91 wishlistsDropdown = Dom.get('allthat-wishlist');
92 wishlistsWrapper = wishlistsDropdown.parentNode;
93 savingThrobber = Dom.get('allthat-throbber');
94 closeButton = Dom.get('allthat-close');
95 titleInput = Dom.get('allthat-product');
96 minPriceInput = Dom.get('allthat-minprice');
97 maxPriceInput = Dom.get('allthat-maxprice');
98 sendButton = Dom.get('allthat-add');
99 alerts.email = Dom.get('allthat-email');
100 alerts.sms = Dom.get('allthat-sms');
101 alerts.twitter = Dom.get('allthat-twitter');
102 };
103 // disable wishlist dropdown before server response with wishlists array doesn't arrive
104 function initializeGUI () {
105 wishlistsDropdown.disabled = 'disabled';
106 wishlistsDropdown.style.width = '90%';
107 wishlistsWrapper = wishlistsDropdown.parentNode;
108 wishlistsWrapper.style.background = 'transparent url(' + allThat.server + 'images/bookmarklet/ajax-loader-blue.gif) no-repeat right';
109 Dom.hide(savingThrobber);
110 };
111 // bind event listeners to the controls
112 function attachListeners () {
113 Dom.addListener(closeButton, 'click', destroy);
114 Dom.addListener(titleInput, 'focus', function () {if (titleInput.value == titleDefaultValue) activateInput(titleInput);});
115 Dom.addListener(minPriceInput, 'focus', function () {if (minPriceInput.value == minPriceDefaultValue) activateInput(minPriceInput);});
116 Dom.addListener(maxPriceInput, 'focus', function () {if (maxPriceInput.value == maxPriceDefaultValue) activateInput(maxPriceInput);});
117 Dom.addListener(sendButton, 'click', function (e) { e = e || window.event; if (e.preventDefault) e.preventDefault(); addItemToList(); return false;});
118 };
119
120 // validators
121 function validateItemTitlePresence () {
122 var t = titleInput.value;
123 if (t.replace(/^s+|s+$/, '').length == 0 || t == titleDefaultValue) errors.push(errorMessages.titleEmpty);
124 };
125
126 function validateWishlistPresence () {
127 if (typeof wishlistsDropdown.value === 'undefined') errors.push(errorMessages.wishlistNotSelected);
128 };
129
130 function validateFrequencyPresence () {
131 for (var alert in alerts) {
132 if (alerts[alert].checked && alertFrequencyDropdown.value == 0) {
133 errors.push(errorMessages.frequencyNonSelected);
134 return;
135 }
136 }
137 };
138
139 function validate () {
140 errors.length = 0;
141 validateItemTitlePresence();
142 validateWishlistPresence();
143 validateFrequencyPresence();
144 return errors.length == 0;
145 };
146
147 function displayErrors () {
148 var output = '', error, i = 0;
149 while (error = errors[i++]) {
150 output += error + '<br/>';
151 }
152
153 errorDiv.innerHTML = output;
154 };
155
156 // this function called if user clicks on the 'send' button
157 // so that we need to validate data, and, if it's all ok,
158 // send request to server. Also, we show throbber and setup callback which would stop it
159 function addItemToList () {
160 if (validate()) {
161 sendItemOnServer();
162 }
163 displayErrors();
164 };
165
166 // serialize data into string, show loader and call sendRequest method
167 function sendItemOnServer () {
168 var data = 'title=' + encodeURIComponent(titleInput.value) + '&wishlist=' + wishlistsDropdown.value;
169
170 var temp = minPriceInput.value;
171 data += (temp == '' || temp == minPriceDefaultValue) ? '' : ('&minPrice=' + temp);
172 temp = maxPriceInput.value;
173 data += (temp == '' || temp == maxPriceDefaultValue) ? '' : ('&maxPrice=' + temp);
174
175 data += '&alerts=[';
176 for (var alert in alerts) {
177 var a = alerts[alert];
178 if (a.checked) {
179 data += a.name;
180 }
181 }
182 data += ']';
183 data += '&alertFrequency=' + alertFrequencyDropdown.value;
184
185 showLoader();
186 sendRequest(sendTo, data, 'itemAdded');
187 };
188
189 // clear inputs
190 function clearFields () {
191 hideLoader();
192 activateInput (titleInput);
193 activateInput (minPriceInput);
194 activateInput (maxPriceInput);
195 for (var alert in alerts) alerts[alert].checked = false;
196 };
197
198 // misc - remove default value and make font color black
199 function activateInput (el) {
200 el.className = '';
201 el.value = '';
202 };
203
204 // feed wishlists dropdown with data and enable it
205 function activateWhishlistsDropdown (response) {
206 var w = response.wishlists,
207 i = 0,
208 wishlist,
209 opt;
210 while(wishlist = w[i++]) {
211 opt = document.createElement("option");
212 opt.appendChild(document.createTextNode(wishlist.title));
213 opt.setAttribute("value", wishlist.id);
214 wishlistsDropdown.appendChild(opt);
215 }
216 wishlistsDropdown.disabled = false;
217 wishlistsDropdown.style.width = '100%';
218 wishlistsWrapper.style.background = '';
219 };
220
221 // remove every track of bookmarklet from the page
222 function destroy () {
223 removeEventListeners();
224 removeDOMReferences();
225 container.parentNode.removeChild(container);
226 container = null;
227 allThat.css.parentNode.removeChild(allThat.css);
228 allThat.script.parentNode.removeChild(allThat.script);
229 allThat = null;
230 };
231 // to prevent memory leaks on ie6 - remove all js to dom references
232 function removeDOMReferences () {
233 errorDiv = null;
234 alertFrequencyDropdown = null;
235 wishlistsDropdown = null;
236 wishlistsWrapper = null;
237 savingThrobber = null;
238 closeButton = null;
239 titleInput = null;
240 minPriceInput = null;
241 maxPriceInput = null;
242 sendButton = null;
243 alerts.email = null;
244 alerts.sms = null;
245 alerts.twitter = null;
246 var i = scripts.length - 1, script, head = document.getElementsByTagName('head')[0];
247 while (script = scripts[i--]) {
248 head.removeChild(script);
249 script = null;
250 scripts[i + 1] = null;
251 }
252 scripts.length = null;
253 };
254 // to prevent memory leaks - remove all event listeners
255 function removeEventListeners () {
256 Dom.removeListener(closeButton, 'click', destroy);
257 Dom.removeListener(titleInput, 'focus', function () {if (titleInput.value == titleDefaultValue) activateInput(titleInput);});
258 Dom.removeListener(minPriceInput, 'focus', function () {if (minPriceInput.value == minPriceDefaultValue) activateInput(minPriceInput);});
259 Dom.removeListener(maxPriceInput, 'focus', function () {if (maxPriceInput.value == maxPriceDefaultValue) activateInput(maxPriceInput);});
260 Dom.removeListener(sendButton, 'click', function (e) { e = e || window.event; if (e.preventDefault) e.preventDefault(); addItemToList(); return false;});
261 };
262
263 // create dynamic script element and remove it immediately after it load
264 function sendRequest (url, data, callback) {
265 var head = document.getElementsByTagName('head')[0],
266 script = document.createElement('script'),
267 noCacheIE = '&noCacheIE=' + (new Date()).getTime(),
268 fullUrl = url + '?callback=' + encodeURIComponent(callback) + '&userId=' + allThat.userId+ ((data) ? ('&' + data) : '') + noCacheIE;
269
270 script.setAttribute('type', 'text/javascript');
271 script.setAttribute('src', fullUrl);
272 script.setAttribute('charset', 'utf-8');
273
274 head.appendChild(script);
275 scripts.push(script);
276 head = null;
277 };
278
279 // misc - hide send button and show saving throbber instead
280 function showLoader () {
281 Dom.show(savingThrobber);
282 Dom.hide(sendButton);
283 };
284 // misc - hide saving throbber and show send button instead
285 function hideLoader () {
286 Dom.hide(savingThrobber);
287 Dom.show(sendButton);
288 };
289
290 return {
291 initialize : function () {
292 createTemplate();
293 initializeDomElementsReferences();
294 initializeGUI();
295 attachListeners();
296 sendRequest(wishlistsLocation, null, 'loadWishlists');
297 },
298
299 destroy : function () {
300 destroy();
301 },
302
303 loadWishlists : activateWhishlistsDropdown,
304
305 itemAdded : clearFields
306 };
307 }();
308
309 // to prevent memory leaks, remove all js <-> dom references, including dom elements references and event listeners
310 Dom.addListener(window, 'unload', function () {
311 if (allThat) allThat.Bookmarklet.destroy();
312 Dom.removeListener(window, 'unload', arguments.callee);
313 });
314
315
316 allThat.Bookmarklet.initialize(); // show bookmarklet - this is visual start of the application
317
318 })();
Примечания:
- Вообще скрипт выполнен мной на заказ в рамках моей фрилансерской деятельности, так что не удивляйтесь идее, логотипам и дизайну.