From c54821f17579eb8a1bf7313e22394c78b217aa63 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sat, 4 Apr 2015 17:28:51 +1100 Subject: [PATCH] Overhaul the plugin installation process in the back-end Themes can now be installed via the back-end --- modules/cms/classes/ThemeManager.php | 126 ++++++++ modules/cms/controllers/Themes.php | 12 +- .../cms/controllers/themes/_theme_list.htm | 6 +- modules/cms/lang/en/lang.php | 2 +- .../assets/css/{ => settings}/settings.css | 0 modules/system/assets/css/updates/install.css | 276 +++++++++++++++++ .../assets/css/{ => updates}/updates.css | 0 modules/system/assets/js/updates/install.js | 164 ++++++++++ .../assets/less/{ => settings}/settings.less | 2 +- .../system/assets/less/updates/install.less | 293 ++++++++++++++++++ .../assets/less/{ => updates}/updates.less | 2 +- modules/system/classes/PluginManager.php | 36 +++ modules/system/classes/UpdateManager.php | 147 ++++++++- modules/system/controllers/Settings.php | 2 +- modules/system/controllers/Updates.php | 214 +++++++++++-- .../controllers/updates/_install_plugins.htm | 102 ++++++ .../controllers/updates/_install_themes.htm | 104 +++++++ .../controllers/updates/_list_search.htm | 24 -- .../controllers/updates/_list_toolbar.htm | 5 + .../controllers/updates/_theme_form.htm | 47 +++ .../controllers/updates/config_list.yaml | 1 - .../system/controllers/updates/install.htm | 49 +++ modules/system/lang/en/lang.php | 8 + .../system/models/pluginversion/columns.yaml | 28 +- 24 files changed, 1552 insertions(+), 98 deletions(-) create mode 100644 modules/cms/classes/ThemeManager.php rename modules/system/assets/css/{ => settings}/settings.css (100%) create mode 100644 modules/system/assets/css/updates/install.css rename modules/system/assets/css/{ => updates}/updates.css (100%) create mode 100644 modules/system/assets/js/updates/install.js rename modules/system/assets/less/{ => settings}/settings.less (96%) create mode 100644 modules/system/assets/less/updates/install.less rename modules/system/assets/less/{ => updates}/updates.less (94%) create mode 100644 modules/system/controllers/updates/_install_plugins.htm create mode 100644 modules/system/controllers/updates/_install_themes.htm delete mode 100644 modules/system/controllers/updates/_list_search.htm create mode 100644 modules/system/controllers/updates/_theme_form.htm create mode 100644 modules/system/controllers/updates/install.htm diff --git a/modules/cms/classes/ThemeManager.php b/modules/cms/classes/ThemeManager.php new file mode 100644 index 000000000..d3042e940 --- /dev/null +++ b/modules/cms/classes/ThemeManager.php @@ -0,0 +1,126 @@ +getInstalled(); + foreach ($installed as $code => $name) { + if ($dirName == $name) { + return $code; + } + } + + return null; + } + + // + // Management + // + + /** + * Completely delete a theme from the system. + * @param string $id Theme code/namespace + * @return void + */ + public function deleteTheme($theme) + { + if (!$theme) { + return false; + } + + if (is_string($theme)) { + $theme = CmsTheme::load($theme); + } + + if ($theme->isActiveTheme()) { + throw new ApplicationException(trans('cms::lang.theme.delete_active_theme_failed')); + } + + /* + * Delete from file system + */ + $themePath = $theme->getPath(); + if (File::isDirectory($themePath)) { + File::deleteDirectory($themePath); + } + + /* + * Set uninstalled + */ + if ($themeCode = $this->findByDirName($theme->getDirName())) { + $this->setUninstalled($themeCode); + } + } + + +} \ No newline at end of file diff --git a/modules/cms/controllers/Themes.php b/modules/cms/controllers/Themes.php index 4815dc37d..08ba468cd 100644 --- a/modules/cms/controllers/Themes.php +++ b/modules/cms/controllers/Themes.php @@ -13,6 +13,7 @@ use Cms\Models\ThemeData; use Cms\Models\ThemeExport; use Cms\Models\ThemeImport; use Cms\Classes\Theme as CmsTheme; +use Cms\Classes\ThemeManager; use System\Classes\SettingsManager; use Backend\Classes\Controller; use Exception; @@ -71,16 +72,7 @@ class Themes extends Controller public function index_onDelete() { - $theme = $this->findThemeObject(); - - if ($theme->isActiveTheme()) { - throw new ApplicationException(trans('cms::lang.theme.delete_active_theme_failed')); - } - - $themePath = $theme->getPath(); - if (File::isDirectory($themePath)) { - File::deleteDirectory($themePath); - } + ThemeManager::instance()->deleteTheme(post('theme')); Flash::success(trans('cms::lang.theme.delete_theme_success')); return Redirect::refresh(); diff --git a/modules/cms/controllers/themes/_theme_list.htm b/modules/cms/controllers/themes/_theme_list.htm index f28a3f298..f2a887292 100644 --- a/modules/cms/controllers/themes/_theme_list.htm +++ b/modules/cms/controllers/themes/_theme_list.htm @@ -19,14 +19,12 @@ data-control="popup" data-handler="onLoadCreateForm" data-size="huge" - href="javascript:;" - target="_blank"> + href="javascript:;"> + href=""> diff --git a/modules/cms/lang/en/lang.php b/modules/cms/lang/en/lang.php index c62ce19dc..6524f842e 100644 --- a/modules/cms/lang/en/lang.php +++ b/modules/cms/lang/en/lang.php @@ -74,7 +74,7 @@ return [ 'new_directory_name_comment' => 'Provide a new directory name for the duplicated theme.', 'dir_name_invalid' => 'Name can contain only digits, Latin letters and the following symbols: _-', 'dir_name_taken' => 'Desired theme directory already exists.', - 'find_more_themes' => 'Find more themes on OctoberCMS Theme Marketplace', + 'find_more_themes' => 'Find more themes', 'return' => 'Return to themes list', ], 'maintenance' => [ diff --git a/modules/system/assets/css/settings.css b/modules/system/assets/css/settings/settings.css similarity index 100% rename from modules/system/assets/css/settings.css rename to modules/system/assets/css/settings/settings.css diff --git a/modules/system/assets/css/updates/install.css b/modules/system/assets/css/updates/install.css new file mode 100644 index 000000000..ddf0db7df --- /dev/null +++ b/modules/system/assets/css/updates/install.css @@ -0,0 +1,276 @@ +.product-list-empty { + padding: 5px 0; + font-size: 16px; + color: #999; +} +.product-list { + margin: 0; + padding: 10px 0; + overflow: hidden; + /* clearfix */ +} +.product-list li button, +.product-list li .image, +.product-list li .details { + -webkit-transition: opacity .2s linear; + -moz-transition: opacity .2s linear; + transition: opacity .2s linear; +} +.product-list li button { + position: absolute; + top: 0; + right: 0; + width: 20px; + height: 20px; + opacity: 0; + outline: none; +} +.product-list li:hover button { + opacity: .3; +} +.product-list li:hover button:hover { + opacity: .8; +} +.plugin-list li { + list-style: none; + position: relative; + border-bottom: 1px solid #E6E9E9; + margin-bottom: 10px; + padding-bottom: 10px; + overflow: hidden; +} +.plugin-list li:last-child { + border-bottom: none; +} +.plugin-list li .image { + float: left; + margin-right: 15px; + margin-left: 5px; +} +.plugin-list li .image img { + width: 50px; + height: 50px; +} +.plugin-list li .details p { + padding: 0; + margin: 3px 0 0 0; + color: #808C8D; +} +.plugin-list li h4 { + padding: 5px 0 0; + margin: 0; + color: #C03F31; + font-weight: 400; +} +.theme-list li { + float: left; + padding: 0; + margin: 0 10px 10px 0; + list-style: none; + border: 1px solid #E6E9E9; + background: #fff; + position: relative; + border-radius: 3px; +} +.theme-list li:hover { + border-color: transparent; +} +.theme-list li { + -webkit-transition: border .2s linear; + -moz-transition: border .2s linear; + transition: border .2s linear; +} +.theme-list li .image { + padding: 5px; +} +.theme-list li .image img { + width: 210px; + height: 140px; +} +.theme-list li:hover .image { + opacity: 0; +} +.theme-list li .details { + position: absolute; + bottom: 0; + left: 0; + opacity: 0; + padding: 10px; + overflow: hidden; +} +.theme-list li:hover .details { + opacity: 1; +} +.theme-list li h4 { + padding: 15px 0 0; + margin: 0; +} +.theme-list li p { + padding: 0; + margin: 0; + color: #999; + text-transform: uppercase; + font-size: 12px; +} +.suggested-products { + padding: 0; +} +.suggested-products .product { + padding: 0; +} +.suggested-products .image img { + width: 40px; + height: 40px; + margin-top: 10px; +} +.suggested-themes .image img { + width: 60px; + height: 40px; +} +.suggested-products .image { + float: left; + position: relative; +} +.suggested-products .details { + margin-left: 50px; + padding: 10px 0; +} +.suggested-themes .details { + margin-left: 70px; +} +.suggested-products .details h5 { + margin: 0 0 3px; + font-size: 14px; + color: #C03F31; + font-weight: 400; +} +.suggested-products .details p { + font-size: 12px; +} +.suggested-products a { + color: #777; + background: #fff; + padding: 5px; + text-decoration: none; + display: block; + overflow: hidden; + border-bottom: 1px solid #E6E9E9; +} +.suggested-products a:hover { + color: #333; + background: #f9f9f9; +} +.suggested-products a:hover .image:after { + content: "+"; + color: #999; + font-size: 32px; + display: block; + width: 40px; + height: 40px; + text-align: center; + line-height: 40px; + position: absolute; + top: 7px; + left: 0; +} +.suggested-products a:hover .image img { + opacity: .5; +} +/*! + * Typeahead + */ +.product-search { + position: relative; + width: 100%; + margin: 0 auto 0 auto; + text-align: left; + padding-bottom: 15px; +} +.twitter-typeahead { + width: 100%; +} +.typeahead, +.tt-hint { + width: 100%; + height: 46px; + padding: 8px 12px; + font-size: 24px; + line-height: 30px; + border: 1px solid #024e6a; + border-radius: 3px; + outline: none; +} +.typeahead { + background-color: #fff; + border-color: #e0e0e0; +} +.tt-input { + font-weight: 200; +} +.tt-input:focus { + border-color: #E6E9E9; +} +.tt-hint { + color: #999; + font-weight: 200; +} +.tt-dropdown-menu { + width: 100%; + margin-top: 0; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 3px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); +} +.tt-suggestion { + font-size: 14px; + line-height: 18px; +} +.tt-suggestion + .tt-suggestion { + font-size: 14px; + border-top: 1px solid #ccc; +} +.tt-suggestions .product-details { + padding: 5px; + overflow: hidden; + position: relative; +} +.tt-suggestions .product-image { + float: left; + margin-right: 10px; +} +.tt-suggestions .product-image img { + height: 45px; + width: 45px; +} +.tt-suggestions .product-name { + font-size: 20px; + padding-top: 5px; +} +.tt-suggestion.tt-cursor { + cursor: pointer; +} +.tt-suggestion.tt-cursor .product-details { + color: #333; + background: #f9f9f9; + border-color: #f0f0f0; +} +.tt-suggestion.tt-cursor .product-details .product-image:after { + content: "+"; + color: #999; + font-size: 38px; + display: block; + width: 45px; + height: 45px; + text-align: center; + line-height: 45px; + position: absolute; + top: 5px; + left: 5px; +} +.tt-suggestion.tt-cursor .product-details .product-image img { + opacity: .5; +} diff --git a/modules/system/assets/css/updates.css b/modules/system/assets/css/updates/updates.css similarity index 100% rename from modules/system/assets/css/updates.css rename to modules/system/assets/css/updates/updates.css diff --git a/modules/system/assets/js/updates/install.js b/modules/system/assets/js/updates/install.js new file mode 100644 index 000000000..1f4c0f462 --- /dev/null +++ b/modules/system/assets/js/updates/install.js @@ -0,0 +1,164 @@ +/* + * Installs class + * + * Dependences: + * - Waterfall plugin (waterfall.js) + */ + ++function ($) { "use strict"; + + var InstallProcess = function () { + + // Init + this.init() + + } + + InstallProcess.prototype.init = function() { + var self = this + + this.dataSet = {} + + $(document).ready(function(){ + + self.bindSearch('#pluginSearchInput') + self.bindSearch('#themeSearchInput') + + self.bindSuggested('#suggestedPlugins') + self.bindSuggested('#suggestedThemes') + }) + } + + // + // Searching + // + + InstallProcess.prototype.bindSearch = function(el) { + var $el = $(el), + $form = $el.closest('form'), + searchType = $el.data('searchType') + + if ($el.length == 0) return + + // Template for search results + var template = Mustache.compile([ + '
', + '
', + '
{{name}}
', + '
{{description}}
', + '
' + ].join('')) + + // Source for product search + var engine = new Bloodhound({ + name: 'products', + method: 'POST', + remote: window.location.pathname + '?search=' + searchType + '&query=%QUERY', + datumTokenizer: function(d) { + return Bloodhound.tokenizers.whitespace(d.val) + }, + queryTokenizer: Bloodhound.tokenizers.whitespace, + limit: 5 + }) + + engine.initialize() + + /* + * Bind autocomplete to search field + */ + $(el) + .typeahead(null, { + displayKey: 'code', + source: engine.ttAdapter(), + minLength: 3, + templates: { + suggestion: template + } + }) + .on('typeahead:opened', function(){ + $(el + ' .tt-dropdown-menu').css('width', $(el).width() + 'px') + }) + .on('typeahead:selected', function(object, datum){ + $form.submit() + }) + .on('keypress', function(e) { + if (e.keyCode == 13) // ENTER key + $form.submit() + }) + } + + InstallProcess.prototype.searchSubmit = function(el) { + var + $el = $(el), + $input = $el.find('.product-search-input.tt-input:first') + + $el.popup() + + $input.typeahead('val', '') + } + + // + // Suggested products + // + + InstallProcess.prototype.bindSuggested = function(el) { + var + self = this, + handler = $(el).data('handler') + + $.request(handler).done(function(data){ + var result = data.result + if (!$.isArray(result)) return + self.renderSuggested(el, result) + }) + } + + InstallProcess.prototype.renderSuggested = function(el, suggestedProducts) { + var self = this, + $el = $(el), + $container = $el.closest('.suggested-products-container'), + partialView = $(el).data('view') + + if (suggestedProducts.length == 0) { + $container.hide() + } + else { + $container.show() + $.each(suggestedProducts, function(index, product) { + self.renderPartial($el, partialView, product, { append:true }) + }) + } + } + + // + // Helpers + // + + InstallProcess.prototype.renderPartial = function($el, name, data, options) { + var container = $el, + template = $('[data-partial="' + name + '"]'), + contents = Mustache.to_html(template.html(), data) + + options = $.extend(true, { + append: false + }, options) + + if (options.append) container.append(contents) + else container.html(contents) + } + + if ($.oc === undefined) + $.oc = {} + + $.oc.installProcess = new InstallProcess; + +}(window.jQuery); + + +/*! + * typeahead.js 0.10.1 + * https://github.com/twitter/typeahead.js + * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT + */ + +!function(a){var b={isMsie:function(){return/(msie|trident)/i.test(navigator.userAgent)?navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]:!1},isBlankString:function(a){return!a||/^\s*$/.test(a)},escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isString:function(a){return"string"==typeof a},isNumber:function(a){return"number"==typeof a},isArray:a.isArray,isFunction:a.isFunction,isObject:a.isPlainObject,isUndefined:function(a){return"undefined"==typeof a},bind:a.proxy,each:function(b,c){function d(a,b){return c(b,a)}a.each(b,d)},map:a.map,filter:a.grep,every:function(b,c){var d=!0;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?void 0:!1}),!!d):d},some:function(b,c){var d=!1;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?!1:void 0}),!!d):d},mixin:a.extend,getUniqueId:function(){var a=0;return function(){return a++}}(),templatify:function(b){function c(){return String(b)}return a.isFunction(b)?b:c},defer:function(a){setTimeout(a,0)},debounce:function(a,b,c){var d,e;return function(){var f,g,h=this,i=arguments;return f=function(){d=null,c||(e=a.apply(h,i))},g=c&&!d,clearTimeout(d),d=setTimeout(f,b),g&&(e=a.apply(h,i)),e}},throttle:function(a,b){var c,d,e,f,g,h;return g=0,h=function(){g=new Date,e=null,f=a.apply(c,d)},function(){var i=new Date,j=b-(i-g);return c=this,d=arguments,0>=j?(clearTimeout(e),e=null,g=i,f=a.apply(c,d)):e||(e=setTimeout(h,j)),f}},noop:function(){}},c="0.10.1",d=function(){function a(a){this.maxSize=a||100,this.size=0,this.hash={},this.list=new c}function c(){this.head=this.tail=null}function d(a,b){this.key=a,this.val=b,this.prev=this.next=null}return b.mixin(a.prototype,{set:function(a,b){var c,e=this.list.tail;this.size>=this.maxSize&&(this.list.remove(e),delete this.hash[e.key]),(c=this.hash[a])?(c.val=b,this.list.moveToFront(c)):(c=new d(a,b),this.list.add(c),this.hash[a]=c,this.size++)},get:function(a){var b=this.hash[a];return b?(this.list.moveToFront(b),b.val):void 0}}),b.mixin(c.prototype,{add:function(a){this.head&&(a.next=this.head,this.head.prev=a),this.head=a,this.tail=this.tail||a},remove:function(a){a.prev?a.prev.next=a.next:this.head=a.next,a.next?a.next.prev=a.prev:this.tail=a.prev},moveToFront:function(a){this.remove(a),this.add(a)}}),a}(this),e=function(){function a(a){this.prefix=["__",a,"__"].join(""),this.ttlKey="__ttl__",this.keyMatcher=new RegExp("^"+this.prefix)}function c(){return(new Date).getTime()}function d(a){return JSON.stringify(b.isUndefined(a)?null:a)}function e(a){return JSON.parse(a)}var f,g;try{f=window.localStorage,f.setItem("~~~","!"),f.removeItem("~~~")}catch(h){f=null}return g=f&&window.JSON?{_prefix:function(a){return this.prefix+a},_ttlKey:function(a){return this._prefix(a)+this.ttlKey},get:function(a){return this.isExpired(a)&&this.remove(a),e(f.getItem(this._prefix(a)))},set:function(a,e,g){return b.isNumber(g)?f.setItem(this._ttlKey(a),d(c()+g)):f.removeItem(this._ttlKey(a)),f.setItem(this._prefix(a),d(e))},remove:function(a){return f.removeItem(this._ttlKey(a)),f.removeItem(this._prefix(a)),this},clear:function(){var a,b,c=[],d=f.length;for(a=0;d>a;a++)(b=f.key(a)).match(this.keyMatcher)&&c.push(b.replace(this.keyMatcher,""));for(a=c.length;a--;)this.remove(c[a]);return this},isExpired:function(a){var d=e(f.getItem(this._ttlKey(a)));return b.isNumber(d)&&c()>d?!0:!1}}:{get:b.noop,set:b.noop,remove:b.noop,clear:b.noop,isExpired:b.noop},b.mixin(a.prototype,g),a}(),f=function(){function c(b){b=b||{},this._send=b.transport?e(b.transport):a.ajax,this._get=b.rateLimiter?b.rateLimiter(this._get):this._get}function e(c){return function(d,e){function f(a){b.defer(function(){h.resolve(a)})}function g(a){b.defer(function(){h.reject(a)})}var h=a.Deferred();return c(d,e,f,g),h}}var f=0,g={},h=6,i=new d(10);return c.setMaxPendingRequests=function(a){h=a},c.resetCache=function(){i=new d(10)},b.mixin(c.prototype,{_get:function(a,b,c){function d(b){c&&c(b),i.set(a,b)}function e(){f--,delete g[a],k.onDeckRequestArgs&&(k._get.apply(k,k.onDeckRequestArgs),k.onDeckRequestArgs=null)}var j,k=this;(j=g[a])?j.done(d):h>f?(f++,g[a]=this._send(a,b).done(d).always(e)):this.onDeckRequestArgs=[].slice.call(arguments,0)},get:function(a,c,d){var e;return b.isFunction(c)&&(d=c,c={}),(e=i.get(a))?b.defer(function(){d&&d(e)}):this._get(a,c,d),!!e}}),c}(),g=function(){function c(b){b=b||{},b.datumTokenizer&&b.queryTokenizer||a.error("datumTokenizer and queryTokenizer are both required"),this.datumTokenizer=b.datumTokenizer,this.queryTokenizer=b.queryTokenizer,this.datums=[],this.trie=e()}function d(a){return a=b.filter(a,function(a){return!!a}),a=b.map(a,function(a){return a.toLowerCase()})}function e(){return{ids:[],children:{}}}function f(a){for(var b={},c=[],d=0;db[e]?e++:(f.push(a[d]),d++,e++);return f}return b.mixin(c.prototype,{bootstrap:function(a){this.datums=a.datums,this.trie=a.trie},add:function(a){var c=this;a=b.isArray(a)?a:[a],b.each(a,function(a){var f,g;f=c.datums.push(a)-1,g=d(c.datumTokenizer(a)),b.each(g,function(a){var b,d,g;for(b=c.trie,d=a.split("");g=d.shift();)b=b.children[g]||(b.children[g]=e()),b.ids.push(f)})})},get:function(a){var c,e,h=this;return c=d(this.queryTokenizer(a)),b.each(c,function(a){var b,c,d,f;if(e&&0===e.length)return!1;for(b=h.trie,c=a.split("");b&&(d=c.shift());)b=b.children[d];return b&&0===c.length?(f=b.ids.slice(0),void(e=e?g(e,f):f)):(e=[],!1)}),e?b.map(f(e),function(a){return h.datums[a]}):[]},serialize:function(){return{datums:this.datums,trie:this.trie}}}),c}(),h=function(){function d(a){var c=a.local||null;return b.isFunction(c)&&(c=c.call(null)),c}function e(d){var e,f;return f={url:null,thumbprint:"",ttl:864e5,filter:null,ajax:{}},(e=d.prefetch||null)&&(e=b.isString(e)?{url:e}:e,e=b.mixin(f,e),e.thumbprint=c+e.thumbprint,e.ajax.type=e.ajax.type||"GET",e.ajax.dataType=e.ajax.dataType||"json",!e.url&&a.error("prefetch requires url to be set")),e}function f(c){function d(a){return function(c){return b.debounce(c,a)}}function e(a){return function(c){return b.throttle(c,a)}}var f,g;return g={url:null,wildcard:"%QUERY",replace:null,rateLimitBy:"debounce",rateLimitWait:300,send:null,filter:null,ajax:{}},(f=c.remote||null)&&(f=b.isString(f)?{url:f}:f,f=b.mixin(g,f),f.rateLimiter=/^throttle$/i.test(f.rateLimitBy)?e(f.rateLimitWait):d(f.rateLimitWait),f.ajax.type=f.ajax.type||"GET",f.ajax.dataType=f.ajax.dataType||"json",delete f.rateLimitBy,delete f.rateLimitWait,!f.url&&a.error("remote requires url to be set")),f}return{local:d,prefetch:e,remote:f}}(),i=(window.Bloodhound=function(){function c(b){b&&(b.local||b.prefetch||b.remote)||a.error("one of local, prefetch, or remote is required"),this.limit=b.limit||5,this.sorter=d(b.sorter),this.dupDetector=b.dupDetector||i,this.local=h.local(b),this.prefetch=h.prefetch(b),this.remote=h.remote(b),this.cacheKey=this.prefetch?this.prefetch.cacheKey||this.prefetch.url:null,this.index=new g({datumTokenizer:b.datumTokenizer,queryTokenizer:b.queryTokenizer}),this.storage=this.cacheKey?new e(this.cacheKey):null}function d(a){function c(b){return b.sort(a)}function d(a){return a}return b.isFunction(a)?c:d}function i(){return!1}var j;return j={data:"data",protocol:"protocol",thumbprint:"thumbprint"},c.tokenizers={whitespace:function(a){return a.split(/\s+/)},nonword:function(a){return a.split(/\W+/)}},b.mixin(c.prototype,{_loadPrefetch:function(b){function c(a){var c;c=b.filter?b.filter(a):a,f.add(c),f._saveToStorage(f.index.serialize(),b.thumbprint,b.ttl)}var d,e,f=this;return(d=this._readFromStorage(b.thumbprint))?(this.index.bootstrap(d),e=a.Deferred().resolve()):e=a.ajax(b.url,b.ajax).done(c),e},_getFromRemote:function(a,b){function c(a){var c=f.remote.filter?f.remote.filter(a):a;b(c)}var d,e,f=this;return a=a||"",e=encodeURIComponent(a),d=this.remote.replace?this.remote.replace(this.remote.url,a):this.remote.url.replace(this.remote.wildcard,e),this.transport.get(d,this.remote.ajax,c)},_saveToStorage:function(a,b,c){this.storage&&(this.storage.set(j.data,a,c),this.storage.set(j.protocol,location.protocol,c),this.storage.set(j.thumbprint,b,c))},_readFromStorage:function(a){var b,c={};return this.storage&&(c.data=this.storage.get(j.data),c.protocol=this.storage.get(j.protocol),c.thumbprint=this.storage.get(j.thumbprint)),b=c.thumbprint!==a||c.protocol!==location.protocol,c.data&&!b?c.data:null},initialize:function(){function b(){d.add(d.local)}var c,d=this;return c=this.prefetch?this._loadPrefetch(this.prefetch):a.Deferred().resolve(),this.local&&c.done(b),this.transport=this.remote?new f(this.remote):null,this.initialize=function(){return c.promise()},c.promise()},add:function(a){this.index.add(a)},get:function(a,c){function d(a){var d=e.slice(0);b.each(a,function(a){var c;return c=b.some(d,function(b){return f.dupDetector(a,b)}),!c&&d.push(a),d.length