From 8835026c1c0ce6591a43b06cee0f3ef39effae0f Mon Sep 17 00:00:00 2001 From: Anton Khodakivskiy Date: Tue, 4 Sep 2012 17:09:59 -0400 Subject: [PATCH] Finally implemented hints and markers, as well as most of the commands. Began writing some dev docs. --- bootstrap.coffee | 1 - docs/bootstrap.html | 56 +++++++++ docs/commands.html | 98 +++++++++++++++ docs/console.html | 30 +++++ docs/docco.css | 192 ++++++++++++++++++++++++++++ docs/event-handlers.html | 37 ++++++ docs/hints.html | 40 ++++++ docs/marker.html | 147 ++++++++++++++++++++++ docs/utils.html | 207 +++++++++++++++++++++++++++++++ docs/vim.html | 68 ++++++++++ docs/window-utils.html | 57 +++++++++ install.rdf | 5 +- packages/commands.coffee | 220 +++++++++++++++++++++++---------- packages/event-handlers.coffee | 49 ++------ packages/hints.coffee | 74 +++-------- packages/marker.coffee | 159 +++++++++++++++++++++--- packages/utils.coffee | 132 +++++++++++++++++--- packages/vim.coffee | 73 ++++++++--- resources/vimff.css | 34 ++--- 19 files changed, 1436 insertions(+), 243 deletions(-) create mode 100644 docs/bootstrap.html create mode 100644 docs/commands.html create mode 100644 docs/console.html create mode 100644 docs/docco.css create mode 100644 docs/event-handlers.html create mode 100644 docs/hints.html create mode 100644 docs/marker.html create mode 100644 docs/utils.html create mode 100644 docs/vim.html create mode 100644 docs/window-utils.html diff --git a/bootstrap.coffee b/bootstrap.coffee index c29bb95..9ded61c 100644 --- a/bootstrap.coffee +++ b/bootstrap.coffee @@ -45,7 +45,6 @@ tracker = new WindowEventTracker handlers - startup = (data, reason) -> loadCss 'vimff' tracker.start() diff --git a/docs/bootstrap.html b/docs/bootstrap.html new file mode 100644 index 0000000..7dbf8bc --- /dev/null +++ b/docs/bootstrap.html @@ -0,0 +1,56 @@ + bootstrap.coffee

bootstrap.coffee

"use strict"
+
+{ classes: Cc, interfaces: Ci, utils: Cu } = Components
+
+((global) ->
+
+  tools = {}
+  Cu.import "resource://gre/modules/Services.jsm", tools
+  baseURI = tools.Services.io.newURI __SCRIPT_URI_SPEC__, null, null
+
+  include = (src, scope = {}) ->
+    try
+      uri = tools.Services.io.newURI "packages/#{ src }.js", null, baseURI
+      tools.Services.scriptloader.loadSubScript uri.spec, scope
+    catch error
+      uri = tools.Services.io.newURI src, null, baseURI
+      tools.Services.scriptloader.loadSubScript uri.spec, scope
+
+    return scope
+  
+
+  modules = {}
+  require = (src) ->
+    if modules[src]
+      return modules[src]
+    else
+      scope = 
+        require: require
+        include: include
+        exports: {}
+
+      include src, scope
+
+      return modules[src] = scope.exports;
+
+  Console = require("console").Console
+  global.console = new Console "vimff"
+  global.include = include
+  global.require = require
+
+)(this);
+
+{ WindowEventTracker, loadCss, unloadCss, } = require 'utils'
+{ handlers } = require 'event-handlers'
+
+tracker = new WindowEventTracker handlers
+
+startup = (data, reason) ->
+  loadCss 'vimff'
+  tracker.start()
+
+shutdown = (data, reason) ->
+  tracker.stop()
+  unloadCss 'vimff'
+
+
\ No newline at end of file diff --git a/docs/commands.html b/docs/commands.html new file mode 100644 index 0000000..fb655e3 --- /dev/null +++ b/docs/commands.html @@ -0,0 +1,98 @@ + commands.coffee

commands.coffee

SCROLL_AMOUNT = 60
+
+{ classes: Cc, interfaces: Ci, utils: Cu } = Components
+
+utils = require 'utils'
+{ handleHintChar, 
+  injectHints, 
+  removeHints, 
+} = require 'hints'
+
+commands = 

Navigate to the address that is currently stored in the system clipboard

  'p':  (vim) ->
+    vim.window.location.assign utils.readFromClipboard()
+    

Open new tab and navigate to the address that is currently stored in the system clipboard

  'P':  (vim) ->
+    if chromeWindow = utils.getRootWindow vim.window
+      if gBrowser = chromeWindow.gBrowser
+        gBrowser.selectedTab = gBrowser.addTab utils.readFromClipboard()

Open new tab and focus the address bar

  't':  (vim) ->
+    if chromeWindow = utils.getRootWindow vim.window
+      if gBrowser = chromeWindow.gBrowser
+        gBrowser.selectedTab = chromeWindow.gBrowser.addTab()

Copy current URL to the clipboard

  'y,f': (vim) ->
+    vim.markers = injectHints vim.window.document, 

This callback will be called with the selected marker as argument

    vim.cb = (marker) ->
+      if url = marker.element.href
+        utils.writeToClipboard url
+
+    vim.enterHintsMode()

Copy current URL to the clipboard

  'y,y': (vim) ->
+    utils.writeToClipboard vim.window.location.toString()

Reload the page, possibly from cache

  'r': (vim) ->
+    vim.window.location.reload(false)

Reload the page from the server

  'R': (vim) ->
+    vim.window.location.reload(false)

Scroll to the top of the page

  'g,g': (vim) ->
+    vim.window.scrollTo(0, 0)

Scroll to the bottom of the page

  'G': (vim) ->
+    vim.window.scrollTo(0, vim.window.document.body.scrollHeight)

Scroll down a bit

  'j': (vim) -> 
+    vim.window.scrollBy(0, SCROLL_AMOUNT)

Scroll up a bit

  'k': (vim) -> 
+    vim.window.scrollBy(0, -SCROLL_AMOUNT)

Scroll down a page

  'c-d': (vim) ->
+    vim.window.scrollBy(0, vim.window.innerHeight)

Scroll up a page

  'c-u': (vim) ->
+    vim.window.scrollBy(0, -vim.window.innerHeight)

Activate previous tab

  'J|g,T': (vim) ->
+    if rootWindow = utils.getRootWindow vim.window
+      rootWindow.gBrowser.tabContainer.advanceSelectedTab(-1, true);

Activate next tab

  'K|g,t': (vim) ->
+    if rootWindow = utils.getRootWindow vim.window
+      rootWindow.gBrowser.tabContainer.advanceSelectedTab(1, true);

Go to the first tab

  'g,^': (vim) ->
+    if rootWindow = utils.getRootWindow vim.window
+      rootWindow.gBrowser.tabContainer.selectedIndex = 0;

Go to the last tab

  'g,$': (vim) ->
+    if rootWindow = utils.getRootWindow vim.window
+      itemCount = rootWindow.gBrowser.tabContainer.itemCount;
+      rootWindow.gBrowser.tabContainer.selectedIndex = itemCount - 1;

Go back in history

  'H': (vim) ->
+    vim.window.history.back()
+    

Go forward in history

  'L': (vim) ->
+    vim.window.history.forward()

Close current tab

  'x': (vim) ->
+    if rootWindow = utils.getRootWindow vim.window
+      rootWindow.gBrowser.removeCurrentTab()

Restore last closed tab

  'X': (vim) ->
+    if rootWindow = utils.getRootWindow vim.window
+      ss = utils.getSessionStore()
+      if ss and ss.getClosedTabCount(rootWindow) > 0
+        ss.undoCloseTab rootWindow, 0

Follow links with hint markers

  'f': (vim) ->
+    vim.markers = injectHints vim.window.document, 

This callback will be called with the selected marker as argument

    vim.cb = (marker) ->
+      marker.element.focus()
+      utils.simulateClick marker.element
+
+    vim.enterHintsMode()
+    

Follow links in a new Tab with hint markers

  'F': (vim) ->
+    vim.markers = injectHints vim.window.document, 

This callback will be called with the selected marker as argument

    vim.cb = (marker) ->
+      marker.element.focus()
+      utils.simulateClick marker.element, metaKey: true
+
+    vim.enterHintsMode()
+
+  'Esc': (vim) ->

