Букмарклеты

Всем привет, сегодня речь пойдет про использование букмарклета, или закладки для браузера.

Кто не знает, это такая штука, которую можно добавить в закладки (да, я сегодня дебютирую в роли Капитана Очевидность :) и, при нажатии на нее, произвести какой-нибудь эффект.

Примером может служить герой сегодняшней заметки, который расположен по адресу 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 })();

Примечания:

  • Вообще скрипт выполнен мной на заказ в рамках моей фрилансерской деятельности, так что не удивляйтесь идее, логотипам и дизайну.
blog comments powered by Disqus