Букмарклеты

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

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

Примером может служить герой сегодняшней заметки, который расположен по адресу http://ulizko. com/demo/allthat/. Инструкция по применению:

  1. Перетащите ссылку «link» на панель закладок или щелкните по ней правой кнопкой мыши и выберите пункт меню «добавить в избранное».
  2. Зайдите на какой-нибудь сайт, вроде 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
 
    })();

Примечания:

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

4 комментария to “Букмарклеты”

  1. ИЕ должен сдохнуть.

  2. Шестой, конечно же.

  3. Конечно, старику пора на покой – он уже морально устарел. Но надо отдать ему должное – в свое время он жег напалмом и подарил миру много клевых фишек.

  4. Как- о я сказала одному знакомому о то м что пользуюсь IE, он долго смеялся, я не могла понять причину его смеха. а потом он выдавил из себя, никогда не называй IЕ браузером. и действительно, он не дает возможности нормально работать, не могу вобще с ним наладить отношения.

Оставить комментарий

Последние твиты