Blur active element if it's editable. Other elements +aren't blurred - we don't want to interfere with +the browser too much

    activeElement = vim.window.document.activeElement
+    if utils.isElementEditable activeElement
+      activeElement.blur()

Remove hints and enter normal mode

    removeHints vim.window.document
+    vim.enterNormalMode()
+
+hintCharHandler = (vim, char) ->
+  maxCount = 0
+  for hint, marker of vim.markers
+    count = marker.matchHintChar char
+    maxCount = Math.max count, maxCount
+
+  for hint, marker of vim.markers
+    if marker.matchedHintCharCount == marker.hintChars.length == maxCount
+      console.log marker.hintChars
+      vim.cb marker
+      removeHints vim.window.document
+      vim.enterNormalMode()
+      break
+
+    if marker.matchedHintCharCount < maxCount
+      marker.hide()
+    else
+      marker.show()
+
+exports.hintCharHandler = hintCharHandler
+exports.commands        = do ->
+  newCommands = {}
+  for keys, command of commands
+    for key in keys.split '|'
+      newCommands[key] = command
+  return newCommands
+
+
\ No newline at end of file diff --git a/docs/console.html b/docs/console.html new file mode 100644 index 0000000..bb50f7b --- /dev/null +++ b/docs/console.html @@ -0,0 +1,30 @@ + console.coffee

console.coffee

