[Enter Select]uc脚本版 - 20110318更新

(9,077 views)
October 7, 2010

2011-03-18:升级v0.02版修复一个bug
2010-10-29:Enter Select扩展升级了 —–> Speak Words

Speak Words现在已经成为了Mozilla Labs – Prospector的一个实验组件,可能未来会直接集成到firefox release中。
这个扩展除了提供Enter Select原有的功能外,在地址栏还加入了对输入的智能匹配功能。(这下完全跟Google Chrome一样了。。。)

比如:在地址栏输入s,扩展会自动补全到slimeden。


这里的匹配并不是基于字典的单词匹配,而是基于输入记录和浏览历史的,所以你输入s时,可能自动补全的并不是slimeden,而是sports什么的。

—————————————————————————–
2010-03-18:v0.02,修复在autocomplete popup列表变小的情况下,不能自动选中第一个条目的问题
2010-10-21:附件已更新,修复了与其他脚本冲突的问题。

附件下载,
enterSelect_v0.02.uc
enterSelect.uc

—————————————————————————–
从小明的blog上看到Enter Select这个扩展。

它提供的功能很简单,就是在地址栏输入时按回车会直接选中并执行第一个结果,不需要先按一下Down或者Tab来选中第一条。非常类似于Google Chrome地址栏相应的功能。

下载了xpi包后,会发现里面只有一个enterSelects.xul文件,并且该文件只提供js代码,没有界面元素,所以非常适合改写成uc脚本。
这篇文章详细解释扩展的代码,并把它修改成uc脚本。

下面来看一下enterSelects.xul的源码:

addEventListener("load", function() {
       ...
 }, false);

结构很简单,就是在firefox chrome主窗体load完时执行一个函数。
这里没有用window.addEventListener,实际是一样的,扩展执行环境的root对象就是window,可以省略不写。

【注意】在改写成uc脚本时,不能把代码放到load事件的处理函数里,否则不会执行,这是因为uc脚本是在主DOM窗口load完以后才执行的。

调用的函数里面最关键的一段代码

let (orig = popup._appendCurrentResult) {
        popup._appendCurrentResult = function() {
          // Run the original first to get results added
          orig.apply(this, arguments);

          // Don't bother if something is already selected
          if (popup.selectedIndex >= 0)
            return;

          // Make sure there's results
          if (popup.noResults)
            return;

          // Don't auto-select if we have a url
          if (gURLBar.willHandle)
            return;

          // We passed all the checks, so pretend the user has the first result
          // selected, so this causes the UI to show the selection style
          popup.selectedIndex = 0;

          // If the just-added result is what to auto-select, make it happen
          if (autoSelectOn == gURLBar.trimmedSearch) {
            gURLBar.controller.handleEnter(true);

            // Clear out what to auto-select now that we've done it once
            autoSelectOn = null;
          }

          // Remember this to notice if the search changes
          lastSearch = gURLBar.trimmedSearch;
        };
      }

popup对象就是代表地址栏弹出的auto complete结果选择框
这里是要重写popup对象的_appendCurrentResult方法(该方法在搜索出结果要弹出选择框前执行,用于向下拉菜单中填充内容),根据地址栏输入自动选中第一个匹配。

第1行,保存_appendCurrentResult原来对应的函数体(第4行用到)
第2-32行,重写_appendCurrentResult

第4行,新的_appendCurrentResult首先执行原来默认的_appendCurrentResult函数(用来向下拉菜单中填充匹配内容)
第7-8行,如果用户从下拉菜单已经选择了结果,那么不用处理了直接返回。

第11-12行,如果没有匹配的结果,也就是下拉菜单为空,那么也不用处理直接返回。
这里的noResults是个getter,不是firefox定义的方法,是扩展作者自定义的帮助函数

popup.__defineGetter__("noResults", function() {
        return this._matchCount == 0;
      });

其实就是判断_matchCount(匹配数量)是不是为0。
所以这里直接

if (popup._matchCount == 0) return;

也是可以的。

第15-16行,如果用户通过在地址栏直接输入一个URL地址来打开网页,这时候不应该从下拉菜单中选择第一个匹配结果(否则会造成用户没法手动输入地址访问网页),而是直接返回。
另外,用户还有可能用搜索引擎关键字在地址栏直接搜索,这种情况也不应该从下拉菜单中选择第一个匹配结果。
这里willHandle也是作者的自定义函数

