mirror of
https://github.com/zhigang1992/typeahead.js.git
synced 2026-05-21 10:34:07 +08:00
Refactor views.
This commit is contained in:
@@ -5,121 +5,128 @@
|
||||
*/
|
||||
|
||||
var DropdownView = (function() {
|
||||
var html = {
|
||||
suggestionsList: '<span class="tt-suggestions"></span>'
|
||||
},
|
||||
css = {
|
||||
suggestionsList: { display: 'block' },
|
||||
suggestion: { whiteSpace: 'nowrap', cursor: 'pointer' },
|
||||
suggestionChild: { whiteSpace: 'normal' }
|
||||
};
|
||||
|
||||
// constructor
|
||||
// -----------
|
||||
|
||||
function DropdownView(o) {
|
||||
utils.bindAll(this);
|
||||
var that = this, onMouseEnter, onMouseLeave, onSuggestionClick,
|
||||
onSuggestionMouseEnter, onSuggestionMouseLeave;
|
||||
|
||||
this.isOpen = false;
|
||||
this.isEmpty = true;
|
||||
this.isMouseOverDropdown = false;
|
||||
|
||||
// bound functions
|
||||
onMouseEnter = utils.bind(this._onMouseEnter, this);
|
||||
onMouseLeave = utils.bind(this._onMouseLeave, this);
|
||||
onSuggestionClick = utils.bind(this._onSuggestionClick, this);
|
||||
onSuggestionMouseEnter = utils.bind(this._onSuggestionMouseEnter, this);
|
||||
onSuggestionMouseLeave = utils.bind(this._onSuggestionMouseLeave, this);
|
||||
|
||||
this.$menu = $(o.menu)
|
||||
.on('mouseenter.tt', this._handleMouseenter)
|
||||
.on('mouseleave.tt', this._handleMouseleave)
|
||||
.on('click.tt', '.tt-suggestion', this._handleSelection)
|
||||
.on('mouseover.tt', '.tt-suggestion', this._handleSuggestionMouseover)
|
||||
.on('mouseleave.tt', '.tt-suggestion', this._handleSuggestionMouseleave);
|
||||
.on('mouseenter.tt', onMouseEnter)
|
||||
.on('mouseleave.tt', onMouseLeave)
|
||||
.on('click.tt', '.tt-suggestion', onSuggestionClick)
|
||||
.on('mouseenter.tt', '.tt-suggestion', onSuggestionMouseEnter)
|
||||
.on('mouseleave.tt', '.tt-suggestion', onSuggestionMouseLeave);
|
||||
|
||||
this.sections = utils.map(o.sections, initializeSection);
|
||||
|
||||
utils.each(this.sections, function(i, section) {
|
||||
that.$menu.append(section.getRoot());
|
||||
section.onSync('rendered', that._onRendered, that);
|
||||
});
|
||||
}
|
||||
|
||||
utils.mixin(DropdownView.prototype, EventTarget, {
|
||||
// private methods
|
||||
// ---------------
|
||||
// instance methods
|
||||
// ----------------
|
||||
|
||||
_handleMouseenter: function() {
|
||||
utils.mixin(DropdownView.prototype, EventEmitter, {
|
||||
|
||||
// ### private
|
||||
|
||||
_onMouseEnter: function onMouseEnter($e) {
|
||||
this.isMouseOverDropdown = true;
|
||||
},
|
||||
|
||||
_handleMouseleave: function() {
|
||||
_onMouseLeave: function onMouseLeave($e) {
|
||||
this.isMouseOverDropdown = false;
|
||||
},
|
||||
|
||||
_handleSelection: function($e) {
|
||||
var $suggestion = $($e.currentTarget);
|
||||
this.trigger('suggestionSelected', extractSuggestion($suggestion));
|
||||
_onSuggestionClick: function onSuggestionClick($e) {
|
||||
this.trigger('suggestionClicked', $($e.currentTarget));
|
||||
},
|
||||
|
||||
_handleSuggestionMouseover: function($e) {
|
||||
var $suggestion = $($e.currentTarget);
|
||||
|
||||
this._getSuggestions().removeClass('tt-is-under-cursor');
|
||||
$suggestion.addClass('tt-is-under-cursor');
|
||||
_onSuggestionMouseEnter: function onSuggestionMouseEnter($e) {
|
||||
this._setCursor($($e.currentTarget));
|
||||
},
|
||||
|
||||
_handleSuggestionMouseleave: function($e) {
|
||||
var $suggestion = $($e.currentTarget);
|
||||
|
||||
$suggestion.removeClass('tt-is-under-cursor');
|
||||
_onSuggestionMouseLeave: function onSuggestionMouseLeave($e) {
|
||||
this._removeCursor();
|
||||
},
|
||||
|
||||
_show: function() {
|
||||
// can't use jQuery#show because $menu is a span element we want
|
||||
// display: block; not dislay: inline;
|
||||
this.$menu.css('display', 'block');
|
||||
_onRendered: function onRendered() {
|
||||
this.trigger('suggestionsRendered');
|
||||
},
|
||||
|
||||
_hide: function() {
|
||||
this.$menu.hide();
|
||||
_getSuggestions: function getSuggestions() {
|
||||
return this.$menu.find('.tt-suggestion');
|
||||
},
|
||||
|
||||
_moveCursor: function(increment) {
|
||||
var $suggestions, $cur, nextIndex, $underCursor;
|
||||
_getCursor: function getCursor() {
|
||||
return this.$menu.find('.tt-cursor');
|
||||
},
|
||||
|
||||
// don't bother moving the cursor if the menu is closed or empty
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
_setCursor: function setCursor($el) {
|
||||
$el.addClass('tt-cursor');
|
||||
},
|
||||
|
||||
_removeCursor: function removeCursor() {
|
||||
this._getCursor().removeClass('tt-cursor');
|
||||
},
|
||||
|
||||
_moveCursor: function moveCursor(increment) {
|
||||
var $suggestions, $oldCursor, newCursorIndex, $newCursor;
|
||||
|
||||
if (!this.isOpen) { return; }
|
||||
|
||||
$oldCursor = this._getCursor();
|
||||
$suggestions = this._getSuggestions();
|
||||
$cur = $suggestions.filter('.tt-is-under-cursor');
|
||||
|
||||
$cur.removeClass('tt-is-under-cursor');
|
||||
this._removeCursor();
|
||||
|
||||
// shifting before and after modulo to deal with -1 index of search input
|
||||
nextIndex = $suggestions.index($cur) + increment;
|
||||
nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1;
|
||||
// shifting before and after modulo to deal with -1 index
|
||||
newCursorIndex = $suggestions.index($oldCursor) + increment;
|
||||
newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1;
|
||||
|
||||
if (nextIndex === -1) {
|
||||
if (newCursorIndex === -1) {
|
||||
this.trigger('cursorRemoved');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
else if (nextIndex < -1) {
|
||||
// circle to last suggestion
|
||||
nextIndex = $suggestions.length - 1;
|
||||
else if (newCursorIndex < -1) {
|
||||
newCursorIndex = $suggestions.length - 1;
|
||||
}
|
||||
|
||||
$underCursor = $suggestions.eq(nextIndex).addClass('tt-is-under-cursor');
|
||||
this._setCursor($newCursor = $suggestions.eq(newCursorIndex));
|
||||
|
||||
// in the case of scrollable overflow
|
||||
// make sure the cursor is visible in the menu
|
||||
this._ensureVisibility($underCursor);
|
||||
this._ensureVisible($newCursor);
|
||||
|
||||
this.trigger('cursorMoved', extractSuggestion($underCursor));
|
||||
this.trigger('cursorMoved');
|
||||
},
|
||||
|
||||
_getSuggestions: function() {
|
||||
return this.$menu.find('.tt-suggestions > .tt-suggestion');
|
||||
},
|
||||
_ensureVisible: function ensureVisible($el) {
|
||||
var elTop, elBottom, menuScrollTop, menuHeight;
|
||||
|
||||
_ensureVisibility: function($el) {
|
||||
var menuHeight = this.$menu.height() +
|
||||
parseInt(this.$menu.css('paddingTop'), 10) +
|
||||
parseInt(this.$menu.css('paddingBottom'), 10),
|
||||
menuScrollTop = this.$menu.scrollTop(),
|
||||
elTop = $el.position().top,
|
||||
elBottom = elTop + $el.outerHeight(true);
|
||||
elTop = $el.position().top;
|
||||
elBottom = elTop + $el.outerHeight(true);
|
||||
menuScrollTop = this.$menu.scrollTop();
|
||||
menuHeight = this.$menu.height() +
|
||||
parseInt(this.$menu.css('paddingTop'), 10) +
|
||||
parseInt(this.$menu.css('paddingBottom'), 10);
|
||||
|
||||
if (elTop < 0) {
|
||||
this.$menu.scrollTop(menuScrollTop + elTop);
|
||||
@@ -130,153 +137,65 @@ var DropdownView = (function() {
|
||||
}
|
||||
},
|
||||
|
||||
// public methods
|
||||
// --------------
|
||||
// ### public
|
||||
|
||||
destroy: function() {
|
||||
this.$menu.off('.tt');
|
||||
|
||||
this.$menu = null;
|
||||
},
|
||||
|
||||
isVisible: function() {
|
||||
return this.isOpen && !this.isEmpty;
|
||||
},
|
||||
|
||||
closeUnlessMouseIsOverDropdown: function() {
|
||||
// this helps detect the scenario a blur event has triggered
|
||||
// this function. we don't want to close the menu in that case
|
||||
// because it'll prevent the probable associated click event
|
||||
// from being fired
|
||||
if (!this.isMouseOverDropdown) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
close: function() {
|
||||
close: function close() {
|
||||
if (this.isOpen) {
|
||||
this.isOpen = false;
|
||||
this.isMouseOverDropdown = false;
|
||||
this._hide();
|
||||
this.isOpen = this.isMouseOverDropdown = false;
|
||||
|
||||
this.$menu
|
||||
.find('.tt-suggestions > .tt-suggestion')
|
||||
.removeClass('tt-is-under-cursor');
|
||||
this._removeCursor();
|
||||
this.$menu.hide();
|
||||
|
||||
this.trigger('closed');
|
||||
}
|
||||
},
|
||||
|
||||
open: function() {
|
||||
open: function open() {
|
||||
if (!this.isOpen) {
|
||||
this.isOpen = true;
|
||||
!this.isEmpty && this._show();
|
||||
|
||||
// can't use jQuery#show because $menu is a span element we want
|
||||
// display: block; not dislay: inline;
|
||||
this.$menu.css('display', 'block');
|
||||
|
||||
this.trigger('opened');
|
||||
}
|
||||
},
|
||||
|
||||
setLanguageDirection: function(dir) {
|
||||
var ltrCss = { left: '0', right: 'auto' },
|
||||
rtlCss = { left: 'auto', right:' 0' };
|
||||
|
||||
dir === 'ltr' ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss);
|
||||
setLanguageDirection: function setLanguageDirection(dir) {
|
||||
this.$menu.css(dir === 'ltr' ? css.ltr : css.rtl);
|
||||
},
|
||||
|
||||
moveCursorUp: function() {
|
||||
moveCursorUp: function moveCursorUp() {
|
||||
this._moveCursor(-1);
|
||||
},
|
||||
|
||||
moveCursorDown: function() {
|
||||
moveCursorDown: function moveCursorDown() {
|
||||
this._moveCursor(+1);
|
||||
},
|
||||
|
||||
getSuggestionUnderCursor: function() {
|
||||
var $suggestion = this._getSuggestions()
|
||||
.filter('.tt-is-under-cursor')
|
||||
.first();
|
||||
|
||||
return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
|
||||
getDatumForSuggestion: function getDatumForSuggestion($el) {
|
||||
return $el.length ? SectionView.extractDatum($el) : null;
|
||||
},
|
||||
|
||||
getFirstSuggestion: function() {
|
||||
var $suggestion = this._getSuggestions().first();
|
||||
|
||||
return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
|
||||
getDatumForCursor: function getDatumForCursor() {
|
||||
return this.getDatumForSuggestion(this._getCursor().first());
|
||||
},
|
||||
|
||||
renderSuggestions: function(dataset, suggestions) {
|
||||
var datasetClassName = 'tt-dataset-' + dataset.name,
|
||||
wrapper = '<div class="tt-suggestion">%body</div>',
|
||||
compiledHtml,
|
||||
$suggestionsList,
|
||||
$dataset = this.$menu.find('.' + datasetClassName),
|
||||
elBuilder,
|
||||
fragment,
|
||||
$el;
|
||||
|
||||
// first time rendering suggestions for this dataset
|
||||
if ($dataset.length === 0) {
|
||||
$suggestionsList = $(html.suggestionsList).css(css.suggestionsList);
|
||||
|
||||
$dataset = $('<div></div>')
|
||||
.addClass(datasetClassName)
|
||||
.append(dataset.header)
|
||||
.append($suggestionsList)
|
||||
.append(dataset.footer)
|
||||
.appendTo(this.$menu);
|
||||
}
|
||||
|
||||
// suggestions to be rendered
|
||||
if (suggestions.length > 0) {
|
||||
this.isEmpty = false;
|
||||
this.isOpen && this._show();
|
||||
|
||||
elBuilder = document.createElement('div');
|
||||
fragment = document.createDocumentFragment();
|
||||
|
||||
utils.each(suggestions, function(i, suggestion) {
|
||||
suggestion.dataset = dataset.name;
|
||||
compiledHtml = dataset.template(suggestion.datum);
|
||||
elBuilder.innerHTML = wrapper.replace('%body', compiledHtml);
|
||||
|
||||
$el = $(elBuilder.firstChild)
|
||||
.css(css.suggestion)
|
||||
.data('suggestion', suggestion);
|
||||
|
||||
$el.children().each(function() {
|
||||
$(this).css(css.suggestionChild);
|
||||
});
|
||||
|
||||
fragment.appendChild($el[0]);
|
||||
});
|
||||
|
||||
// show this dataset in case it was previously empty
|
||||
// and render the new suggestions
|
||||
$dataset.show().find('.tt-suggestions').html(fragment);
|
||||
}
|
||||
|
||||
// no suggestions to render
|
||||
else {
|
||||
this.clearSuggestions(dataset.name);
|
||||
}
|
||||
|
||||
this.trigger('suggestionsRendered');
|
||||
getDatumForTopSuggestion: function getDatumForTopSuggestion() {
|
||||
return this.getDatumForSuggestion(this._getSuggestions().first());
|
||||
},
|
||||
|
||||
clearSuggestions: function(datasetName) {
|
||||
var $datasets = datasetName ?
|
||||
this.$menu.find('.tt-dataset-' + datasetName) :
|
||||
this.$menu.find('[class^="tt-dataset-"]'),
|
||||
$suggestions = $datasets.find('.tt-suggestions');
|
||||
update: function update(query) {
|
||||
utils.each(this.sections, updateSection);
|
||||
|
||||
$datasets.hide();
|
||||
$suggestions.empty();
|
||||
function updateSection(i, section) { section.update(query); }
|
||||
},
|
||||
|
||||
if (this._getSuggestions().length === 0) {
|
||||
this.isEmpty = true;
|
||||
this._hide();
|
||||
}
|
||||
empty: function empty() {
|
||||
utils.each(this.sections, clearSection);
|
||||
|
||||
function clearSection(i, section) { section.clear(); }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -285,7 +204,7 @@ var DropdownView = (function() {
|
||||
// helper functions
|
||||
// ----------------
|
||||
|
||||
function extractSuggestion($el) {
|
||||
return $el.data('suggestion');
|
||||
function initializeSection(o) {
|
||||
return new SectionView(o);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -5,6 +5,17 @@
|
||||
*/
|
||||
|
||||
var InputView = (function() {
|
||||
var specialKeyCodeMap;
|
||||
|
||||
specialKeyCodeMap = {
|
||||
9: 'tab',
|
||||
27: 'esc',
|
||||
37: 'left',
|
||||
39: 'right',
|
||||
13: 'enter',
|
||||
38: 'up',
|
||||
40: 'down'
|
||||
};
|
||||
|
||||
// constructor
|
||||
// -----------
|
||||
@@ -12,40 +23,27 @@ var InputView = (function() {
|
||||
function InputView(o) {
|
||||
var that = this;
|
||||
|
||||
utils.bindAll(this);
|
||||
|
||||
this.specialKeyCodeMap = {
|
||||
9: 'tab',
|
||||
27: 'esc',
|
||||
37: 'left',
|
||||
39: 'right',
|
||||
13: 'enter',
|
||||
38: 'up',
|
||||
40: 'down'
|
||||
};
|
||||
|
||||
this.$hint = $(o.hint);
|
||||
this.$input = $(o.input)
|
||||
.on('blur.tt', this._handleBlur)
|
||||
.on('focus.tt', this._handleFocus)
|
||||
.on('keydown.tt', this._handleSpecialKeyEvent);
|
||||
.on('blur.tt', utils.bind(this._onBlur, this))
|
||||
.on('focus.tt', utils.bind(this._onFocus, this))
|
||||
.on('keydown.tt', utils.bind(this._onKeydown, this));
|
||||
|
||||
// ie7 and ie8 don't support the input event
|
||||
// ie9 doesn't fire the input event when characters are removed
|
||||
// not sure if ie10 is compatible
|
||||
if (!utils.isMsie()) {
|
||||
this.$input.on('input.tt', this._compareQueryToInputValue);
|
||||
this.$input.on('input.tt', utils.bind(this._onInput, this));
|
||||
}
|
||||
|
||||
else {
|
||||
this.$input
|
||||
.on('keydown.tt keypress.tt cut.tt paste.tt', function($e) {
|
||||
this.$input.on('keydown.tt keypress.tt cut.tt paste.tt', function($e) {
|
||||
// if a special key triggered this, ignore it
|
||||
if (that.specialKeyCodeMap[$e.which || $e.keyCode]) { return; }
|
||||
if (specialKeyCodeMap[$e.which || $e.keyCode]) { return; }
|
||||
|
||||
// give the browser a chance to update the value of the input
|
||||
// before checking to see if the query changed
|
||||
utils.defer(that._compareQueryToInputValue);
|
||||
utils.defer(utils.bind(that._onInput, that, $e));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,105 +55,167 @@ var InputView = (function() {
|
||||
this.$overflowHelper = buildOverflowHelper(this.$input);
|
||||
}
|
||||
|
||||
utils.mixin(InputView.prototype, EventTarget, {
|
||||
// private methods
|
||||
// ---------------
|
||||
// static methods
|
||||
// --------------
|
||||
|
||||
_handleFocus: function() {
|
||||
InputView.normalizeQuery = function(str) {
|
||||
// strips leading whitespace and condenses all whitespace
|
||||
return (str || '').replace(/^\s*/g, '').replace(/\s{2,}/g, ' ');
|
||||
};
|
||||
|
||||
// instance methods
|
||||
// ----------------
|
||||
|
||||
utils.mixin(InputView.prototype, EventEmitter, {
|
||||
|
||||
// ### private
|
||||
|
||||
_onBlur: function onBlur($e) {
|
||||
this.resetInputValue();
|
||||
this.trigger('blurred');
|
||||
},
|
||||
|
||||
_onFocus: function onFocus($e) {
|
||||
this.trigger('focused');
|
||||
},
|
||||
|
||||
_handleBlur: function() {
|
||||
this.trigger('blured');
|
||||
},
|
||||
_onKeydown: function onKeydown($e) {
|
||||
// which is normalized and consistent (but not for ie)
|
||||
var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
|
||||
|
||||
_handleSpecialKeyEvent: function($e) {
|
||||
// which is normalized and consistent (but not for IE)
|
||||
var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode];
|
||||
|
||||
keyName && this.trigger(keyName + 'Keyed', $e);
|
||||
},
|
||||
|
||||
_compareQueryToInputValue: function() {
|
||||
var inputValue = this.getInputValue(),
|
||||
isSameQuery = compareQueries(this.query, inputValue),
|
||||
isSameQueryExceptWhitespace = isSameQuery ?
|
||||
this.query.length !== inputValue.length : false;
|
||||
|
||||
if (isSameQueryExceptWhitespace) {
|
||||
this.trigger('whitespaceChanged', { value: this.query });
|
||||
}
|
||||
|
||||
else if (!isSameQuery) {
|
||||
this.trigger('queryChanged', { value: this.query = inputValue });
|
||||
this._managePreventDefault(keyName, $e);
|
||||
if (keyName && this._shouldTrigger(keyName, $e)) {
|
||||
this.trigger(keyName + 'Keyed', $e);
|
||||
}
|
||||
},
|
||||
|
||||
// public methods
|
||||
// --------------
|
||||
|
||||
destroy: function() {
|
||||
this.$hint.off('.tt');
|
||||
this.$input.off('.tt');
|
||||
|
||||
this.$hint = this.$input = this.$overflowHelper = null;
|
||||
_onInput: function onInput($e) {
|
||||
this._checkInputValue();
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
_managePreventDefault: function managePreventDefault(keyName, $e) {
|
||||
var preventDefault, hintValue, inputValue;
|
||||
|
||||
switch (keyName) {
|
||||
case 'tab':
|
||||
hintValue = this.getHintValue();
|
||||
inputValue = this.getInputValue();
|
||||
|
||||
preventDefault = hintValue &&
|
||||
hintValue !== inputValue &&
|
||||
!withModifier($e);
|
||||
break;
|
||||
|
||||
case 'up':
|
||||
case 'down':
|
||||
preventDefault = !withModifier($e);
|
||||
break;
|
||||
|
||||
default:
|
||||
preventDefault = false;
|
||||
}
|
||||
|
||||
preventDefault && $e.preventDefault();
|
||||
},
|
||||
|
||||
_shouldTrigger: function shouldTrigger(keyName, $e) {
|
||||
var trigger;
|
||||
|
||||
switch (keyName) {
|
||||
case 'tab':
|
||||
trigger = !withModifier($e);
|
||||
break;
|
||||
|
||||
default:
|
||||
trigger = true;
|
||||
}
|
||||
|
||||
return trigger;
|
||||
},
|
||||
|
||||
_checkInputValue: function checkInputValue() {
|
||||
var inputValue, areEquivalent, hasDifferentWhitespace;
|
||||
|
||||
inputValue = this.getInputValue();
|
||||
areEquivalent = areQueriesEquivalent(inputValue, this.query);
|
||||
hasDifferentWhitespace = areEquivalent ?
|
||||
this.query.length !== inputValue.length : false;
|
||||
|
||||
if (!areEquivalent) {
|
||||
this.trigger('queryChanged', this.query = inputValue);
|
||||
}
|
||||
|
||||
else if (hasDifferentWhitespace) {
|
||||
this.trigger('whitespaceChanged', this.query);
|
||||
}
|
||||
},
|
||||
|
||||
// ### public
|
||||
|
||||
focus: function focus() {
|
||||
this.$input.focus();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
blur: function blur() {
|
||||
this.$input.blur();
|
||||
},
|
||||
|
||||
getQuery: function() {
|
||||
getQuery: function getQuery() {
|
||||
return this.query;
|
||||
},
|
||||
|
||||
setQuery: function(query) {
|
||||
setQuery: function setQuery(query) {
|
||||
this.query = query;
|
||||
},
|
||||
|
||||
getInputValue: function() {
|
||||
getInputValue: function getInputValue() {
|
||||
return this.$input.val();
|
||||
},
|
||||
|
||||
setInputValue: function(value, silent) {
|
||||
setInputValue: function setInputValue(value, silent) {
|
||||
this.$input.val(value);
|
||||
|
||||
!silent && this._compareQueryToInputValue();
|
||||
!silent && this._checkInputValue();
|
||||
},
|
||||
|
||||
getHintValue: function() {
|
||||
getHintValue: function getHintValue() {
|
||||
return this.$hint.val();
|
||||
},
|
||||
|
||||
setHintValue: function(value) {
|
||||
setHintValue: function setHintValue(value) {
|
||||
this.$hint.val(value);
|
||||
},
|
||||
|
||||
getLanguageDirection: function() {
|
||||
resetInputValue: function resetInputValue() {
|
||||
this.$input.val(this.query);
|
||||
},
|
||||
|
||||
clearHint: function clearHint() {
|
||||
this.$hint.val('');
|
||||
},
|
||||
|
||||
getLanguageDirection: function getLanguageDirection() {
|
||||
return (this.$input.css('direction') || 'ltr').toLowerCase();
|
||||
},
|
||||
|
||||
isOverflow: function() {
|
||||
hasOverflow: function hasOverflow() {
|
||||
this.$overflowHelper.text(this.getInputValue());
|
||||
|
||||
return this.$overflowHelper.width() > this.$input.width();
|
||||
},
|
||||
|
||||
isCursorAtEnd: function() {
|
||||
var valueLength = this.$input.val().length,
|
||||
selectionStart = this.$input[0].selectionStart,
|
||||
range;
|
||||
var valueLength, selectionStart, range;
|
||||
|
||||
valueLength = this.$input.val().length;
|
||||
selectionStart = this.$input[0].selectionStart;
|
||||
|
||||
if (utils.isNumber(selectionStart)) {
|
||||
return selectionStart === valueLength;
|
||||
}
|
||||
|
||||
else if (document.selection) {
|
||||
// this won't work unless the input has focus, the good news
|
||||
// NOTE: this won't work unless the input has focus, the good news
|
||||
// is this code should only get called when the input has focus
|
||||
range = document.selection.createRange();
|
||||
range.moveStart('character', -valueLength);
|
||||
@@ -169,6 +229,9 @@ var InputView = (function() {
|
||||
|
||||
return InputView;
|
||||
|
||||
// helper functions
|
||||
// ----------------
|
||||
|
||||
function buildOverflowHelper($input) {
|
||||
return $('<span></span>')
|
||||
.css({
|
||||
@@ -193,11 +256,11 @@ var InputView = (function() {
|
||||
.insertAfter($input);
|
||||
}
|
||||
|
||||
function compareQueries(a, b) {
|
||||
// strips leading whitespace and condenses all whitespace
|
||||
a = (a || '').replace(/^\s*/g, '').replace(/\s{2,}/g, ' ');
|
||||
b = (b || '').replace(/^\s*/g, '').replace(/\s{2,}/g, ' ');
|
||||
function areQueriesEquivalent(a, b) {
|
||||
return InputView.normalizeQuery(a) === InputView.normalizeQuery(b);
|
||||
}
|
||||
|
||||
return a === b;
|
||||
function withModifier($e) {
|
||||
return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -5,54 +5,7 @@
|
||||
*/
|
||||
|
||||
var TypeaheadView = (function() {
|
||||
var html = {
|
||||
wrapper: '<span class="twitter-typeahead"></span>',
|
||||
hint: '<input class="tt-hint" type="text" autocomplete="off" spellcheck="off" disabled>',
|
||||
dropdown: '<span class="tt-dropdown-menu"></span>'
|
||||
},
|
||||
css = {
|
||||
wrapper: {
|
||||
position: 'relative',
|
||||
display: 'inline-block'
|
||||
},
|
||||
hint: {
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
borderColor: 'transparent',
|
||||
boxShadow: 'none'
|
||||
},
|
||||
query: {
|
||||
position: 'relative',
|
||||
verticalAlign: 'top',
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
dropdown: {
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: '0',
|
||||
// TODO: should this be configurable?
|
||||
zIndex: '100',
|
||||
display: 'none'
|
||||
}
|
||||
};
|
||||
|
||||
// ie specific styling
|
||||
if (utils.isMsie()) {
|
||||
// ie6-8 (and 9?) doesn't fire hover and click events for elements with
|
||||
// transparent backgrounds, for a workaround, use 1x1 transparent gif
|
||||
utils.mixin(css.query, {
|
||||
backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)'
|
||||
});
|
||||
}
|
||||
|
||||
// ie7 and under specific styling
|
||||
if (utils.isMsie() && utils.isMsie() <= 7) {
|
||||
utils.mixin(css.wrapper, { display: 'inline', zoom: '1' });
|
||||
// if someone can tell me why this is necessary to align
|
||||
// the hint with the query in ie7, i'll send you $5 - @JakeHarding
|
||||
utils.mixin(css.query, { marginTop: '-1px' });
|
||||
}
|
||||
var attrsKey = 'ttAttrs';
|
||||
|
||||
// constructor
|
||||
// -----------
|
||||
@@ -60,275 +13,213 @@ var TypeaheadView = (function() {
|
||||
function TypeaheadView(o) {
|
||||
var $menu, $input, $hint;
|
||||
|
||||
utils.bindAll(this);
|
||||
|
||||
this.$node = buildDomStructure(o.input);
|
||||
this.datasets = o.datasets;
|
||||
this.dir = null;
|
||||
|
||||
this.eventBus = o.eventBus;
|
||||
|
||||
$menu = this.$node.find('.tt-dropdown-menu');
|
||||
$input = this.$node.find('.tt-query');
|
||||
$input = this.$node.find('.tt-input');
|
||||
$hint = this.$node.find('.tt-hint');
|
||||
|
||||
this.dropdownView = new DropdownView({ menu: $menu })
|
||||
.on('suggestionSelected', this._handleSelection)
|
||||
.on('cursorMoved', this._clearHint)
|
||||
.on('cursorMoved', this._setInputValueToSuggestionUnderCursor)
|
||||
.on('cursorRemoved', this._setInputValueToQuery)
|
||||
.on('cursorRemoved', this._updateHint)
|
||||
.on('suggestionsRendered', this._updateHint)
|
||||
.on('opened', this._updateHint)
|
||||
.on('closed', this._clearHint)
|
||||
.on('opened closed', this._propagateEvent);
|
||||
this.dropdown = new DropdownView({ menu: $menu, sections: o.sections })
|
||||
.onSync('suggestionClicked', this._onSuggestionClicked, this)
|
||||
.onSync('cursorMoved', this._onCursorMoved, this)
|
||||
.onSync('cursorRemoved', this._onCursorRemoved, this)
|
||||
.onSync('suggestionsRendered', this._onSuggestionsRendered, this)
|
||||
.onSync('opened', this._onOpened, this)
|
||||
.onSync('closed', this._onClosed, this);
|
||||
|
||||
this.inputView = new InputView({ input: $input, hint: $hint })
|
||||
.on('focused', this._openDropdown)
|
||||
.on('blured', this._closeDropdown)
|
||||
.on('blured', this._setInputValueToQuery)
|
||||
.on('enterKeyed tabKeyed', this._handleSelection)
|
||||
.on('queryChanged', this._clearHint)
|
||||
.on('queryChanged', this._clearSuggestions)
|
||||
.on('queryChanged', this._getSuggestions)
|
||||
.on('whitespaceChanged', this._updateHint)
|
||||
.on('queryChanged whitespaceChanged', this._openDropdown)
|
||||
.on('queryChanged whitespaceChanged', this._setLanguageDirection)
|
||||
.on('escKeyed', this._closeDropdown)
|
||||
.on('escKeyed', this._setInputValueToQuery)
|
||||
.on('tabKeyed upKeyed downKeyed', this._managePreventDefault)
|
||||
.on('upKeyed downKeyed', this._moveDropdownCursor)
|
||||
.on('upKeyed downKeyed', this._openDropdown)
|
||||
.on('tabKeyed leftKeyed rightKeyed', this._autocomplete);
|
||||
this.input = new InputView({ input: $input, hint: $hint })
|
||||
.onSync('focused', this._onFocused, this)
|
||||
.onSync('blurred', this._onBlurred, this)
|
||||
.onSync('enterKeyed', this._onEnterKeyed, this)
|
||||
.onSync('tabKeyed', this._onTabKeyed, this)
|
||||
.onSync('escKeyed', this._onEscKeyed, this)
|
||||
.onSync('upKeyed', this._onUpKeyed, this)
|
||||
.onSync('downKeyed', this._onDownKeyed, this)
|
||||
.onSync('leftKeyed', this._onLeftKeyed, this)
|
||||
.onSync('rightKeyed', this._onRightKeyed, this)
|
||||
.onSync('queryChanged', this._onQueryChanged, this)
|
||||
.onSync('whitespaceChanged', this._onWhitespaceChanged, this);
|
||||
}
|
||||
|
||||
utils.mixin(TypeaheadView.prototype, EventTarget, {
|
||||
// private methods
|
||||
// ---------------
|
||||
// instance methods
|
||||
// ----------------
|
||||
|
||||
_managePreventDefault: function(e) {
|
||||
var $e = e.data,
|
||||
hint,
|
||||
inputValue,
|
||||
preventDefault = false;
|
||||
utils.mixin(TypeaheadView.prototype, {
|
||||
|
||||
switch (e.type) {
|
||||
case 'tabKeyed':
|
||||
hint = this.inputView.getHintValue();
|
||||
inputValue = this.inputView.getInputValue();
|
||||
preventDefault = hint && hint !== inputValue;
|
||||
break;
|
||||
// ### private
|
||||
|
||||
case 'upKeyed':
|
||||
case 'downKeyed':
|
||||
preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey;
|
||||
break;
|
||||
}
|
||||
_onSuggestionClicked: function onSuggestionClicked(type, $el) {
|
||||
var datum;
|
||||
|
||||
preventDefault && $e.preventDefault();
|
||||
},
|
||||
if (datum = this.dropdown.getDatumForSuggestion($el)) {
|
||||
this._select(datum);
|
||||
|
||||
_setLanguageDirection: function() {
|
||||
var dir = this.inputView.getLanguageDirection();
|
||||
|
||||
if (dir !== this.dir) {
|
||||
this.dir = dir;
|
||||
this.$node.css('direction', dir);
|
||||
this.dropdownView.setLanguageDirection(dir);
|
||||
// the click event will cause the input to lose focus, so refocus
|
||||
this.input.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_updateHint: function() {
|
||||
var suggestion = this.dropdownView.getFirstSuggestion(),
|
||||
hint = suggestion ? suggestion.value : null,
|
||||
dropdownIsVisible = this.dropdownView.isVisible(),
|
||||
inputHasOverflow = this.inputView.isOverflow(),
|
||||
inputValue,
|
||||
query,
|
||||
escapedQuery,
|
||||
beginsWithQuery,
|
||||
match;
|
||||
_onCursorMoved: function onCursorMoved() {
|
||||
var datum = this.dropdown.getDatumForCursor();
|
||||
|
||||
if (hint && dropdownIsVisible && !inputHasOverflow) {
|
||||
inputValue = this.inputView.getInputValue();
|
||||
query = inputValue
|
||||
.replace(/\s{2,}/g, ' ') // condense whitespace
|
||||
.replace(/^\s+/g, ''); // strip leading whitespace
|
||||
escapedQuery = utils.escapeRegExChars(query);
|
||||
this.input.clearHint();
|
||||
this.input.setInputValue(datum.value, true);
|
||||
},
|
||||
|
||||
beginsWithQuery = new RegExp('^(?:' + escapedQuery + ')(.*$)', 'i');
|
||||
match = beginsWithQuery.exec(hint);
|
||||
_onCursorRemoved: function onCursorRemoved() {
|
||||
this.input.resetInputValue();
|
||||
this._updateHint();
|
||||
},
|
||||
|
||||
this.inputView.setHintValue(inputValue + (match ? match[1] : ''));
|
||||
_onSuggestionsRendered: function onSuggestionsRendered() {
|
||||
this._updateHint();
|
||||
},
|
||||
|
||||
_onOpened: function onOpened() {
|
||||
this._updateHint();
|
||||
},
|
||||
|
||||
_onClosed: function onClosed() {
|
||||
this.input.clearHint();
|
||||
},
|
||||
|
||||
_onFocused: function onFocused() {
|
||||
this.dropdown.open();
|
||||
},
|
||||
|
||||
_onBlurred: function onBlurred() {
|
||||
// don't close the menu because this was triggered by a blur event
|
||||
// and if the menu is closed, it'll prevent the probable associated
|
||||
// click event from being fired
|
||||
!this.dropdown.isMouseOverDropdown && this.dropdown.close();
|
||||
},
|
||||
|
||||
_onEnterKeyed: function onEnterKeyed(type, $e) {
|
||||
var datum;
|
||||
|
||||
if (datum = this.dropdown.getDatumForCursor()) {
|
||||
this._select(datum);
|
||||
$e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
_clearHint: function() {
|
||||
this.inputView.setHintValue('');
|
||||
},
|
||||
_onTabKeyed: function onTabKeyed(type, $e) {
|
||||
var datum;
|
||||
|
||||
_clearSuggestions: function() {
|
||||
this.dropdownView.clearSuggestions();
|
||||
},
|
||||
|
||||
_setInputValueToQuery: function() {
|
||||
this.inputView.setInputValue(this.inputView.getQuery());
|
||||
},
|
||||
|
||||
_setInputValueToSuggestionUnderCursor: function(e) {
|
||||
var suggestion = e.data;
|
||||
|
||||
this.inputView.setInputValue(suggestion.value, true);
|
||||
},
|
||||
|
||||
_openDropdown: function() {
|
||||
this.dropdownView.open();
|
||||
},
|
||||
|
||||
_closeDropdown: function(e) {
|
||||
this.dropdownView[e.type === 'blured' ?
|
||||
'closeUnlessMouseIsOverDropdown' : 'close']();
|
||||
},
|
||||
|
||||
_moveDropdownCursor: function(e) {
|
||||
var $e = e.data;
|
||||
|
||||
if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) {
|
||||
this.dropdownView[e.type === 'upKeyed' ?
|
||||
'moveCursorUp' : 'moveCursorDown']();
|
||||
}
|
||||
},
|
||||
|
||||
_handleSelection: function(e) {
|
||||
var byClick = e.type === 'suggestionSelected',
|
||||
suggestion,
|
||||
modifierPressed,
|
||||
$e;
|
||||
|
||||
if(byClick) {
|
||||
suggestion = e.data;
|
||||
modifierPressed = false;
|
||||
if (datum = this.dropdown.getDatumForCursor()) {
|
||||
this._select(datum);
|
||||
$e.preventDefault();
|
||||
}
|
||||
|
||||
else {
|
||||
suggestion = this.dropdownView.getSuggestionUnderCursor();
|
||||
$e = e.data;
|
||||
modifierPressed = $e.shiftKey || $e.ctrlKey || $e.metaKey || $e.altKey;
|
||||
}
|
||||
|
||||
if (suggestion && !modifierPressed) {
|
||||
this.inputView.setInputValue(suggestion.value);
|
||||
|
||||
// if triggered by click, ensure the query input still has focus
|
||||
// if triggered by keypress, prevent default browser behavior
|
||||
// which is most likely the submission of a form
|
||||
// note: e.data is the jquery event
|
||||
byClick ? this.inputView.focus() : e.data.preventDefault();
|
||||
|
||||
// focus is not a synchronous event in ie, so we deal with it
|
||||
byClick && utils.isMsie() ?
|
||||
utils.defer(this.dropdownView.close) : this.dropdownView.close();
|
||||
|
||||
this.eventBus.trigger('selected', suggestion.datum, suggestion.dataset);
|
||||
this._autocomplete();
|
||||
}
|
||||
},
|
||||
|
||||
_getSuggestions: function() {
|
||||
var that = this, query = this.inputView.getQuery();
|
||||
|
||||
if (utils.isBlankString(query)) { return; }
|
||||
|
||||
utils.each(this.datasets, function(i, dataset) {
|
||||
dataset.getSuggestions(query, function(suggestions) {
|
||||
// only render the suggestions if the view hasn't
|
||||
// been destroyed and if the query hasn't changed
|
||||
if (that.$node && query === that.inputView.getQuery()) {
|
||||
that.dropdownView.renderSuggestions(dataset, suggestions);
|
||||
}
|
||||
});
|
||||
});
|
||||
_onEscKeyed: function onEscKeyed() {
|
||||
this.dropdown.close();
|
||||
this.input.resetInputValue();
|
||||
},
|
||||
|
||||
_autocomplete: function(e) {
|
||||
var isCursorAtEnd, ignoreEvent, query, hint, suggestion;
|
||||
_onUpKeyed: function onUpKeyed() {
|
||||
this.dropdown.open();
|
||||
this.dropdown.moveCursorUp();
|
||||
},
|
||||
|
||||
if (e.type === 'rightKeyed' || e.type === 'leftKeyed') {
|
||||
isCursorAtEnd = this.inputView.isCursorAtEnd();
|
||||
ignoreEvent = this.inputView.getLanguageDirection() === 'ltr' ?
|
||||
e.type === 'leftKeyed' : e.type === 'rightKeyed';
|
||||
_onDownKeyed: function onDownKeyed() {
|
||||
this.dropdown.open();
|
||||
this.dropdown.moveCursorDown();
|
||||
},
|
||||
|
||||
if (!isCursorAtEnd || ignoreEvent) { return; }
|
||||
}
|
||||
_onLeftKeyed: function onLeftKeyed() {
|
||||
this.dir === 'rtl' && this._autocomplete();
|
||||
},
|
||||
|
||||
query = this.inputView.getQuery();
|
||||
hint = this.inputView.getHintValue();
|
||||
_onRightKeyed: function onRightKeyed() {
|
||||
this.dir === 'ltr' && this._autocomplete();
|
||||
},
|
||||
|
||||
if (hint !== '' && query !== hint) {
|
||||
suggestion = this.dropdownView.getFirstSuggestion();
|
||||
this.inputView.setInputValue(suggestion.value);
|
||||
_onQueryChanged: function onQueryChanged(e, query) {
|
||||
this.input.clearHint();
|
||||
this.dropdown.empty();
|
||||
this.dropdown.update(query);
|
||||
this.dropdown.open();
|
||||
this._setLanguageDirection();
|
||||
},
|
||||
|
||||
this.eventBus.trigger(
|
||||
'autocompleted',
|
||||
suggestion.datum,
|
||||
suggestion.dataset
|
||||
);
|
||||
_onWhitespaceChanged: function onWhitespaceChanged() {
|
||||
this._updateHint();
|
||||
this.dropdown.open();
|
||||
this._setLanguageDirection();
|
||||
},
|
||||
|
||||
_setLanguageDirection: function setLanguageDirection() {
|
||||
var dir;
|
||||
|
||||
if (this.dir !== (dir = this.input.getLanguageDirection())) {
|
||||
this.dir = dir;
|
||||
this.$node.css('direction', dir);
|
||||
this.dropdown.setLanguageDirection(dir);
|
||||
}
|
||||
},
|
||||
|
||||
_propagateEvent: function(e) {
|
||||
this.eventBus.trigger(e.type);
|
||||
_updateHint: function updateHint() {
|
||||
var datum, inputValue, query, escapedQuery, frontMatchRegEx, match;
|
||||
|
||||
datum = this.dropdown.getDatumForTopSuggestion();
|
||||
|
||||
if (datum && this.dropdown.isOpen && !this.input.hasOverflow()) {
|
||||
inputValue = this.input.getInputValue();
|
||||
query = InputView.normalizeQuery(inputValue);
|
||||
escapedQuery = utils.escapeRegExChars(query);
|
||||
|
||||
frontMatchRegEx = new RegExp('^(?:' + escapedQuery + ')(.*$)', 'i');
|
||||
match = frontMatchRegEx.exec(datum.value);
|
||||
|
||||
this.input.setHintValue(inputValue + (match ? match[1] : ''));
|
||||
}
|
||||
},
|
||||
|
||||
// public methods
|
||||
// --------------
|
||||
_autocomplete: function autocomplete() {
|
||||
var hint, query, datum;
|
||||
|
||||
destroy: function() {
|
||||
this.inputView.destroy();
|
||||
this.dropdownView.destroy();
|
||||
hint = this.input.getHintValue();
|
||||
query = this.input.getQuery();
|
||||
|
||||
destroyDomStructure(this.$node);
|
||||
|
||||
this.$node = null;
|
||||
if (hint !== '' && query !== hint && this.input.isCursorAtEnd()) {
|
||||
datum = this.dropdown.getDatumForTopSuggestion();
|
||||
datum && this.input.setInputValue(datum.value);
|
||||
}
|
||||
},
|
||||
|
||||
setQuery: function(query) {
|
||||
this.inputView.setQuery(query);
|
||||
this.inputView.setInputValue(query);
|
||||
_select: function select(datum) {
|
||||
this.input.setInputValue(datum.value);
|
||||
|
||||
this._clearHint();
|
||||
this._clearSuggestions();
|
||||
this._getSuggestions();
|
||||
// in ie, focus is not a synchronous event, so when a selection
|
||||
// is triggered by a click within the dropdown menu, we need to
|
||||
// defer the closing of the dropdown otherwise it'll stay open
|
||||
utils.defer(utils.bind(this.dropdown.close, this.dropdown));
|
||||
|
||||
// TODO: trigger an event
|
||||
}
|
||||
|
||||
// ### public
|
||||
|
||||
});
|
||||
|
||||
return TypeaheadView;
|
||||
|
||||
function buildDomStructure(input) {
|
||||
var $wrapper = $(html.wrapper),
|
||||
$dropdown = $(html.dropdown),
|
||||
$input = $(input),
|
||||
$hint = $(html.hint);
|
||||
var $input, $wrapper, $dropdown, $hint;
|
||||
|
||||
$wrapper = $wrapper.css(css.wrapper);
|
||||
$dropdown = $dropdown.css(css.dropdown);
|
||||
|
||||
$hint
|
||||
.css(css.hint)
|
||||
// copy background styles from query input to hint input
|
||||
.css({
|
||||
backgroundAttachment: $input.css('background-attachment'),
|
||||
backgroundClip: $input.css('background-clip'),
|
||||
backgroundColor: $input.css('background-color'),
|
||||
backgroundImage: $input.css('background-image'),
|
||||
backgroundOrigin: $input.css('background-origin'),
|
||||
backgroundPosition: $input.css('background-position'),
|
||||
backgroundRepeat: $input.css('background-repeat'),
|
||||
backgroundSize: $input.css('background-size')
|
||||
});
|
||||
$input = $(input);
|
||||
$wrapper = $(html.wrapper).css(css.wrapper);
|
||||
$dropdown = $(html.dropdown).css(css.dropdown);
|
||||
$hint = $(html.hint).css(css.hint).css(getBackgroundStyles($input));
|
||||
|
||||
// store the original values of the attrs that get modified
|
||||
// so modifications can be reverted on destroy
|
||||
$input.data('ttAttrs', {
|
||||
$input.data(attrsKey, {
|
||||
dir: $input.attr('dir'),
|
||||
autocomplete: $input.attr('autocomplete'),
|
||||
spellcheck: $input.attr('spellcheck'),
|
||||
@@ -336,36 +227,27 @@ var TypeaheadView = (function() {
|
||||
});
|
||||
|
||||
$input
|
||||
.addClass('tt-query')
|
||||
.addClass('tt-input')
|
||||
.attr({ autocomplete: 'off', spellcheck: false })
|
||||
.css(css.query);
|
||||
.css(css.input);
|
||||
|
||||
// ie7 does not like it when dir is set to auto,
|
||||
// it does not like it one bit
|
||||
try { !$input.attr('dir') && $input.attr('dir', 'auto'); } catch (e) {}
|
||||
|
||||
return $input
|
||||
.wrap($wrapper)
|
||||
.parent()
|
||||
.prepend($hint)
|
||||
.append($dropdown);
|
||||
return $input.wrap($wrapper).parent().prepend($hint).append($dropdown);
|
||||
}
|
||||
|
||||
function destroyDomStructure($node) {
|
||||
var $input = $node.find('.tt-query');
|
||||
|
||||
// need to remove attrs that weren't previously defined and
|
||||
// revert attrs that originally had a value
|
||||
utils.each($input.data('ttAttrs'), function(key, val) {
|
||||
utils.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
|
||||
});
|
||||
|
||||
$input
|
||||
.detach()
|
||||
.removeData('ttAttrs')
|
||||
.removeClass('tt-query')
|
||||
.insertAfter($node);
|
||||
|
||||
$node.remove();
|
||||
function getBackgroundStyles($el) {
|
||||
return {
|
||||
backgroundAttachment: $el.css('background-attachment'),
|
||||
backgroundClip: $el.css('background-clip'),
|
||||
backgroundColor: $el.css('background-color'),
|
||||
backgroundImage: $el.css('background-image'),
|
||||
backgroundOrigin: $el.css('background-origin'),
|
||||
backgroundPosition: $el.css('background-position'),
|
||||
backgroundRepeat: $el.css('background-repeat'),
|
||||
backgroundSize: $el.css('background-size')
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user