"use strict"
+
+stringify = (arg) ->
+  try 
+    return String(arg)
+  catch error
+    return "<toString() error>"
+
+message = (prefix, level, args) ->
+  dump("#{ prefix } - #{ level }: #{ Array.map(args, stringify).join(" ") }\n") 
+
+class Console
+  constructor: (@prefix='extension') ->
+
+  log: -> message(@prefix, 'log', arguments)
+  info: -> message(@prefix, 'info', arguments)
+  error: -> message(@prefix, 'error', arguments)
+  warning: -> message(@prefix, 'warning', arguments)
+  expand: ->
+    for arg in arguments
+      if typeof(arg) == "object"
+        len = Object.keys(arg).length
+        for k, v of arg
+          @log k, v
+      else
+        @log arg
+
+exports.Console = Console
+
+
\ No newline at end of file diff --git a/docs/docco.css b/docs/docco.css new file mode 100644 index 0000000..04cc7ec --- /dev/null +++ b/docs/docco.css @@ -0,0 +1,192 @@ +/*--------------------- Layout and Typography ----------------------------*/ +body { + font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; + font-size: 15px; + line-height: 22px; + color: #252519; + margin: 0; padding: 0; +} +a { + color: #261a3b; +} + a:visited { + color: #261a3b; + } +p { + margin: 0 0 15px 0; +} +h1, h2, h3, h4, h5, h6 { + margin: 0px 0 15px 0; +} + h1 { + margin-top: 40px; + } +hr { + border: 0 none; + border-top: 1px solid #e5e5ee; + height: 1px; + margin: 20px 0; +} +#container { + position: relative; +} +#background { + position: fixed; + top: 0; left: 525px; right: 0; bottom: 0; + background: #f5f5ff; + border-left: 1px solid #e5e5ee; + z-index: -1; +} +#jump_to, #jump_page { + background: white; + -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; + -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; + font: 10px Arial; + text-transform: uppercase; + cursor: pointer; + text-align: right; +} +#jump_to, #jump_wrapper { + position: fixed; + right: 0; top: 0; + padding: 5px 10px; +} + #jump_wrapper { + padding: 0; + display: none; + } + #jump_to:hover #jump_wrapper { + display: block; + } + #jump_page { + padding: 5px 0 3px; + margin: 0 0 25px 25px; + } + #jump_page .source { + display: block; + padding: 5px 10px; + text-decoration: none; + border-top: 1px solid #eee; + } + #jump_page .source:hover { + background: #f5f5ff; + } + #jump_page .source:first-child { + } +table td { + border: 0; + outline: 0; +} + td.docs, th.docs { + max-width: 450px; + min-width: 450px; + min-height: 5px; + padding: 10px 25px 1px 50px; + overflow-x: hidden; + vertical-align: top; + text-align: left; + } + .docs pre { + margin: 15px 0 15px; + padding-left: 15px; + } + .docs p tt, .docs p code { + background: #f8f8ff; + border: 1px solid #dedede; + font-size: 12px; + padding: 0 0.2em; + } + .pilwrap { + position: relative; + } + .pilcrow { + font: 12px Arial; + text-decoration: none; + color: #454545; + position: absolute; + top: 3px; left: -20px; + padding: 1px 2px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + } + td.docs:hover .pilcrow { + opacity: 1; + } + td.code, th.code { + padding: 14px 15px 16px 25px; + width: 100%; + vertical-align: top; + background: #f5f5ff; + border-left: 1px solid #e5e5ee; + } + pre, tt, code { + font-size: 12px; line-height: 18px; + font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; + margin: 0; padding: 0; + } + + +/*---------------------- Syntax Highlighting -----------------------------*/ +td.linenos { background-color: #f0f0f0; padding-right: 10px; } +span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } +body .hll { background-color: #ffffcc } +body .c { color: #408080; font-style: italic } /* Comment */ +body .err { border: 1px solid #FF0000 } /* Error */ +body .k { color: #954121 } /* Keyword */ +body .o { color: #666666 } /* Operator */ +body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +body .cp { color: #BC7A00 } /* Comment.Preproc */ +body .c1 { color: #408080; font-style: italic } /* Comment.Single */ +body .cs { color: #408080; font-style: italic } /* Comment.Special */ +body .gd { color: #A00000 } /* Generic.Deleted */ +body .ge { font-style: italic } /* Generic.Emph */ +body .gr { color: #FF0000 } /* Generic.Error */ +body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +body .gi { color: #00A000 } /* Generic.Inserted */ +body .go { color: #808080 } /* Generic.Output */ +body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +body .gs { font-weight: bold } /* Generic.Strong */ +body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +body .gt { color: #0040D0 } /* Generic.Traceback */ +body .kc { color: #954121 } /* Keyword.Constant */ +body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ +body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ +body .kp { color: #954121 } /* Keyword.Pseudo */ +body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ +body .kt { color: #B00040 } /* Keyword.Type */ +body .m { color: #666666 } /* Literal.Number */ +body .s { color: #219161 } /* Literal.String */ +body .na { color: #7D9029 } /* Name.Attribute */ +body .nb { color: #954121 } /* Name.Builtin */ +body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +body .no { color: #880000 } /* Name.Constant */ +body .nd { color: #AA22FF } /* Name.Decorator */ +body .ni { color: #999999; font-weight: bold } /* Name.Entity */ +body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +body .nf { color: #0000FF } /* Name.Function */ +body .nl { color: #A0A000 } /* Name.Label */ +body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +body .nt { color: #954121; font-weight: bold } /* Name.Tag */ +body .nv { color: #19469D } /* Name.Variable */ +body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +body .w { color: #bbbbbb } /* Text.Whitespace */ +body .mf { color: #666666 } /* Literal.Number.Float */ +body .mh { color: #666666 } /* Literal.Number.Hex */ +body .mi { color: #666666 } /* Literal.Number.Integer */ +body .mo { color: #666666 } /* Literal.Number.Oct */ +body .sb { color: #219161 } /* Literal.String.Backtick */ +body .sc { color: #219161 } /* Literal.String.Char */ +body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ +body .s2 { color: #219161 } /* Literal.String.Double */ +body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +body .sh { color: #219161 } /* Literal.String.Heredoc */ +body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +body .sx { color: #954121 } /* Literal.String.Other */ +body .sr { color: #BB6688 } /* Literal.String.Regex */ +body .s1 { color: #219161 } /* Literal.String.Single */ +body .ss { color: #19469D } /* Literal.String.Symbol */ +body .bp { color: #954121 } /* Name.Builtin.Pseudo */ +body .vc { color: #19469D } /* Name.Variable.Class */ +body .vg { color: #19469D } /* Name.Variable.Global */ +body .vi { color: #19469D } /* Name.Variable.Instance */ +body .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/event-handlers.html b/docs/event-handlers.html new file mode 100644 index 0000000..e7856cb --- /dev/null +++ b/docs/event-handlers.html @@ -0,0 +1,37 @@ + event-handlers.coffee

event-handlers.coffee

utils           = require 'utils'
+{ getCommand }  = require 'commands'
+{ Vim }         = require 'vim'
+
+{ interfaces: Ci } = Components
+
+vimBucket = new utils.Bucket utils.getWindowId, (obj) -> new Vim obj
+
+suppressEvent = (event) ->
+  event.preventDefault()
+  event.stopPropagation()

The following handlers are installed on every top level DOM window

handlers = 
+  'keypress': (event) ->
+    try
+      isEditable =  utils.isElementEditable event.originalTarget
+      key = utils.keyboardEventKey event

We only handle the key if there is no focused editable element +or if it's the Esc key, which will remote the focus from +the currently focused element

      if key and (key == 'Esc' or not isEditable)
+        if window = utils.getEventTabWindow event
+          if vimBucket.get(window)?.keypress key
+            suppressEvent event
+    catch err
+      console.log err
+
+  'TabClose': (event) ->
+    if gBrowser = utils.getEventTabBrowser event
+      if browser = gBrowser.getBrowserForTab event.originalTarget
+        vimBucket.forget browser.contentWindow.wrappedJSObject
+
+  'DOMWindowClose': (event) ->
+    if gBrowser = event.originalTarget.gBrowser
+      for tab in gBrowser.tabs
+        if browser = gBrowser.getBrowserForTab tab
+          vimBucket.forget browser.contentWindow.wrappedJSObject
+
+exports.handlers = handlers
+
+
\ No newline at end of file diff --git a/docs/hints.html b/docs/hints.html new file mode 100644 index 0000000..666aca2 --- /dev/null +++ b/docs/hints.html @@ -0,0 +1,40 @@ + hints.coffee

hints.coffee

CONTAINER_ID  = 'vimffHintMarkerContainer'
+
+{ interfaces: Ci }  = Components
+HTMLDocument        = Ci.nsIDOMHTMLDocument
+{ Marker }          = require 'marker'
+
+getHintsContainer = (document) ->
+  document.getElementById CONTAINER_ID
+
+createHintsContainer = (document) ->
+  container = document.createElement 'div'
+  container.id = CONTAINER_ID
+  container.className = 'vimffReset'
+  return container
+    
+injectHints = (document) ->
+  removeHints document
+
+  if document instanceof HTMLDocument
+    markers = Marker.createMarkers document
+
+    container = createHintsContainer document
+    for hint, marker of markers
+      container.appendChild marker.markerElement
+
+    document.body.appendChild container
+
+    return markers
+
+removeHints = (document, markers) ->
+  if container = getHintsContainer document
+    document.body.removeChild container 
+
+handleHintChar = (markers, char) ->
+
+exports.injectHints     = injectHints
+exports.removeHints     = removeHints
+exports.handleHintChar  = handleHintChar
+
+
\ No newline at end of file diff --git a/docs/marker.html b/docs/marker.html new file mode 100644 index 0000000..d1e4816 --- /dev/null +++ b/docs/marker.html @@ -0,0 +1,147 @@ + marker.coffee

marker.coffee

{ interfaces: Ci }      = Components
+XPathResult             = Ci.nsIDOMXPathResult
+
+HINTCHARS     = 'asdfgercvhjkl;uinm'

All elements that have one or more of the following properties +qualify for their own marker in hints mode

MARKABLE_ELEMENT_PROPERTIES = [
+  "@tabindex"
+  "@onclick"
+  "@onmousedown"
+  "@onmouseup"
+  "@oncommand"
+  "@role='link'"
+  "@role='button'"
+  "contains(@class, 'button')"
+  "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true'"
+]

All the following elements qualify for their own marker in hints mode

MARKABLE_ELEMENTS = [
+  "a"
+  "area[@href]"
+  "textarea"
+  "button" 
+  "select"
+  "input[not(@type='hidden' or @disabled or @readonly)]"
+]

Marker class wraps the markable element and provides +methods to manipulate the markers

class Marker

Creates the marker DOM node

  constructor: (@element) ->
+    document = @element.ownerDocument
+    window = document.defaultView
+    @markerElement = document.createElement 'div'
+    @markerElement.className = 'vimffReset vimffHintMarker'

Hides the marker

  hide: -> @markerElement.style.display = 'none'

Shows the marker

  show: -> @markerElement.style.display = 'block'

Positions the marker on the page. The positioning is absulute

  setPosition: (rect) ->
+    @markerElement.style.left = rect.left + 'px'
+    @markerElement.style.top  = rect.top  + 'px'

Assigns hint string to the marker

  setHint: (@hintChars) ->

number of hint chars that have been matched so far

    @matchedHintCharCount = 0 
+
+    document = @element.ownerDocument
+
+    while @markerElement.hasChildNodes()
+      @markerElement.removeChild @markedElement.firstChild
+
+    for char in @hintChars
+      span = document.createElement 'span'
+      span.className = 'vimffReset'
+      span.textContent = char.toUpperCase()
+      
+      @markerElement.appendChild span
+
+  matchHintChar: (char) ->
+    if char == 'backspace' 
+      if @matchedHintCharCount > 0
+        @matchedHintCharCount -= 1
+        @markerElement.children[@matchedHintCharCount].className = 'vimffReset'
+    else
+      if @hintChars[@matchedHintCharCount] == char
+        @markerElement.children[@matchedHintCharCount].className = 'vimffReset vimffCharMatch'
+        @matchedHintCharCount += 1
+
+    return @matchedHintCharCount
+
+  isComplete: ->
+    return @hintChars.length == @hintCompletion

Selects all markable elements on the page, creates markers +for each of them The markers are then positioned on the page

+ +

The array of markers is returned

Marker.createMarkers = (document) ->
+  elementsSet = getMarkableElements(document)
+  markers = {};
+  j = 0
+  for i in [0...elementsSet.snapshotLength] by 1
+    element = elementsSet.snapshotItem(i)
+    if rect = getElementRect element
+      hint = indexToHint(j++)
+      marker = new Marker(element)
+      marker.setPosition rect
+      marker.setHint hint
+      markers[hint] = marker
+
+  return markers

Function generator that creates a function that +returns hint string for supplied numeric index.

indexToHint = do ->

split the characters into two groups:

+ +
    +
  • left chars are used for the head
  • +
  • right chars are used to build the tail
  • +
  left = HINTCHARS[...HINTCHARS.length / 3]
+  right = HINTCHARS[HINTCHARS.length / 3...]

Helper function that returns a permutation number i +of some of the characters in the chars agrument

  f = (i, chars) ->
+    return '' if i < 0
+
+    n = chars.length
+    l = Math.floor(i / n); k = i % n;
+
+    return f(l - 1, chars) + chars[k]
+
+  return (i) ->
+    n = Math.floor(i / left.length)
+    m = i % left.length
+    return f(n - 1, right) + left[m]
+      

Returns elements that qualify for hint markers in hints mode. +Generates and memoizes an XPath query internally

getMarkableElements = do ->

Some preparations done on startup

  elements = Array.concat \
+    MARKABLE_ELEMENTS,
+    ["*[#{ MARKABLE_ELEMENT_PROPERTIES.join(" or ") }]"]
+
+  xpath = elements.reduce((m, rule) -> 
+    m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
+  , []).join(' | ')
+
+  namespaceResolver = (namespace) ->
+    if (namespace == "xhtml") then "http://www.w3.org/1999/xhtml" else null

The actual function that will return the desired elements

  return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
+    document.evaluate xpath, document.documentElement, namespaceResolver, resultType, null

Checks if the given TextRectangle object qualifies +for its own Marker with respect to the window object

isRectOk = (rect, window) ->
+  rect.width > 2 and rect.height > 2 and \
+  rect.top > -2 and rect.left > -2 and \
+  rect.top < window.innerHeight - 2 and \
+  rect.left < window.innerWidth - 2

Will scan through element.getClientRects() and look for +the first visible rectange. If there are no visible rectangles, then +will look at the children of the markable node.

+ +

The logic has been copied over from Vimiun

getElementRect = (element) ->
+  document = element.ownerDocument
+  window   = document.defaultView
+  docElem  = document.documentElement
+  body     = document.body
+
+  clientTop  = docElem.clientTop  || body.clientTop  || 0;
+  clientLeft = docElem.clientLeft || body.clientLeft || 0;
+  scrollTop  = window.pageYOffset || docElem.scrollTop;
+  scrollLeft = window.pageXOffset || docElem.scrollLeft;
+
+  rects = [rect for rect in element.getClientRects()]
+  rects.push element.getBoundingClientRect()
+
+  for rect in rects
+    if isRectOk rect, window
+      return {
+        top:    rect.top  + scrollTop  - clientTop
+        left:   rect.left + scrollLeft - clientLeft
+        width:  rect.width
+        height: rect.height
+      }

If the element has 0 dimentions then check what's inside. +Floated or absolutely positioned elements are of particular interest

  for rect in rects
+    if rect.width == 0 or rect.height == 0
+      for childElement in element.children
+        computedStyle = window.getComputedStyle childElement, null
+        if computedStyle.getPropertyValue 'float' != 'none' or \
+           computedStyle.getPropertyValue 'position' == 'absolute'
+
+          childRect if childRect = getElementRect childElement
+
+  return undefined
+
+exports.Marker              = Marker
+
+
\ No newline at end of file diff --git a/docs/utils.html b/docs/utils.html new file mode 100644 index 0000000..9867568 --- /dev/null +++ b/docs/utils.html @@ -0,0 +1,207 @@ + utils.coffee

utils.coffee

{ WindowTracker, isBrowserWindow } = require 'window-utils'
+
+{ interfaces: Ci, classes: Cc } = Components
+
+HTMLInputElement    = Ci.nsIDOMHTMLInputElement
+HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement
+HTMLSelectElement   = Ci.nsIDOMHTMLSelectElement
+XULDocument         = Ci.nsIDOMXULDocument
+XULElement          = Ci.nsIDOMXULElement
+HTMLDocument        = Ci.nsIDOMHTMLDocument
+HTMLElement         = Ci.nsIDOMHTMLElement
+Window              = Ci.nsIDOMWindow
+ChromeWindow        = Ci.nsIDOMChromeWindow
+KeyboardEvent       = Ci.nsIDOMKeyEvent
+
+_sss  = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService)
+_clip = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard)
+
+class Bucket
+  constructor: (@idFunc, @newFunc) ->
+    @bucket = {}
+
+  get: (obj) ->
+    id = @idFunc obj
+    if container = @bucket[id]
+      return container
+    else
+      return @bucket[id] = @newFunc obj
+
+  forget: (obj) ->
+    delete @bucket[id] if id = @idFunc obj
+
+
+class WindowEventTracker
+  constructor: (events, eventFilter = null) ->
+
+    handlerFilter = (handler) ->
+      return (event) ->
+        if !eventFilter or eventFilter event
+          handler event
+
+    addEventListeners = (window) ->
+      for name, handler of events
+        window.addEventListener name, handlerFilter(handler), false
+
+    removeEventListeners = (window) ->
+      for name, handler of events
+        window.removeEventListener name, handlerFilter(handler), false
+
+    @windowTracker = new WindowTracker
+      track: (window) -> 
+        if isBrowserWindow window
+          addEventListeners window
+
+      untrack: (window) ->
+        if isBrowserWindow window
+          removeEventListeners window
+
+  start: -> @windowTracker.start()
+  stop: -> @windowTracker.stop()
+
+isRootWindow = (window) -> 
+  window.location == "chrome://browser/content/browser.xul"
+
+getEventWindow = (event) ->
+  if event.originalTarget instanceof Window
+    return event.originalTarget
+  else 
+    doc = event.originalTarget.ownerDocument or event.originalTarget
+    if doc instanceof HTMLDocument or doc instanceof XULDocument
+      return doc.defaultView 
+
+getEventTabWindow = (event) ->
+  if window = getEventWindow event
+    if isRootWindow window
+      return window.gBrowser.tabs.selectedItem?.contentWindow.wrappedJSObject
+    else
+      return window
+
+getEventRootWindow = (event) ->
+  if window = getEventWindow event
+    return getRootWindow window
+
+getEventTabBrowser = (event) -> 
+  cw.gBrowser if cw = getEventRootWindow event
+
+getRootWindow = (window) ->
+  return window.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIWebNavigation)
+               .QueryInterface(Ci.nsIDocShellTreeItem)
+               .rootTreeItem
+               .QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Window); 
+
+isElementEditable = (element) ->
+  return element.isContentEditable or \
+         element instanceof HTMLInputElement or \
+         element instanceof HTMLTextAreaElement or \
+         element instanceof HTMLSelectElement
+
+getWindowId = (window) ->
+  return window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+               .getInterface(Components.interfaces.nsIDOMWindowUtils)
+               .outerWindowID
+
+getSessionStore = ->
+  Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);