gURLBar.__defineGetter__("willHandle", function() {
        // Potentially it's a url if there's no spaces
        let search = this.trimmedSearch;
        if (search.match(/ /) == null) {
          try {
            // Quit early if the input is already a URI
            return Cc["@mozilla.org/network/io-service;1"].
              getService(Ci.nsIIOService).newURI(gURLBar.value, null, null);
          }
          catch(ex) {}

          try {
            // Quit early if the input is domain-like (e.g., site.com/page)
            return Cc["@mozilla.org/network/effective-tld-service;1"].
              getService(Ci.nsIEffectiveTLDService).
              getBaseDomainFromHost(gURLBar.value);
          }
          catch(ex) {}
        }

        // Check if there's an search engine registered for the first keyword
        let keyword = search.split(/\s+/)[0];
        return Cc["@mozilla.org/browser/search-service;1"].
          getService(Ci.nsIBrowserSearchService).getEngineByAlias(keyword);
      });

分为2个部分
第一部分,如果输入中没有空格,就用firefox的io-service和effective-tld-service去解析输入看看它是不是URL,是的话就返回不处理。
第二部分,检查输入的第一个空格前的内容是不是给搜索引擎设置的关键字,是的话直接返回不处理。

第20行,如果不是上面说的这些特殊情况,那么一般情况下,就自动选择第一条匹配结果。
到这里其实已经为输入选择好了第一个匹配结果。

第23-31行,用来处理一种由于异步操作而导致的异常情况,下面详述。

在上面的代码第20行已经根据输入选择好第一个匹配结果,下面还需要处理一下”回车”键来确认提交。

gURLBar.addEventListener("keydown", function(aEvent) {
        switch (aEvent.keyCode) {
          // For movement keys, unselect the first item to allow editing
          case KeyEvent.DOM_VK_LEFT:
          case KeyEvent.DOM_VK_RIGHT:
          case KeyEvent.DOM_VK_HOME:
            popup.selectedIndex = -1;
            return;

          // We're interested in handling enter (return), do so below
          case KeyEvent.DOM_VK_RETURN:
            break;

          // For anything else, just ignore
          default:
            return;
        }

        // Ignore special key combinations
        if (aEvent.shiftKey || aEvent.ctrlKey || aEvent.metaKey)
          return;

        // Deselect if the selected result isn't for the current search
        if (!popup.noResults && lastSearch != gURLBar.trimmedSearch) {
          popup.selectedIndex = -1;

          // If it's not a url, we'll want to auto-select the first result
          if (!gURLBar.willHandle) {
            autoSelectOn = gURLBar.trimmedSearch;

            // Don't load what's typed in the location bar because it's a search
            aEvent.preventDefault();
          }

          return;
        }

        // Pretend the user pressed right in the location bar which will cause
        // the selected index to be filled in. If the user has already pressed
        // down to some other selection, it'll just show the same value.
        gURLBar.controller.handleKeyNavigation(KeyEvent.DOM_VK_RIGHT);
      }, false);

可以很清楚的看到,这一段是用来处理地址栏里的键盘输入的。
第4-8行,如果是左/右/Home键,这是在编辑输入,不应该自动选择第一个结果,所以取消选择,popup.selectedIndex = -1;
第11-12行,监听到“回车”键,从switch里break出来,通过20-35行重点处理这个“回车”事件。
第15-16行,其他的键盘输入,不用管,直接退出。

第20-21行,如果回车是和ctrl/alt/shift一起按的,不处理。
第24-36行,和_appendCurrentResult函数的第23-31行一起来解决一个问题。
回想一下整个操作过程是这样的,用户在地址栏输入内容,然后浏览器会根据输入的内容去访问历史、书签啊等这些数据库,去查找匹配结果,然后得到结果时把结果呈现在下拉菜单中(通过_appendCurrentResult函数);在这之后,enterSelect自动选择第一个匹配结果;用户按“回车”键提交。如果在用户按“回车”前,他改变主意修改了输入内容,整个过程会重新来一遍。但是由于查找匹配结果是需要花时间的,这个时间间隔前,用户再次“回车”提交。由于新的查找还没有完成,导致_appendCurrentResult还未执行,新的匹配项还未选择。这时候提交的地址是根据上一次输入得到的第一个匹配结果,而不是新的。问题就出现了。
所以这几行代码就是为了避免修改了输入(gURLBar.trimmedSearch变了)但是新lastSearch还没有生成(lastSearch还是对应老输入)这种情况的。这时候,应该取消上一次的选择(第25行),并等待_appendCurrentResult函数执行时为新的输入重新选择结果。
最后第32行阻止这个“回车”事件默认的提交动作。

第41行,通过popup.selectedIndex从下拉菜单中选择了条目后,直接“出车”并不会跳转到条目对应的网页。
比如我现在在地址栏输入abc,然后假设第一个匹配条目是www.abc.com,然后我通过设置popup.selectedIndex=0,并回车确认。这时候实际上浏览器会去访问abc这个地址,而不是www.abc.com,其结果是abc页面打不开。这是为什么呢?这是因为浏览器是根据地址栏里的内容去访问页面的,只设置selectedIndex并不会用其对应的地址去更新地址栏的值。
第41行就是为了解决这个问题的。通过模拟用户按右键,来更新selectedIndex对应地址到地址栏。
我们可以亲自试试右键的效果,如果地址栏已经弹出下拉菜单了,用鼠标悬停在某个匹配项上,地址栏的内容是不会更新的,这时候如果按一下右键,匹配项的地址就会更新到地址栏。

