Всем привет, сегодня речь пойдет про использование букмарклета, или закладки для браузера.
Кто не знает, это такая штука, которую можно добавить в закладки (да, я сегодня дебютирую в роли Капитана Очевидность :) и, при нажатии на нее, произвести какой-нибудь эффект.
Примером может служить герой сегодняшней заметки, который расположен по адресу 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, в который уже сгружают код приложения.
Так поступил и я. Вот код моего букмарклета в человекоадаптированном виде:
(function () { // создаем новую внутреннюю переменную a (лучше в данном случае использовать короткие идентификаторы) // и сразу же добавляем свой объект в глобальный объект window, и записываем в него данные, которые уникальны // для каждого пользователя (ведь они сгенерированы сервером для пользователя перед тем, как он добавил этот букмарклет к себе) var a = window.allThat = { userId : '123345456', server : 'http://mysite.com/', script : document.createElement('script'), // создадим и запомним тэг скрипт, // который сгрузит нам код нашего приложения - мы его потом удалим, если пользователь нажмет кнопку "закрыть" css : document.createElement('link') }, /* динамически создаем элементы: */ h = document.getElementsByTagName('head')[0]; a.css.rel = 'stylesheet'; a.css.href = a.server + 'css/bookmarklet.2.css'; h.appendChild(a.css); a.script.src = a.server + 'js/bookmarklet.7.js'; h.appendChild(a.script); h=null; })();
Потом подгружается непосредственно код самого окошка. Думаю, он может представлять некий интерес сам по себе, так что и его я сюда запощу (все комментарии идут на английском, так как заказчик американец):
(function () { var Dom = { get : function (el) { return (el && el.nodeType) ? el : document.getElementById(el); }, addListener : function (el, type, fn) { if (document.body.addEventListener) { return function (el, type, fn) { el.addEventListener(type, fn, false); }; } else if (document.body.attachEvent) { return function (el, type, fn) { el.attachEvent('on' + type, fn); }; } else { return function (el, type, fn) { el['on' + type] = fn; }; } }(), removeListener : function (el, type, fn) { if (document.body.removeEventListener){ return function (el, type, fn) { el.removeEventListener(type, fn, false); }; } else if (document.body.detachEvent) { return function (el, type, fn) { el.detachEvent('on' + type, fn); }; } else { return function (el, type, fn) { el['on' + type] = function () { return true; }; }; } }(), hide : function (el) { el.style.display = 'none'; }, show : function (el) { el.style.display = ''; } }, allThat = window.allThat; allThat.Bookmarklet = function () { // where to send user data and where from take wishlists list var wishlistsLocation = allThat.server + 'wishlists/', sendTo = allThat.server + 'wishes/', // bookmarklet window html code 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>', // dom elements: container = document.createElement('div'), errorDiv, alertFrequencyDropdown, wishlistsDropdown, wishlistsWrapper, savingThrobber, closeButton, titleInput, minPriceInput, maxPriceInput, sendButton, alerts = {}, scripts = [], // input default values: titleDefaultValue = '(Ex: Black iPhone Adapter)', minPriceDefaultValue = '(min)', maxPriceDefaultValue = '(max)', // errors array - used for validation errors = [], errorMessages = { titleEmpty : 'Please enter product name', wishlistNotSelected : 'Please chose list', frequencyNonSelected : 'Please chose alert frequency' }; // append bookmarklet window html to the target page function createTemplate () { container.innerHTML = innerHTML; document.body.appendChild(container); }; // initialize javascript references to the Dom elements function initializeDomElementsReferences () { errorDiv = Dom.get('allthat-errors'); alertFrequencyDropdown = Dom.get('allthat-frequency'); wishlistsDropdown = Dom.get('allthat-wishlist'); wishlistsWrapper = wishlistsDropdown.parentNode; savingThrobber = Dom.get('allthat-throbber'); closeButton = Dom.get('allthat-close'); titleInput = Dom.get('allthat-product'); minPriceInput = Dom.get('allthat-minprice'); maxPriceInput = Dom.get('allthat-maxprice'); sendButton = Dom.get('allthat-add'); alerts.email = Dom.get('allthat-email'); alerts.sms = Dom.get('allthat-sms'); alerts.twitter = Dom.get('allthat-twitter'); }; // disable wishlist dropdown before server response with wishlists array doesn't arrive function initializeGUI () { wishlistsDropdown.disabled = 'disabled'; wishlistsDropdown.style.width = '90%'; wishlistsWrapper = wishlistsDropdown.parentNode; wishlistsWrapper.style.background = 'transparent url(' + allThat.server + 'images/bookmarklet/ajax-loader-blue.gif) no-repeat right'; Dom.hide(savingThrobber); }; // bind event listeners to the controls function attachListeners () { Dom.addListener(closeButton, 'click', destroy); Dom.addListener(titleInput, 'focus', function () {if (titleInput.value == titleDefaultValue) activateInput(titleInput);}); Dom.addListener(minPriceInput, 'focus', function () {if (minPriceInput.value == minPriceDefaultValue) activateInput(minPriceInput);}); Dom.addListener(maxPriceInput, 'focus', function () {if (maxPriceInput.value == maxPriceDefaultValue) activateInput(maxPriceInput);}); Dom.addListener(sendButton, 'click', function (e) { e = e || window.event; if (e.preventDefault) e.preventDefault(); addItemToList(); return false;}); }; // validators function validateItemTitlePresence () { var t = titleInput.value; if (t.replace(/^s+|s+$/, '').length == 0 || t == titleDefaultValue) errors.push(errorMessages.titleEmpty); }; function validateWishlistPresence () { if (typeof wishlistsDropdown.value === 'undefined') errors.push(errorMessages.wishlistNotSelected); }; function validateFrequencyPresence () { for (var alert in alerts) { if (alerts[alert].checked && alertFrequencyDropdown.value == 0) { errors.push(errorMessages.frequencyNonSelected); return; } } }; function validate () { errors.length = 0; validateItemTitlePresence(); validateWishlistPresence(); validateFrequencyPresence(); return errors.length == 0; }; function displayErrors () { var output = '', error, i = 0; while (error = errors[i++]) { output += error + '<br/>'; } errorDiv.innerHTML = output; }; // this function called if user clicks on the 'send' button // so that we need to validate data, and, if it's all ok, // send request to server. Also, we show throbber and setup callback which would stop it function addItemToList () { if (validate()) { sendItemOnServer(); } displayErrors(); }; // serialize data into string, show loader and call sendRequest method function sendItemOnServer () { var data = 'title=' + encodeURIComponent(titleInput.value) + '&wishlist=' + wishlistsDropdown.value; var temp = minPriceInput.value; data += (temp == '' || temp == minPriceDefaultValue) ? '' : ('&minPrice=' + temp); temp = maxPriceInput.value; data += (temp == '' || temp == maxPriceDefaultValue) ? '' : ('&maxPrice=' + temp); data += '&alerts=['; for (var alert in alerts) { var a = alerts[alert]; if (a.checked) { data += a.name; } } data += ']'; data += '&alertFrequency=' + alertFrequencyDropdown.value; showLoader(); sendRequest(sendTo, data, 'itemAdded'); }; // clear inputs function clearFields () { hideLoader(); activateInput (titleInput); activateInput (minPriceInput); activateInput (maxPriceInput); for (var alert in alerts) alerts[alert].checked = false; }; // misc - remove default value and make font color black function activateInput (el) { el.className = ''; el.value = ''; }; // feed wishlists dropdown with data and enable it function activateWhishlistsDropdown (response) { var w = response.wishlists, i = 0, wishlist, opt; while(wishlist = w[i++]) { opt = document.createElement("option"); opt.appendChild(document.createTextNode(wishlist.title)); opt.setAttribute("value", wishlist.id); wishlistsDropdown.appendChild(opt); } wishlistsDropdown.disabled = false; wishlistsDropdown.style.width = '100%'; wishlistsWrapper.style.background = ''; }; // remove every track of bookmarklet from the page function destroy () { removeEventListeners(); removeDOMReferences(); container.parentNode.removeChild(container); container = null; allThat.css.parentNode.removeChild(allThat.css); allThat.script.parentNode.removeChild(allThat.script); allThat = null; }; // to prevent memory leaks on ie6 - remove all js to dom references function removeDOMReferences () { errorDiv = null; alertFrequencyDropdown = null; wishlistsDropdown = null; wishlistsWrapper = null; savingThrobber = null; closeButton = null; titleInput = null; minPriceInput = null; maxPriceInput = null; sendButton = null; alerts.email = null; alerts.sms = null; alerts.twitter = null; var i = scripts.length - 1, script, head = document.getElementsByTagName('head')[0]; while (script = scripts[i--]) { head.removeChild(script); script = null; scripts[i + 1] = null; } scripts.length = null; }; // to prevent memory leaks - remove all event listeners function removeEventListeners () { Dom.removeListener(closeButton, 'click', destroy); Dom.removeListener(titleInput, 'focus', function () {if (titleInput.value == titleDefaultValue) activateInput(titleInput);}); Dom.removeListener(minPriceInput, 'focus', function () {if (minPriceInput.value == minPriceDefaultValue) activateInput(minPriceInput);}); Dom.removeListener(maxPriceInput, 'focus', function () {if (maxPriceInput.value == maxPriceDefaultValue) activateInput(maxPriceInput);}); Dom.removeListener(sendButton, 'click', function (e) { e = e || window.event; if (e.preventDefault) e.preventDefault(); addItemToList(); return false;}); }; // create dynamic script element and remove it immediately after it load function sendRequest (url, data, callback) { var head = document.getElementsByTagName('head')[0], script = document.createElement('script'), noCacheIE = '&noCacheIE=' + (new Date()).getTime(), fullUrl = url + '?callback=' + encodeURIComponent(callback) + '&userId=' + allThat.userId+ ((data) ? ('&' + data) : '') + noCacheIE; script.setAttribute('type', 'text/javascript'); script.setAttribute('src', fullUrl); script.setAttribute('charset', 'utf-8'); head.appendChild(script); scripts.push(script); head = null; }; // misc - hide send button and show saving throbber instead function showLoader () { Dom.show(savingThrobber); Dom.hide(sendButton); }; // misc - hide saving throbber and show send button instead function hideLoader () { Dom.hide(savingThrobber); Dom.show(sendButton); }; return { initialize : function () { createTemplate(); initializeDomElementsReferences(); initializeGUI(); attachListeners(); sendRequest(wishlistsLocation, null, 'loadWishlists'); }, destroy : function () { destroy(); }, loadWishlists : activateWhishlistsDropdown, itemAdded : clearFields }; }(); // to prevent memory leaks, remove all js <-> dom references, including dom elements references and event listeners Dom.addListener(window, 'unload', function () { if (allThat) allThat.Bookmarklet.destroy(); Dom.removeListener(window, 'unload', arguments.callee); }); allThat.Bookmarklet.initialize(); // show bookmarklet - this is visual start of the application })();
Примечания:
- Вообще скрипт выполнен мной на заказ в рамках моей фрилансерской деятельности, так что не удивляйтесь идее, логотипам и дизайну.
ИЕ должен сдохнуть.
Шестой, конечно же.
Конечно, старику пора на покой – он уже морально устарел. Но надо отдать ему должное – в свое время он жег напалмом и подарил миру много клевых фишек.
Как- о я сказала одному знакомому о то м что пользуюсь IE, он долго смеялся, я не могла понять причину его смеха. а потом он выдавил из себя, никогда не называй IЕ браузером. и действительно, он не дает возможности нормально работать, не могу вобще с ним наладить отношения.