Function that returns a URI to the css file that's part of the extension

cssUri = do () ->
+  tools = {}
+  Cu.import "resource://gre/modules/Services.jsm", tools
+  (name) ->
+    baseURI = tools.Services.io.newURI __SCRIPT_URI_SPEC__, null, null
+    uri = tools.Services.io.newURI "resources/#{ name }.css", null, baseURI
+    return uri

Loads the css identified by the name in the StyleSheetService as User Stylesheet +The stylesheet is then appended to every document, but it can be overwritten by +any user css

loadCss = (name) ->
+  _sss.loadAndRegisterSheet(cssUri(name), _sss.USER_SHEET)

Unloads the css file that's been loaded earlier with loadCss

unloadCss = (name) ->
+  uri = cssUri(name)
+  if _sss.sheetRegistered(uri, _sss.USER_SHEET)
+    _sss.unregisterSheet(uri, _sss.USER_SHEET)

processes the keyboard event and extracts string representation +of the key without modifiers in case this is the kind of a key +that can be handled by the extension

+ +

Currently we handle letters, Escape and Tab keys

keyboardEventChar = (keyboardEvent) ->
+  if keyboardEvent.charCode > 0
+    char = String.fromCharCode(keyboardEvent.charCode)
+    if char.match /\s/
+      char = undefined 
+  else
+    switch keyboardEvent.keyCode
+      when KeyboardEvent.DOM_VK_ESCAPE      then char = 'Esc'
+      when KeyboardEvent.DOM_VK_BACK_SPACE  then char = 'Backspace'
+      else char = undefined
+
+  return char

extracts string representation of the KeyboardEvent and adds +relevant modifiers (ctrl, alt, meta) in case they were pressed

keyboardEventKey = (keyboardEvent) ->
+  char = keyboardEventChar keyboardEvent
+
+  { 
+    shiftKey: shift, 
+    altKey:   alt, 
+    ctrlKey:  ctrl, 
+    metaKey:  meta, 
+  } = keyboardEvent
+
+  if alt or ctrl or meta
+    k = (a, b) -> if a then b else ''
+    return "<#{ k(ctrl, 'c') + k(alt, 'a') + k(meta, 'm') }-#{ char }>"
+  else
+    return char

Simulate mouse click with full chain of event +Copied from Vimium codebase

simulateClick = (element, modifiers) ->
+  document = element.ownerDocument
+  window = document.defaultView
+  modifiers ||= {}
+
+  eventSequence = ["mouseover", "mousedown", "mouseup", "click"]
+  for event in eventSequence
+    mouseEvent = document.createEvent("MouseEvents")
+    mouseEvent.initMouseEvent(event, true, true, window, 1, 0, 0, 0, 0, modifiers.ctrlKey, false, false,
+        modifiers.metaKey, 0, null)

Debugging note: Firefox will not execute the element's default action if we dispatch this click event, +but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately

    element.dispatchEvent(mouseEvent)

Write a string into system clipboard

writeToClipboard = (text) ->
+  str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+  str.data = text
+
+  trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+  trans.addDataFlavor("text/unicode");
+  trans.setTransferData("text/unicode", str, text.length * 2);
+
+  _clip.setData trans, null, Ci.nsIClipboard.kGlobalClipboard

Write a string into system clipboard