如果都不是这些特殊情况,那么最后这个“回车”事件会由firefox监听到,按照“回车”的默认处理来提交地址栏的输入。

看完了整个代码,发现只要注意别把代码放到window的load处理函数里,其他的直接另存为uc脚本就可以,都不用做任何修改。


related post

(9,077 views)

21 Responses to [Enter Select]uc脚本版

  1. Hailo says:

    好像不能使用、、、

    • admin says:

      还真是,谢谢提醒。
      uc脚本加载执行时chrome窗口已经load完了,所以代码没执行。

      不需要把处理代码放到load的listener里面。附件已更新。

      • harnack says:

        那为啥扩展里要用listener呢?是不是因为扩展比uc脚本加载的时间要早呢?

      • admin says:

        对于扩展准确的加载起点我还不是太清楚。(希望高人指点)
        但是,扩展比uc脚本加载得早,这点很容易理解,因为uc脚本是由userChrome这个扩展来加载的。
        另外,从userChrome的源码能看到uc脚本是在DOM Window load完以后执行的,所以肯定脚本代码没法监听到第一个DOM Window窗口(就是firefox的程序窗口)的load事件。
        (如果userChrome.js文件里面用最通用的加载规则userChrome.import(“*”, “UChrm”);,那么脚本会在任何一个DOM Window load完都会加载一遍)

      • harnack says:

        呵呵,我是菜鸟,只会用不会写,或许我行我速那些经常写脚本的高手知道加载的时间吧。

        这么一说UC脚本加载的晚倒是可以理解的。另外我以前也是用
        userChrome.import(“*”, “UChrm”);
        的方式加载脚本,后来发现错误控制台里总是一大堆错误,才晓得最好在前面加个if语句判断下是不是主窗口。

  2. harnack says:

    赞一个,难得看到这么详细的技术贴(文章长度堪比电脑玩物了^^),博主肯定花了不少心思吧。很少有高手像博主这样耐心讲解代码的。

    看完最大的感受是写扩展的确不容易,需要考虑如此多的细节,稍微考虑不周全就会带来各种bug。那个模拟右键真的很有意思呢,不自己试很难发现这么做的原因。

    • admin says:

      读源码容易些,讲解起来好麻烦,想说得清楚些,结果越写越长越啰嗦。。。

      • harnack says:

        罗嗦点好,这样我们能多看懂一点。^^ 说实话你要是不讲的话这些程序对我这种不是搞代码的人来说简直是如看天书。

        ps:博主可以考虑添加个头像哦。

  3. alan says:

    这个脚本很不错;
    楼主很热心,解释得很详细。

  4. harnack says:

    的确如此,我所读过的最详尽的脚本剖析文章了。
    ps:好久没来,刚刚才注意到这篇文章已经被博主更新过了。

    • admin says:

      对于以前的文章,我有新想法、新内容,或者发现错误什么的,都会更新。
      所以旧文章常回头看看,说不定能有新发现^_^
      侧栏的“最近更新”可以看到我最近更新的文章

      也欢迎大家,指出文章中的错误或者补充内容。

  5. harnack says:

    原来如此,明白了。平时一般用greader订阅博客,不过里面看不到旧文章的更新,看来以后得常点进来看看。

  6. harnack says:

    另外有点诡异的是博主Reply的时候能够将留言嵌套在以前的留言里,而我点击Reply留言后却会新开一楼。

    • admin says:

      以前修复过这个问题,由于wordpress升级了,修复又给覆盖了。。。
      回头再看看

  7. alan says:

    能不能改进与cybersearch的兼容,如果第一项结果是cybersearch的,脚本就不起作用。

    • admin says:

      好,回头看一下。

    • admin says:

      查了好久。。。
      不是与cybersearch扩展的兼容性问题,是脚本自己的bug。
      当你输入新的字符导致建议列表在原来的基础上变小时,第一个条目就不再被自动选中了。

      试试v0.02版

  8. alan says:

    原来留言不能带网址;
    firefox 4可用的要到作者网站下载,网址贴不出来,google“”cybersearch“”第二项就是了。

    • admin says:

      这个主要是为了防垃圾留言,垃圾留言实在太多了。
      有时候每天都有几百。。。

  9. alan says:

    0.02版可以了;
    谢谢博主。

①若要贴代码,请将 "<" 改成 "&lt;",">" 改成 "&gt;".
②若要从他人留言中复制代码,注意检查引号可能是中文的,请手动修改成英文符号,避免不能工作