opal/opal-browser

View on GitHub
opal/browser/storage.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# backtick_javascript: true
require 'json'
require 'stringio'

module Browser

# A {Storage} allows you to store data across page loads and browser
# restarts.
#
# Compatibility
# -------------
# The compatibility layer will try various implementations in the following
# order.
#
# + [window.localStorage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage)
# + [window.globalStorage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#globalStorage)
# + [document.body.addBehavior](http://msdn.microsoft.com/en-us/library/ms531424(VS.85).aspx)
# + [document.cookie](https://developer.mozilla.org/en-US/docs/Web/API/document.cookie)
#
# @see https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage
# @todo remove method_defined? checks when require order is fixed
class Storage
  def self.json_create(data)
    data.delete(JSON.create_id)

    Hash[data.map {|key, value|
      [JSON.parse(key), value]
    }]
  end

  # @!attribute [r] name
  # @return [String] the name of the storage
  attr_reader :name

  # Create a new storage on the given window with the given name.
  #
  # @param window [native] the window to save the storage to
  # @param name [String] the name to use to discern different storages
  def initialize(window, name)
    super()

    @window = window
    @name   = name
    @data   = {}

    autosave!
    reload
  end

  # Check if autosaving is enabled.
  #
  # When autosaving is enabled the {Storage} is saved every time a change is
  # made, otherwise you'll have to save it manually yourself.
  def autosave?
    @autosave
  end

  # Enable autosaving.
  def autosave!
    @autosave = true
  end

  # Disable autosaving.
  def no_autosave!
    @autosave = false
  end

  include Enumerable

  # Iterate over the (key, value) pairs in the storage.
  #
  # @yield [key, value]
  def each(&block)
    return enum_for :each unless block

    @data.each(&block)

    self
  end

  def method_missing(*args, &block)
    @data.__send__(*args, &block)
  end

  # Set a value in the storage.
  def []=(key, value)
    @data[key] = value

    save if autosave?
  end

  # Delete a value from the storage.
  def delete(key)
    @data.delete(key).tap {
      save if autosave?
    }
  end

  # Clear the storage.
  def clear
    @data.clear.tap {
      save if autosave?
    }
  end

  # Replace the current storage with the given one.
  #
  # @param new [Hash, String] if new is a {String} it will be parsed as JSON
  def replace(new)
    if String === new
      @data.replace(JSON.parse(new))
    else
      @data.replace(new)
    end
  end

  # Call the block between a [#reload] and [#save].
  def commit(&block)
    autosave  = @autosave
    @autosave = false
    result    = nil

    reload

    begin
      result = block.call
      save
    rescue
      reload
      raise
    ensure
      @autosave = autosave
    end

    result
  end

  def to_h
    @data
  end

  # @!method reload
  #   Load the storage.

  # @!method save
  #   Persist the current state to the storage.

  if Browser.supports? 'Storage.local'
    def reload
      replace `#@window.localStorage[#@name] || '{}'`
    end

    def save
      `#@window.localStorage[#@name] = #{JSON.dump(self)}`
    end
  elsif Browser.supports? 'Storage.global'
    def reload
      replace `#@window.globalStorage[#@window.location.hostname][#@name] || '{}'`
    end

    def save
      `#@window.globalStorage[#@window.location.hostname][#@name] = #{JSON.dump(self)}`
    end
  elsif Browser.supports? 'Element.addBehavior'
    def reload
      %x{
        #@element = #@window.document.createElement('link');
        #@element.addBehavior('#default#userData');

        #@window.document.getElementsByTagName('head')[0].appendChild(#@element);

        #@element.load(#@name);
      }

      replace `#@element.getAttribute(#@name) || '{}'`
    end

    def save
      %x{
        #@element.setAttribute(#@name, #{JSON.dump(self)});
        #@element.save(#@name);
      }
    end
  else
    def reload
      $document.cookies.options expires: 60 * 60 * 24 * 365

      replace $document.cookies[@name]
    end

    def save
      $document.cookies[@name] = JSON.dump(self)
    end
  end

  # Convert the storage to JSON.
  #
  # @return [String] the JSON representation
  def to_json
    io = StringIO.new << "{"

    io << JSON.create_id.to_json << ":" << self.class.name.to_json << ","

    @data.each {|key, value|
      io << key.to_json.to_s << ":" << value.to_json << ","
    }

    io.seek(-1, IO::SEEK_CUR)
    io << "}"

    io.string
  end
end

# A {SessionStorage} allows you to store data across page reloads, as long as the session
# is active.
#
# @see https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#sessionStorage
class SessionStorage < Storage
  def self.supported?
    Browser.supports? 'Storage.session'
  end

  def reload
    replace `#@window.sessionStorage[#@name] || '{}'`
  end

  def save
    `#@window.sessionStorage[#@name] = #{JSON.dump(self)}`
  end
end

class Window
  # Get a storage with the given name.
  #
  # @param name [Symbol] the name of the storage
  #
  # @return [Storage]
  def storage(name = :default)
    Storage.new(to_n, name)
  end

  # Get a session storage with the given name.
  #
  # @param name [Symbol] the name of the storage
  #
  # @return [SessionStorage]
  def session_storage(name = :default)
    SessionStorage.new(to_n, name)
  end
end

end