readFromClipboard = () ->
+  trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+  trans.addDataFlavor("text/unicode");
+
+  _clip.getData trans, Ci.nsIClipboard.kGlobalClipboard
+
+  str = {}
+  strLength = {}
+
+  trans.getTransferData("text/unicode", str, strLength)
+
+  if str
+    str = str.value.QueryInterface(Ci.nsISupportsString);
+    return str.data.substring 0, strLength.value / 2
+
+  return undefined
+
+
+exports.WindowEventTracker      = WindowEventTracker
+exports.Bucket                  = Bucket
+exports.isRootWindow            = isRootWindow
+exports.getEventWindow          = getEventWindow
+exports.getEventTabWindow       = getEventTabWindow
+exports.getEventRootWindow      = getEventRootWindow
+exports.getEventTabBrowser      = getEventTabBrowser
+
+exports.getWindowId             = getWindowId
+exports.getRootWindow           = getRootWindow
+exports.isElementEditable       = isElementEditable
+exports.getSessionStore         = getSessionStore
+
+exports.loadCss                 = loadCss
+exports.unloadCss               = unloadCss
+
+exports.keyboardEventKey        = keyboardEventKey
+exports.simulateClick           = simulateClick
+exports.readFromClipboard       = readFromClipboard
+exports.writeToClipboard        = writeToClipboard
+
+
\ No newline at end of file diff --git a/docs/vim.html b/docs/vim.html new file mode 100644 index 0000000..27106ac --- /dev/null +++ b/docs/vim.html @@ -0,0 +1,68 @@ + vim.coffee

vim.coffee

{ getWindowId, Bucket } = require 'utils'
+
+{ commands,
+  hintCharHandler 
+} = require 'commands'
+
+MODE_NORMAL = 1
+MODE_HINTS  = 2
+
+class Vim
+  constructor: (@window) ->
+    @mode     = MODE_NORMAL
+    @keys     = []
+    @markers  = undefined
+    @cb       = undefined
+
+  keypress: (key) ->
+    @keys.push key
+    if command = _getCommand(@mode, @keys)
+      command @
+      @keys = []
+      return key != 'Esc' 
+    else if _maybeCommand(@mode, @keys) 
+      return true
+    else
+      @keys.pop()
+      return false
+
+  enterHintsMode: () ->
+    @mode = MODE_HINTS
+
+  enterNormalMode: () ->
+    @markers = @cb = undefined
+
+    @mode = MODE_NORMAL
+
+_endsWithEsc = (keys) ->
+  return keys.join(',').match(/Esc$/)
+
+_getCommand = (mode, keys) ->
+  if mode == MODE_NORMAL or _endsWithEsc(keys)
+    sequence = keys.join(',')
+    if command = commands[sequence]
+      return command
+    else if keys.length > 0
+      return _getCommand mode, keys.slice(1)
+
+  else if mode == MODE_HINTS
+    return (vim) =>
+      char = keys[keys.length - 1].toLowerCase()
+      return hintCharHandler(vim, char)
+
+  return undefined
+
+_maybeCommand = (mode, keys) ->
+  if mode == MODE_NORMAL and keys.length > 0
+    sequence = keys.join(',')
+    for commandSequence in Object.keys(commands)
+      if commandSequence.search(sequence) == 0
+        return true
+
+    return _maybeCommand mode, keys.slice(1)
+
+  return false
+
+exports.Vim = Vim
+
+
\ No newline at end of file diff --git a/docs/window-utils.html b/docs/window-utils.html new file mode 100644 index 0000000..cace40b --- /dev/null +++ b/docs/window-utils.html @@ -0,0 +1,57 @@ + window-utils.coffee

window-utils.coffee

{ classes: Cc, interfaces: Ci, utils: Cu } = Components
+
+tools = {}
+Cu.import "resource://gre/modules/Services.jsm", tools
+
+ww = tools.Services.ww
+
+runOnWindowLoad = (callback, window) ->
+  if window.document.readyState == 'complete'
+    callback window
+  else
+    onLoad = ->
+      window.removeEventListener 'load', arguments.callee, false
+      callback(window)
+
+    window.addEventListener 'load', onLoad, false
+
+applyToWindows = (callback) ->
+  winEnum = ww.getWindowEnumerator()
+  while winEnum.hasMoreElements()
+    window = winEnum.getNext().QueryInterface(Ci.nsIDOMWindow)
+    runOnWindowLoad callback, window
+
+isBrowserWindow = (window) ->
+  return window.document.documentElement.getAttribute("windowtype") == "navigator:browser"
+
+class WindowObserver
+  constructor: (@delegate) ->
+
+  observe: (subject, topic, data) ->
+    window = subject.QueryInterface(Ci.nsIDOMWindow)
+    switch topic
+      when 'domwindowopened'
+        runOnWindowLoad @delegate.track, window
+      when 'domwindowclosed'
+        runOnWindowLoad @delegate.untrack, window
+
+class WindowTracker
+
+  constructor: (@delegate) ->
+    @observer = new WindowObserver @delegate
+
+  start: ->
+    applyToWindows @delegate.track
+    ww.registerNotification @observer
+
+  stop: ->
+    ww.unregisterNotification @observer
+    applyToWindows @delegate.untrack
+
+
+exports.runOnWindowLoad = runOnWindowLoad
+exports.applyToWindows = applyToWindows
+exports.WindowTracker = WindowTracker
+exports.isBrowserWindow = isBrowserWindow
+
+
\ No newline at end of file diff --git a/install.rdf b/install.rdf index e588135..d2dd6aa 100644 --- a/install.rdf +++ b/install.rdf @@ -16,7 +16,7 @@ {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 12.0 - 14.* + 15.* @@ -24,8 +24,5 @@ vimff Vim powered browsing Anton Khodakivskiy - - - diff --git a/packages/commands.coffee b/packages/commands.coffee index cec9430..5d5b834 100644 --- a/packages/commands.coffee +++ b/packages/commands.coffee @@ -3,79 +3,171 @@ SCROLL_AMOUNT = 60 { classes: Cc, interfaces: Ci, utils: Cu } = Components utils = require 'utils' -{ addHints, removeHints, hasHints } = require 'hints' +{ handleHintChar, + injectHints, + removeHints, +} = require 'hints' commands = - 'g,g': (window) -> - window.scrollTo(0, 0) - 'G': (window) -> - window.scrollTo(0, window.document.body.scrollHeight) - - 'j': (window) -> - window.scrollBy(0, SCROLL_AMOUNT) - - 'k': (window) -> - window.scrollBy(0, -SCROLL_AMOUNT) - - 'd': (window) -> - window.scrollBy(0, window.innerHeight) - - 'u': (window) -> - window.scrollBy(0, -window.innerHeight) + # Navigate to the address that is currently stored in the system clipboard + 'p': (vim) -> + vim.window.location.assign utils.readFromClipboard() + + # Open new tab and navigate to the address that is currently stored in the system clipboard + 'P': (vim) -> + if chromeWindow = utils.getRootWindow vim.window + if gBrowser = chromeWindow.gBrowser + gBrowser.selectedTab = gBrowser.addTab utils.readFromClipboard() + # + # Open new tab and focus the address bar + 't': (vim) -> + if chromeWindow = utils.getRootWindow vim.window + if gBrowser = chromeWindow.gBrowser + gBrowser.selectedTab = chromeWindow.gBrowser.addTab() + + # Copy current URL to the clipboard + 'y,f': (vim) -> + vim.markers = injectHints vim.window.document, + # This callback will be called with the selected marker as argument + vim.cb = (marker) -> + if url = marker.element.href + utils.writeToClipboard url + + vim.enterHintsMode() + # + # Copy current URL to the clipboard + 'y,y': (vim) -> + utils.writeToClipboard vim.window.location.toString() + + # Reload the page, possibly from cache + 'r': (vim) -> + vim.window.location.reload(false) + # + # Reload the page from the server + 'R': (vim) -> + vim.window.location.reload(false) + + # Scroll to the top of the page + 'g,g': (vim) -> + vim.window.scrollTo(0, 0) + + # Scroll to the bottom of the page + 'G': (vim) -> + vim.window.scrollTo(0, vim.window.document.body.scrollHeight) + + # Scroll down a bit + 'j': (vim) -> + vim.window.scrollBy(0, SCROLL_AMOUNT) + + # Scroll up a bit + 'k': (vim) -> + vim.window.scrollBy(0, -SCROLL_AMOUNT) + + # Scroll down a page + 'c-d': (vim) -> + vim.window.scrollBy(0, vim.window.innerHeight) + + # Scroll up a page + 'c-u': (vim) -> + vim.window.scrollBy(0, -vim.window.innerHeight) + + # Activate previous tab + 'J|g,T': (vim) -> + if rootWindow = utils.getRootWindow vim.window + rootWindow.gBrowser.tabContainer.advanceSelectedTab(-1, true); - 'J': (window) -> - if rootWindow = utils.getRootWindow window + # Activate next tab + 'K|g,t': (vim) -> + if rootWindow = utils.getRootWindow vim.window rootWindow.gBrowser.tabContainer.advanceSelectedTab(1, true); - 'K': (window) -> - if rootWindow = utils.getRootWindow window - rootWindow.gBrowser.tabContainer.advanceSelectedTab(-1, true); - - 'x': (window) -> - if rootWindow = utils.getRootWindow window + # Go to the first tab + 'g,^': (vim) -> + if rootWindow = utils.getRootWindow vim.window + rootWindow.gBrowser.tabContainer.selectedIndex = 0; + # + # Go to the last tab + 'g,$': (vim) -> + if rootWindow = utils.getRootWindow vim.window + itemCount = rootWindow.gBrowser.tabContainer.itemCount; + rootWindow.gBrowser.tabContainer.selectedIndex = itemCount - 1; + + # Go back in history + 'H': (vim) -> + vim.window.history.back() + + # Go forward in history + 'L': (vim) -> + vim.window.history.forward() + + # Close current tab + 'x': (vim) -> + if rootWindow = utils.getRootWindow vim.window rootWindow.gBrowser.removeCurrentTab() - 'X': (window) -> - if rootWindow = utils.getRootWindow window + # Restore last closed tab + 'X': (vim) -> + if rootWindow = utils.getRootWindow vim.window ss = utils.getSessionStore() if ss and ss.getClosedTabCount(rootWindow) > 0 ss.undoCloseTab rootWindow, 0 - 'f': (window) -> - try - addHints window.top.document, (el) -> - console.log 'f hint', el - catch err - console.log err - - 'Esc': (window) -> - try - window.document.activeElement?.blur() - removeHints window.top.document - catch err - console.log err - - -getCommand = (keys) -> - sequence = [key.toString() for key in keys].join(',') - if command = commands[sequence] - return command - else if keys.length > 0 - return getCommand keys.slice(1) - else - undefined - -maybeCommand = (keys) -> - if keys.length == 0 - return false - else - sequence = [key.toString() for key in keys].join(',') - for s in Object.keys(commands) - if s.search(sequence) == 0 - return true - - return maybeCommand keys.slice(1) - -exports.getCommand = getCommand -exports.maybeCommand = maybeCommand + # Follow links with hint markers + 'f': (vim) -> + vim.markers = injectHints vim.window.document, + # This callback will be called with the selected marker as argument + vim.cb = (marker) -> + marker.element.focus() + utils.simulateClick marker.element + + vim.enterHintsMode() + + # Follow links in a new Tab with hint markers + 'F': (vim) -> + vim.markers = injectHints vim.window.document, + # This callback will be called with the selected marker as argument + vim.cb = (marker) -> + marker.element.focus() + utils.simulateClick marker.element, metaKey: true + + vim.enterHintsMode() + + 'Esc': (vim) -> + # Blur active element if it's editable. Other elements + # aren't blurred - we don't want to interfere with + # the browser too much + activeElement = vim.window.document.activeElement + if utils.isElementEditable activeElement + activeElement.blur() + + # Remove hints and enter normal mode + removeHints vim.window.document + vim.enterNormalMode() + +hintCharHandler = (vim, char) -> + maxCount = 0 + for hint, marker of vim.markers + count = marker.matchHintChar char + maxCount = Math.max count, maxCount + + for hint, marker of vim.markers + if marker.matchedHintCharCount == marker.hintChars.length == maxCount + console.log marker.hintChars + vim.cb marker + removeHints vim.window.document + vim.enterNormalMode() + break + + if marker.matchedHintCharCount < maxCount + marker.hide() + else + marker.show() + +exports.hintCharHandler = hintCharHandler +exports.commands = do -> + newCommands = {} + for keys, command of commands + for key in keys.split '|' + newCommands[key] = command + return newCommands diff --git a/packages/event-handlers.coffee b/packages/event-handlers.coffee index 86795cc..f470abb 100644 --- a/packages/event-handlers.coffee +++ b/packages/event-handlers.coffee @@ -4,60 +4,29 @@ utils = require 'utils' { interfaces: Ci } = Components -KeyboardEvent = Ci.nsIDOMKeyEvent - vimBucket = new utils.Bucket utils.getWindowId, (obj) -> new Vim obj -class KeyInfo - constructor: (event) -> - if event.charCode > 0 - @key = String.fromCharCode(event.charCode) - else - switch event.keyCode - when KeyboardEvent.DOM_VK_ESCAPE then @key = 'Esc' - when KeyboardEvent.DOM_VK_TAB then @key = 'Tab' - - @shift = event.shiftKey - @alt = event.altKey - @ctrl = event.ctrlKey - @meta = event.metaKey - - isValid: -> @key - - toString: -> - k = (a, b) -> if a then b else '' - if @at or @ctrl or @meta - "<#{ k(@ctrl, 'c') }#{ k(@alt, 'a') }#{ k(@meta, 'm') }-#{ @key }>" - else - @key - suppressEvent = (event) -> event.preventDefault() event.stopPropagation() +# The following handlers are installed on every top level DOM window handlers = 'keypress': (event) -> try isEditable = utils.isElementEditable event.originalTarget - if event.keyCode == KeyboardEvent.DOM_VK_ESCAPE or not isEditable + key = utils.keyboardEventKey event + + # We only handle the key if there is no focused editable element + # or if it's the *Esc* key, which will remote the focus from + # the currently focused element + if key and (key == 'Esc' or not isEditable) if window = utils.getEventTabWindow event - keyInfo = new KeyInfo event - if keyInfo.isValid() - console.log event.keyCode, event.which, event.charCode, keyInfo.toString() - if vimBucket.get(window)?.keypress keyInfo - suppressEvent event + if vimBucket.get(window)?.keypress key + suppressEvent event catch err console.log err - - 'focus': (event) -> - if window = utils.getEventTabWindow event - vimBucket.get(window)?.focus event.originalTarget - - 'blur': (event) -> - if window = utils.getEventTabWindow event - vimBucket.get(window)?.blur event.originalTarget - 'TabClose': (event) -> if gBrowser = utils.getEventTabBrowser event if browser = gBrowser.getBrowserForTab event.originalTarget diff --git a/packages/hints.coffee b/packages/hints.coffee index 7af5c12..9f47232 100644 --- a/packages/hints.coffee +++ b/packages/hints.coffee @@ -1,29 +1,8 @@ -HINTCHARS = 'asdfghjkl;' CONTAINER_ID = 'vimffHintMarkerContainer' -{ interfaces: Ci } = Components -HTMLDocument = Ci.nsIDOMHTMLDocument -{ Marker, getElementRect } = require 'marker' - -indexToHint = (i, chars) -> - return '' if i < 0 - - n = chars.length - l = Math.floor(i / n); k = i % n; - - return indexToHint(l - 1, chars) + chars[k] - -hintToIndex = (hint, chars) -> - return -1 if hint.length < 1 - - n = chars.length; m = hint.length - - i = chars.indexOf(hint[m - 1]) - if hint.length > 1 - base = hintToIndex(hint.slice(0, m - 1), chars) - i += (base + 1) * n - - return i +{ interfaces: Ci } = Components +HTMLDocument = Ci.nsIDOMHTMLDocument +{ Marker } = require 'marker' getHintsContainer = (document) -> document.getElementById CONTAINER_ID @@ -31,44 +10,29 @@ getHintsContainer = (document) -> createHintsContainer = (document) -> container = document.createElement 'div' container.id = CONTAINER_ID + container.className = 'vimffReset' return container - -hasHints = (document) -> - document.getUserData 'vimff.has_hints' -addHints = (document, cb) -> - if hasHints document - removeHints document +injectHints = (document) -> + removeHints document if document instanceof HTMLDocument - container = createHintsContainer document - - start = new Date().getTime() - - markers = {}; i = 0; - for link in document.links - if rect = getElementRect link - hint = indexToHint(i++, HINTCHARS) - marker = new Marker(link, container) - marker.setPosition rect - marker.setHint hint - markers[hint] = marker + markers = Marker.createMarkers document - console.log new Date().getTime() - start, 'aaaaa' - - document.setUserData 'vimff.has_hints', true, null - document.setUserData 'vimff.markers', markers, null + container = createHintsContainer document + for hint, marker of markers + container.appendChild marker.markerElement document.body.appendChild container -removeHints = (document) -> - console.log hasHints document - if hasHints document - document.setUserData 'vimff.has_hints', undefined, null - document.setUserData 'vimff.markers', undefined, null + return markers + +removeHints = (document, markers) -> + if container = getHintsContainer document + document.body.removeChild container - document.body.removeChild container if container = getHintsContainer document +handleHintChar = (markers, char) -> -exports.addHints = addHints -exports.removeHints = removeHints -exports.hasHints = removeHints +exports.injectHints = injectHints +exports.removeHints = removeHints +exports.handleHintChar = handleHintChar diff --git a/packages/marker.coffee b/packages/marker.coffee index e82c134..782b490 100644 --- a/packages/marker.coffee +++ b/packages/marker.coffee @@ -1,34 +1,164 @@ +{ interfaces: Ci } = Components +XPathResult = Ci.nsIDOMXPathResult + +HINTCHARS = 'asdfgercvhjkl;uinm' + +# All elements that have one or more of the following properties +# qualify for their own marker in hints mode +MARKABLE_ELEMENT_PROPERTIES = [ + "@tabindex" + "@onclick" + "@onmousedown" + "@onmouseup" + "@oncommand" + "@role='link'" + "@role='button'" + "contains(@class, 'button')" + "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true'" +] + +# All the following elements qualify for their own marker in hints mode +MARKABLE_ELEMENTS = [ + "a" + "area[@href]" + "textarea" + "button" + "select" + "input[not(@type='hidden' or @disabled or @readonly)]" +] + + +# Marker class wraps the markable element and provides +# methods to manipulate the markers class Marker - constructor: (@element, @container) -> + # Creates the marker DOM node + constructor: (@element) -> document = @element.ownerDocument window = document.defaultView @markerElement = document.createElement 'div' @markerElement.className = 'vimffReset vimffHintMarker' - @container.appendChild @markerElement + # Hides the marker + hide: -> @markerElement.style.display = 'none' - show: -> @markerElement.style.display = 'none' - hide: -> delete @markerElement.style.display + # Shows the marker + show: -> @markerElement.style.display = 'block' + # Positions the marker on the page. The positioning is absulute setPosition: (rect) -> @markerElement.style.left = rect.left + 'px' @markerElement.style.top = rect.top + 'px' - setHint: (@hint) -> + # Assigns hint string to the marker + setHint: (@hintChars) -> + # number of hint chars that have been matched so far + @matchedHintCharCount = 0 + document = @element.ownerDocument while @markerElement.hasChildNodes() @markerElement.removeChild @markedElement.firstChild - for char in @hint + for char in @hintChars span = document.createElement 'span' span.className = 'vimffReset' span.textContent = char.toUpperCase() @markerElement.appendChild span + matchHintChar: (char) -> + if char == 'backspace' + if @matchedHintCharCount > 0 + @matchedHintCharCount -= 1 + @markerElement.children[@matchedHintCharCount].className = 'vimffReset' + else + if @hintChars[@matchedHintCharCount] == char + @markerElement.children[@matchedHintCharCount].className = 'vimffReset vimffCharMatch' + @matchedHintCharCount += 1 + + return @matchedHintCharCount + + isComplete: -> + return @hintChars.length == @hintCompletion + + +# Selects all markable elements on the page, creates markers +# for each of them The markers are then positioned on the page +# +# The array of markers is returned +Marker.createMarkers = (document) -> + elementsSet = getMarkableElements(document) + markers = {}; + j = 0 + for i in [0...elementsSet.snapshotLength] by 1 + element = elementsSet.snapshotItem(i) + if rect = getElementRect element + hint = indexToHint(j++) + marker = new Marker(element) + marker.setPosition rect + marker.setHint hint + markers[hint] = marker + + return markers + +# Function generator that creates a function that +# returns hint string for supplied numeric index. +indexToHint = do -> + # split the characters into two groups: + # + # * left chars are used for the head + # * right chars are used to build the tail + left = HINTCHARS[...HINTCHARS.length / 3] + right = HINTCHARS[HINTCHARS.length / 3...] + + # Helper function that returns a permutation number `i` + # of some of the characters in the `chars` agrument + f = (i, chars) -> + return '' if i < 0 + + n = chars.length + l = Math.floor(i / n); k = i % n; + + return f(l - 1, chars) + chars[k] + + return (i) -> + n = Math.floor(i / left.length) + m = i % left.length + return f(n - 1, right) + left[m] + -# The login in this method is copied from Vimium for Chrome +# Returns elements that qualify for hint markers in hints mode. +# Generates and memoizes an XPath query internally +getMarkableElements = do -> + # Some preparations done on startup + elements = Array.concat \ + MARKABLE_ELEMENTS, + ["*[#{ MARKABLE_ELEMENT_PROPERTIES.join(" or ") }]"] + + xpath = elements.reduce((m, rule) -> + m.concat(["//#{ rule }", "//xhtml:#{ rule }"]) + , []).join(' | ') + + namespaceResolver = (namespace) -> + if (namespace == "xhtml") then "http://www.w3.org/1999/xhtml" else null + + # The actual function that will return the desired elements + return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) -> + document.evaluate xpath, document.documentElement, namespaceResolver, resultType, null + +# Checks if the given TextRectangle object qualifies +# for its own Marker with respect to the `window` object +isRectOk = (rect, window) -> + rect.width > 2 and rect.height > 2 and \ + rect.top > -2 and rect.left > -2 and \ + rect.top < window.innerHeight - 2 and \ + rect.left < window.innerWidth - 2 + +# Will scan through `element.getClientRects()` and look for +# the first visible rectange. If there are no visible rectangles, then +# will look at the children of the markable node. +# +# The logic has been copied over from Vimiun getElementRect = (element) -> document = element.ownerDocument window = document.defaultView @@ -44,14 +174,10 @@ getElementRect = (element) -> rects.push element.getBoundingClientRect() for rect in rects - if rect.width > 2 and rect.height > 2 and \ - rect.top > -2 and rect.left > -2 and \ - rect.top < window.innerHeight - 2 and \ - rect.left < window.innerWidth - 2 - + if isRectOk rect, window return { - top: rect.top + scrollTop - clientTop; - left: rect.left + scrollLeft - clientLeft; + top: rect.top + scrollTop - clientTop + left: rect.left + scrollLeft - clientLeft width: rect.width height: rect.height } @@ -65,9 +191,8 @@ getElementRect = (element) -> if computedStyle.getPropertyValue 'float' != 'none' or \ computedStyle.getPropertyValue 'position' == 'absolute' - return childRect if childRect = getElementRect childElement + childRect if childRect = getElementRect childElement return undefined -exports.Marker = Marker -exports.getElementRect = getElementRect +exports.Marker = Marker diff --git a/packages/utils.coffee b/packages/utils.coffee index 746e152..a17081b 100644 --- a/packages/utils.coffee +++ b/packages/utils.coffee @@ -2,8 +2,19 @@ { interfaces: Ci, classes: Cc } = Components -sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService) -ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService) +HTMLInputElement = Ci.nsIDOMHTMLInputElement +HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement +HTMLSelectElement = Ci.nsIDOMHTMLSelectElement +XULDocument = Ci.nsIDOMXULDocument +XULElement = Ci.nsIDOMXULElement +HTMLDocument = Ci.nsIDOMHTMLDocument +HTMLElement = Ci.nsIDOMHTMLElement +Window = Ci.nsIDOMWindow +ChromeWindow = Ci.nsIDOMChromeWindow +KeyboardEvent = Ci.nsIDOMKeyEvent + +_sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService) +_clip = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard) class Bucket constructor: (@idFunc, @newFunc) -> @@ -11,11 +22,13 @@ class Bucket get: (obj) -> id = @idFunc obj - @bucket[id] or @bucket[id] = @newFunc obj + if container = @bucket[id] + return container + else + return @bucket[id] = @newFunc obj forget: (obj) -> - id = @idFunc obj - delete @bucket[id] if id + delete @bucket[id] if id = @idFunc obj class WindowEventTracker @@ -46,16 +59,6 @@ class WindowEventTracker start: -> @windowTracker.start() stop: -> @windowTracker.stop() -HTMLInputElement = Ci.nsIDOMHTMLInputElement -HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement -HTMLSelectElement = Ci.nsIDOMHTMLSelectElement -XULDocument = Ci.nsIDOMXULDocument -XULElement = Ci.nsIDOMXULElement -HTMLDocument = Ci.nsIDOMHTMLDocument -HTMLElement = Ci.nsIDOMHTMLElement -Window = Ci.nsIDOMWindow -ChromeWindow = Ci.nsIDOMChromeWindow - isRootWindow = (window) -> window.location == "chrome://browser/content/browser.xul" @@ -81,7 +84,6 @@ getEventRootWindow = (event) -> getEventTabBrowser = (event) -> cw.gBrowser if cw = getEventRootWindow event - getRootWindow = (window) -> return window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) @@ -104,6 +106,7 @@ getWindowId = (window) -> getSessionStore = -> Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); +# Function that returns a URI to the css file that's part of the extension cssUri = do () -> tools = {} Cu.import "resource://gre/modules/Services.jsm", tools @@ -112,13 +115,99 @@ cssUri = do () -> uri = tools.Services.io.newURI "resources/#{ name }.css", null, baseURI return uri +# Loads the css identified by the name in the StyleSheetService as User Stylesheet +# The stylesheet is then appended to every document, but it can be overwritten by +# any user css loadCss = (name) -> - sss.loadAndRegisterSheet(cssUri(name), sss.USER_SHEET) + _sss.loadAndRegisterSheet(cssUri(name), _sss.USER_SHEET) +# Unloads the css file that's been loaded earlier with `loadCss` unloadCss = (name) -> uri = cssUri(name) - if sss.sheetRegistered(uri, sss.USER_SHEET) - sss.unregisterSheet(uri, sss.USER_SHEET) + if _sss.sheetRegistered(uri, _sss.USER_SHEET) + _sss.unregisterSheet(uri, _sss.USER_SHEET) + +# processes the keyboard event and extracts string representation +# of the key *without modifiers* in case this is the kind of a key +# that can be handled by the extension +# +# Currently we handle letters, Escape and Tab keys +keyboardEventChar = (keyboardEvent) -> + if keyboardEvent.charCode > 0 + char = String.fromCharCode(keyboardEvent.charCode) + if char.match /\s/ + char = undefined + else + switch keyboardEvent.keyCode + when KeyboardEvent.DOM_VK_ESCAPE then char = 'Esc' + when KeyboardEvent.DOM_VK_BACK_SPACE then char = 'Backspace' + else char = undefined + + return char + +# extracts string representation of the KeyboardEvent and adds +# relevant modifiers (_ctrl_, _alt_, _meta_) in case they were pressed +keyboardEventKey = (keyboardEvent) -> + char = keyboardEventChar keyboardEvent + + { + shiftKey: shift, + altKey: alt, + ctrlKey: ctrl, + metaKey: meta, + } = keyboardEvent + + if alt or ctrl or meta + k = (a, b) -> if a then b else '' + return "<#{ k(ctrl, 'c') + k(alt, 'a') + k(meta, 'm') }-#{ char }>" + else + return char + +# Simulate mouse click with full chain of event +# Copied from Vimium codebase +simulateClick = (element, modifiers) -> + document = element.ownerDocument + window = document.defaultView + modifiers ||= {} + + eventSequence = ["mouseover", "mousedown", "mouseup", "click"] + for event in eventSequence + mouseEvent = document.createEvent("MouseEvents") + mouseEvent.initMouseEvent(event, true, true, window, 1, 0, 0, 0, 0, modifiers.ctrlKey, false, false, + modifiers.metaKey, 0, null) + # Debugging note: Firefox will not execute the element's default action if we dispatch this click event, + # but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately + element.dispatchEvent(mouseEvent) + +# Write a string into system clipboard +writeToClipboard = (text) -> + str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + str.data = text + + trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + trans.addDataFlavor("text/unicode"); + trans.setTransferData("text/unicode", str, text.length * 2); + + _clip.setData trans, null, Ci.nsIClipboard.kGlobalClipboard + # +# Write a string into system clipboard +readFromClipboard = () -> + trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + trans.addDataFlavor("text/unicode"); + + _clip.getData trans, Ci.nsIClipboard.kGlobalClipboard + + str = {} + strLength = {} + + trans.getTransferData("text/unicode", str, strLength) + + if str + str = str.value.QueryInterface(Ci.nsISupportsString); + return str.data.substring 0, strLength.value / 2 + + return undefined + exports.WindowEventTracker = WindowEventTracker exports.Bucket = Bucket @@ -135,3 +224,8 @@ exports.getSessionStore = getSessionStore exports.loadCss = loadCss exports.unloadCss = unloadCss + +exports.keyboardEventKey = keyboardEventKey +exports.simulateClick = simulateClick +exports.readFromClipboard = readFromClipboard +exports.writeToClipboard = writeToClipboard diff --git a/packages/vim.coffee b/packages/vim.coffee index c9f90bc..ae0f74c 100644 --- a/packages/vim.coffee +++ b/packages/vim.coffee @@ -1,31 +1,66 @@ -{ getCommand, maybeCommand } = require 'commands' -{ getWindowId, Bucket } = require 'utils' +{ getWindowId, Bucket } = require 'utils' -MODE_NORMAL = 1 +{ commands, + hintCharHandler +} = require 'commands' +MODE_NORMAL = 1 +MODE_HINTS = 2 class Vim constructor: (@window) -> - @mode = MODE_NORMAL - @keys = [] + @mode = MODE_NORMAL + @keys = [] + @markers = undefined + @cb = undefined - keypress: (keyInfo) -> - @keys.push keyInfo - if command = getCommand @keys - command @window + keypress: (key) -> + @keys.push key + if command = _getCommand(@mode, @keys) + command @ @keys = [] - true - else if maybeCommand @keys - true + return key != 'Esc' + else if _maybeCommand(@mode, @keys) + return true else - false + @keys.pop() + return false + + enterHintsMode: () -> + @mode = MODE_HINTS + + enterNormalMode: () -> + @markers = @cb = undefined + + @mode = MODE_NORMAL + +_endsWithEsc = (keys) -> + return keys.join(',').match(/Esc$/) + +_getCommand = (mode, keys) -> + if mode == MODE_NORMAL or _endsWithEsc(keys) + sequence = keys.join(',') + if command = commands[sequence] + return command + else if keys.length > 0 + return _getCommand mode, keys.slice(1) + + else if mode == MODE_HINTS + return (vim) => + char = keys[keys.length - 1].toLowerCase() + return hintCharHandler(vim, char) + + return undefined + +_maybeCommand = (mode, keys) -> + if mode == MODE_NORMAL and keys.length > 0 + sequence = keys.join(',') + for commandSequence in Object.keys(commands) + if commandSequence.search(sequence) == 0 + return true - focus: (element) -> - @activeElement = element - console.log 'focus', @activeElement + return _maybeCommand mode, keys.slice(1) - blur: (element) -> - console.log 'blur', @activeElement - delete @activeElement if @activeElement == element + return false exports.Vim = Vim diff --git a/resources/vimff.css b/resources/vimff.css index a0528a7..f9215bf 100644 --- a/resources/vimff.css +++ b/resources/vimff.css @@ -1,14 +1,6 @@ /* Copied over from vimium project */ -.vimffReset, div.vimffReset, span.vimffReset, -table.vimffReset, -a.vimffReset, -a:visited.vimffReset, -a:link.vimffReset, -a:hover.vimffReset, -td.vimffReset, -tr.vimffReset { background: none; border: none; @@ -51,31 +43,25 @@ tr.vimffReset div.vimffHintMarker { position: absolute; display: block; - top: -1px; - left: -1px; white-space: nowrap; overflow: hidden; font-size: 11px; padding: 1px 3px 0px 3px; - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785),color-stop(100%,#FFC542)); - border: solid 1px #C38A22; - border-radius: 3px; box-shadow: 0px 3px 7px 0px rgba(0, 0, 0, 0.3); - background-color: yellow; -} - -div > .vimiumHintMarker > .matchingCharacter { + background-color: #AEDC3A; + border: solid 1px #2B95B0; + border-radius: 5px; } div.vimffHintMarker span { - color: #302505; - font-family: Helvetica, Arial, sans-serif; - font-weight: bold; - font-size: 11px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); + color: #303030; + font-family: Helvetica, Arial, sans-serif; + font-weight: bold; + font-size: 11px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); } -div.vimffHintMarker > span.vimffCharMatch { - color: #D4AC3A; +div.vimffHintMarker span.vimffCharMatch { + color: #FFFFFF; } -- 2.39.3