diff options
author | Jim Shaver <dcypherd@gmail.com> | 2015-05-31 01:21:44 -0400 |
---|---|---|
committer | Jim Shaver <dcypherd@gmail.com> | 2015-05-31 01:21:44 -0400 |
commit | b51363b3ca43f6572acb673186e6ae78a1f48434 (patch) | |
tree | a7488b32871c142141a813dc6ff2ede172672c31 /libmproxy | |
parent | 4fe2c069cca07aadf983f54e18dac4de492d5d69 (diff) | |
parent | 06fba18106a8f759ec6f08453e86772a170c653b (diff) | |
download | mitmproxy-b51363b3ca43f6572acb673186e6ae78a1f48434.tar.gz mitmproxy-b51363b3ca43f6572acb673186e6ae78a1f48434.tar.bz2 mitmproxy-b51363b3ca43f6572acb673186e6ae78a1f48434.zip |
Merge remote-tracking branch 'upstream/master' into print-bracket-fix
Conflicts:
examples/har_extractor.py
examples/nonblocking.py
examples/read_dumpfile
libmproxy/web/app.py
Diffstat (limited to 'libmproxy')
50 files changed, 12547 insertions, 8193 deletions
diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index ece476f2..eb24bed7 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -65,7 +65,7 @@ def parse_replace_hook(s): patt, regex, replacement = _parse_hook(s) try: re.compile(regex) - except re.error, e: + except re.error as e: raise ParseException("Malformed replacement regex: %s" % str(e.message)) return patt, regex, replacement @@ -127,7 +127,6 @@ def parse_server_spec_special(url): return ret - def get_common_options(options): stickycookie, stickyauth = None, None if options.stickycookie_filt: @@ -142,17 +141,17 @@ def get_common_options(options): for i in options.replace: try: p = parse_replace_hook(i) - except ParseException, e: + except ParseException as e: raise configargparse.ArgumentTypeError(e.message) reps.append(p) for i in options.replace_file: try: patt, rex, path = parse_replace_hook(i) - except ParseException, e: + except ParseException as e: raise configargparse.ArgumentTypeError(e.message) try: v = open(path, "rb").read() - except IOError, e: + except IOError as e: raise configargparse.ArgumentTypeError( "Could not read replace file: %s" % path ) @@ -162,7 +161,7 @@ def get_common_options(options): for i in options.setheader: try: p = parse_setheader(i) - except ParseException, e: + except ParseException as e: raise configargparse.ArgumentTypeError(e.message) setheaders.append(p) @@ -221,7 +220,7 @@ def common_options(parser): parser.add_argument( "--cadir", action="store", type=str, dest="cadir", default=config.CA_DIR, - help="Location of the default mitmproxy CA files. (%s)"%config.CA_DIR + help="Location of the default mitmproxy CA files. (%s)" % config.CA_DIR ) parser.add_argument( "--host", @@ -466,7 +465,7 @@ def common_options(parser): "--replay-ignore-payload-param", action="append", dest="replay_ignore_payload_params", type=str, help=""" - Request's payload parameters (application/x-www-form-urlencoded) to + Request's payload parameters (application/x-www-form-urlencoded or multipart/form-data) to be ignored while searching for a saved flow to replay. Can be passed multiple times. """ @@ -482,9 +481,10 @@ def common_options(parser): ) group.add_argument( "--replay-ignore-host", - action="store_true", dest="replay_ignore_host", default=False, - help="Ignore request's destination host while searching for a saved flow to replay" - ) + action="store_true", + dest="replay_ignore_host", + default=False, + help="Ignore request's destination host while searching for a saved flow to replay") group = parser.add_argument_group( "Replacements", @@ -575,12 +575,17 @@ def mitmproxy(): ) common_options(parser) parser.add_argument( - "--palette", type=str, default="dark", + "--palette", type=str, default=palettes.DEFAULT, action="store", dest="palette", choices=sorted(palettes.palettes.keys()), help="Select color palette: " + ", ".join(palettes.palettes.keys()) ) parser.add_argument( + "--palette-transparent", + action="store_true", dest="palette_transparent", default=False, + help="Set transparent background for palette." + ) + parser.add_argument( "-e", "--eventlog", action="store_true", dest="eventlog", help="Show event log." @@ -594,6 +599,11 @@ def mitmproxy(): type=str, dest="intercept", default=None, help="Intercept filter expression." ) + group.add_argument( + "-l", "--limit", action="store", + type=str, dest="limit", default=None, + help="Limit filter expression." + ) return parser diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 198b7bbe..8f39e283 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -1,281 +1,38 @@ from __future__ import absolute_import -import glob import mailcap import mimetypes import tempfile import os import os.path import shlex +import signal import stat import subprocess import sys -import time import traceback import urwid import weakref -from .. import controller, utils, flow, script -from . import flowlist, flowview, help, common -from . import grideditor, palettes, contentview, flowdetailview +from .. import controller, flow, script +from . import flowlist, flowview, help, window, signals, options +from . import grideditor, palettes, contentview, statusbar, palettepicker EVENTLOG_SIZE = 500 -class _PathCompleter: - def __init__(self, _testing=False): - """ - _testing: disables reloading of the lookup table to make testing possible. - """ - self.lookup, self.offset = None, None - self.final = None - self._testing = _testing - - def reset(self): - self.lookup = None - self.offset = -1 - - def complete(self, txt): - """ - Returns the next completion for txt, or None if there is no completion. - """ - path = os.path.expanduser(txt) - if not self.lookup: - if not self._testing: - # Lookup is a set of (display value, actual value) tuples. - self.lookup = [] - if os.path.isdir(path): - files = glob.glob(os.path.join(path, "*")) - prefix = txt - else: - files = glob.glob(path+"*") - prefix = os.path.dirname(txt) - prefix = prefix or "./" - for f in files: - display = os.path.join(prefix, os.path.basename(f)) - if os.path.isdir(f): - display += "/" - self.lookup.append((display, f)) - if not self.lookup: - self.final = path - return path - self.lookup.sort() - self.offset = -1 - self.lookup.append((txt, txt)) - self.offset += 1 - if self.offset >= len(self.lookup): - self.offset = 0 - ret = self.lookup[self.offset] - self.final = ret[1] - return ret[0] - - -class PathEdit(urwid.Edit, _PathCompleter): - def __init__(self, *args, **kwargs): - urwid.Edit.__init__(self, *args, **kwargs) - _PathCompleter.__init__(self) - - def keypress(self, size, key): - if key == "tab": - comp = self.complete(self.get_edit_text()) - self.set_edit_text(comp) - self.set_edit_pos(len(comp)) - else: - self.reset() - return urwid.Edit.keypress(self, size, key) - - -class ActionBar(urwid.WidgetWrap): - def __init__(self): - self.message("") - - def selectable(self): - return True - - def path_prompt(self, prompt, text): - self.expire = None - self._w = PathEdit(prompt, text) - - def prompt(self, prompt, text = ""): - self.expire = None - # A (partial) workaround for this Urwid issue: - # https://github.com/Nic0/tyrs/issues/115 - # We can remove it once veryone is beyond 1.0.1 - if isinstance(prompt, basestring): - prompt = unicode(prompt) - self._w = urwid.Edit(prompt, text or "") - - def message(self, message, expire=None): - self.expire = expire - self._w = urwid.Text(message) - - -class StatusBar(urwid.WidgetWrap): - def __init__(self, master, helptext): - self.master, self.helptext = master, helptext - self.ab = ActionBar() - self.ib = urwid.WidgetWrap(urwid.Text("")) - self._w = urwid.Pile([self.ib, self.ab]) - - def get_status(self): - r = [] - - if self.master.setheaders.count(): - r.append("[") - r.append(("heading_key", "H")) - r.append("eaders]") - if self.master.replacehooks.count(): - r.append("[") - r.append(("heading_key", "R")) - r.append("eplacing]") - if self.master.client_playback: - r.append("[") - r.append(("heading_key", "cplayback")) - r.append(":%s to go]"%self.master.client_playback.count()) - if self.master.server_playback: - r.append("[") - r.append(("heading_key", "splayback")) - if self.master.nopop: - r.append(":%s in file]"%self.master.server_playback.count()) - else: - r.append(":%s to go]"%self.master.server_playback.count()) - if self.master.get_ignore_filter(): - r.append("[") - r.append(("heading_key", "I")) - r.append("gnore:%d]" % len(self.master.get_ignore_filter())) - if self.master.get_tcp_filter(): - r.append("[") - r.append(("heading_key", "T")) - r.append("CP:%d]" % len(self.master.get_tcp_filter())) - if self.master.state.intercept_txt: - r.append("[") - r.append(("heading_key", "i")) - r.append(":%s]"%self.master.state.intercept_txt) - if self.master.state.limit_txt: - r.append("[") - r.append(("heading_key", "l")) - r.append(":%s]"%self.master.state.limit_txt) - if self.master.stickycookie_txt: - r.append("[") - r.append(("heading_key", "t")) - r.append(":%s]"%self.master.stickycookie_txt) - if self.master.stickyauth_txt: - r.append("[") - r.append(("heading_key", "u")) - r.append(":%s]"%self.master.stickyauth_txt) - if self.master.state.default_body_view.name != "Auto": - r.append("[") - r.append(("heading_key", "M")) - r.append(":%s]"%self.master.state.default_body_view.name) - - opts = [] - if self.master.anticache: - opts.append("anticache") - if self.master.anticomp: - opts.append("anticomp") - if self.master.showhost: - opts.append("showhost") - if not self.master.refresh_server_playback: - opts.append("norefresh") - if self.master.killextra: - opts.append("killextra") - if self.master.server.config.no_upstream_cert: - opts.append("no-upstream-cert") - if self.master.state.follow_focus: - opts.append("following") - if self.master.stream_large_bodies: - opts.append("stream:%s" % utils.pretty_size(self.master.stream_large_bodies.max_size)) - - if opts: - r.append("[%s]"%(":".join(opts))) - - if self.master.server.config.mode in ["reverse", "upstream"]: - dst = self.master.server.config.mode.dst - scheme = "https" if dst[0] else "http" - if dst[1] != dst[0]: - scheme += "2https" if dst[1] else "http" - r.append("[dest:%s]"%utils.unparse_url(scheme, *dst[2:])) - if self.master.scripts: - r.append("[") - r.append(("heading_key", "s")) - r.append("cripts:%s]"%len(self.master.scripts)) - # r.append("[lt:%0.3f]"%self.master.looptime) - - if self.master.stream: - r.append("[W:%s]"%self.master.stream_path) - - return r - - def redraw(self): - if self.ab.expire and time.time() > self.ab.expire: - self.message("") - - fc = self.master.state.flow_count() - if self.master.state.focus is None: - offset = 0 - else: - offset = min(self.master.state.focus + 1, fc) - t = [ - ('heading', ("[%s/%s]"%(offset, fc)).ljust(9)) - ] - - if self.master.server.bound: - host = self.master.server.address.host - if host == "0.0.0.0": - host = "*" - boundaddr = "[%s:%s]"%(host, self.master.server.address.port) - else: - boundaddr = "" - t.extend(self.get_status()) - status = urwid.AttrWrap(urwid.Columns([ - urwid.Text(t), - urwid.Text( - [ - self.helptext, - boundaddr - ], - align="right" - ), - ]), "heading") - self.ib._w = status - - def update(self, text): - self.helptext = text - self.redraw() - self.master.loop.draw_screen() - - def selectable(self): - return True - - def get_edit_text(self): - return self.ab._w.get_edit_text() - - def path_prompt(self, prompt, text): - return self.ab.path_prompt(prompt, text) - - def prompt(self, prompt, text = ""): - self.ab.prompt(prompt, text) - - def message(self, msg, expire=None): - if expire: - expire = time.time() + float(expire)/1000 - self.ab.message(msg, expire) - self.master.loop.draw_screen() - - class ConsoleState(flow.State): def __init__(self): flow.State.__init__(self) self.focus = None self.follow_focus = None self.default_body_view = contentview.get("Auto") - - self.view_mode = common.VIEW_LIST - self.view_flow_mode = common.VIEW_FLOW_REQUEST - - self.last_script = "" - self.last_saveload = "" self.flowsettings = weakref.WeakKeyDictionary() + self.last_search = None + + def __setattr__(self, name, value): + self.__dict__[name] = value + signals.update_settings.send(self) def add_flow_setting(self, flow, key, value): d = self.flowsettings.setdefault(flow, {}) @@ -316,6 +73,8 @@ class ConsoleState(flow.State): elif idx < 0: idx = 0 self.focus = idx + else: + self.focus = None def set_focus_flow(self, f): self.set_focus(self.view.index(f)) @@ -326,10 +85,10 @@ class ConsoleState(flow.State): return self.view[pos], pos def get_next(self, pos): - return self.get_from_pos(pos+1) + return self.get_from_pos(pos + 1) def get_prev(self, pos): - return self.get_from_pos(pos-1) + return self.get_from_pos(pos - 1) def delete_flow(self, f): if f in self.view and self.view.index(f) <= self.focus: @@ -357,6 +116,7 @@ class Options(object): "keepserving", "kill", "intercept", + "limit", "no_server", "refresh_server_playback", "rfile", @@ -373,6 +133,7 @@ class Options(object): "wfile", "nopop", "palette", + "palette_transparent" ] def __init__(self, **kwargs): @@ -388,7 +149,6 @@ class ConsoleMaster(flow.FlowMaster): def __init__(self, server, options): flow.FlowMaster.__init__(self, server, ConsoleState()) - self.looptime = 0 self.stream_path = None self.options = options @@ -398,14 +158,14 @@ class ConsoleMaster(flow.FlowMaster): for i in options.setheaders: self.setheaders.add(*i) - self.flow_list_walker = None - self.set_palette(options.palette) - r = self.set_intercept(options.intercept) if r: print >> sys.stderr, "Intercept error:", r sys.exit(1) + if options.limit: + self.set_limit(options.limit) + r = self.set_stickycookie(options.stickycookie) if r: print >> sys.stderr, "Sticky cookies error:", r @@ -425,12 +185,12 @@ class ConsoleMaster(flow.FlowMaster): self.rheaders = options.rheaders self.nopop = options.nopop self.showhost = options.showhost + self.palette = options.palette + self.palette_transparent = options.palette_transparent self.eventlog = options.eventlog self.eventlist = urwid.SimpleListWalker([]) - self.statusbar = None - if options.client_replay: self.client_playback_path(options.client_replay) @@ -453,15 +213,49 @@ class ConsoleMaster(flow.FlowMaster): print >> sys.stderr, "Stream file error:", err sys.exit(1) + self.view_stack = [] + if options.app: self.start_app(self.options.app_host, self.options.app_port) + signals.call_in.connect(self.sig_call_in) + signals.pop_view_state.connect(self.sig_pop_view_state) + signals.push_view_state.connect(self.sig_push_view_state) + + def __setattr__(self, name, value): + self.__dict__[name] = value + signals.update_settings.send(self) + + def sig_call_in(self, sender, seconds, callback, args=()): + def cb(*_): + return callback(*args) + self.loop.set_alarm_in(seconds, cb) + + def sig_pop_view_state(self, sender): + if len(self.view_stack) > 1: + self.view_stack.pop() + self.loop.widget = self.view_stack[-1] + else: + signals.status_prompt_onekey.send( + self, + prompt = "Quit", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = self.quit, + ) + + def sig_push_view_state(self, sender, window): + self.view_stack.append(window) + self.loop.widget = window + self.loop.draw_screen() def start_stream_to_path(self, path, mode="wb"): path = os.path.expanduser(path) try: f = file(path, mode) self.start_stream(f, None) - except IOError, v: + except IOError as v: return str(v) self.stream_path = path @@ -469,20 +263,24 @@ class ConsoleMaster(flow.FlowMaster): status, val = s.run(method, f) if val: if status: - self.add_event("Method %s return: %s"%(method, val), "debug") + self.add_event("Method %s return: %s" % (method, val), "debug") else: - self.add_event("Method %s error: %s"%(method, val[1]), "error") + self.add_event( + "Method %s error: %s" % + (method, val[1]), "error") def run_script_once(self, command, f): if not command: return - self.add_event("Running script on flow: %s"%command, "debug") + self.add_event("Running script on flow: %s" % command, "debug") try: s = script.Script(command, self) - except script.ScriptError, v: - self.statusbar.message("Error loading script.") - self.add_event("Error loading script:\n%s"%v.args[0], "error") + except script.ScriptError as v: + signals.status_message.send( + message = "Error loading script." + ) + self.add_event("Error loading script:\n%s" % v.args[0], "error") return if f.request: @@ -492,22 +290,21 @@ class ConsoleMaster(flow.FlowMaster): if f.error: self._run_script_method("error", s, f) s.unload() - self.refresh_flow(f) - self.state.last_script = command + signals.flow_change.send(self, flow = f) def set_script(self, command): if not command: return ret = self.load_script(command) if ret: - self.statusbar.message(ret) - self.state.last_script = command + signals.status_message.send(message=ret) def toggle_eventlog(self): self.eventlog = not self.eventlog + signals.pop_view_state.send(self) self.view_flowlist() - def _readflow(self, paths): + def _readflows(self, path): """ Utitility function that reads a list of flows or prints an error to the UI if that fails. @@ -516,22 +313,21 @@ class ConsoleMaster(flow.FlowMaster): - a list of flows, otherwise. """ try: - return flow.read_flows_from_paths(paths) + return flow.read_flows_from_paths(path) except flow.FlowReadError as e: - if not self.statusbar: - print >> sys.stderr, e.strerror - sys.exit(1) - else: - self.statusbar.message(e.strerror) - return None + signals.status_message.send(message=e.strerror) def client_playback_path(self, path): - flows = self._readflow(path) + if not isinstance(path, list): + path = [path] + flows = self._readflows(path) if flows: self.start_client_playback(flows, False) def server_playback_path(self, path): - flows = self._readflow(path) + if not isinstance(path, list): + path = [path] + flows = self._readflows(path) if flows: self.start_server_playback( flows, @@ -557,7 +353,9 @@ class ConsoleMaster(flow.FlowMaster): try: subprocess.call(cmd) except: - self.statusbar.message("Can't start editor: %s" % " ".join(c)) + signals.status_message.send( + message = "Can't start editor: %s" % " ".join(c) + ) else: data = open(name, "rb").read() self.ui.start() @@ -596,191 +394,34 @@ class ConsoleMaster(flow.FlowMaster): try: subprocess.call(cmd, shell=shell) except: - self.statusbar.message( - "Can't start external viewer: %s" % " ".join(c) + signals.status_message.send( + message="Can't start external viewer: %s" % " ".join(c) ) self.ui.start() os.unlink(name) def set_palette(self, name): - self.palette = palettes.palettes[name] - - def input_filter(self, keys, raw): - for k in keys: - if self.prompting: - if k == "esc": - self.prompt_cancel() - elif self.onekey: - if k == "enter": - self.prompt_cancel() - elif k in self.onekey: - self.prompt_execute(k) - elif k == "enter": - self.prompt_execute() - else: - self.view.keypress(self.loop.screen_size, k) - else: - k = self.view.keypress(self.loop.screen_size, k) - if k: - self.statusbar.message("") - if k == "?": - self.view_help() - elif k == "c": - if not self.client_playback: - self.path_prompt( - "Client replay: ", - self.state.last_saveload, - self.client_playback_path - ) - else: - self.prompt_onekey( - "Stop current client replay?", - ( - ("yes", "y"), - ("no", "n"), - ), - self.stop_client_playback_prompt, - ) - elif k == "H": - self.view_grideditor( - grideditor.SetHeadersEditor( - self, - self.setheaders.get_specs(), - self.setheaders.set - ) - ) - elif k == "I": - self.view_grideditor( - grideditor.HostPatternEditor( - self, - [[x] for x in self.get_ignore_filter()], - self.edit_ignore_filter - ) - ) - elif k == "T": - self.view_grideditor( - grideditor.HostPatternEditor( - self, - [[x] for x in self.get_tcp_filter()], - self.edit_tcp_filter - ) - ) - elif k == "i": - self.prompt( - "Intercept filter: ", - self.state.intercept_txt, - self.set_intercept - ) - elif k == "Q": - raise urwid.ExitMainLoop - elif k == "q": - self.prompt_onekey( - "Quit", - ( - ("yes", "y"), - ("no", "n"), - ), - self.quit, - ) - elif k == "M": - self.prompt_onekey( - "Global default display mode", - contentview.view_prompts, - self.change_default_display_mode - ) - elif k == "R": - self.view_grideditor( - grideditor.ReplaceEditor( - self, - self.replacehooks.get_specs(), - self.replacehooks.set - ) - ) - elif k == "s": - self.view_grideditor( - grideditor.ScriptEditor( - self, - [[i.command] for i in self.scripts], - self.edit_scripts - ) - ) - #if self.scripts: - # self.load_script(None) - #else: - # self.path_prompt( - # "Set script: ", - # self.state.last_script, - # self.set_script - # ) - elif k == "S": - if not self.server_playback: - self.path_prompt( - "Server replay path: ", - self.state.last_saveload, - self.server_playback_path - ) - else: - self.prompt_onekey( - "Stop current server replay?", - ( - ("yes", "y"), - ("no", "n"), - ), - self.stop_server_playback_prompt, - ) - elif k == "o": - self.prompt_onekey( - "Options", - ( - ("anticache", "a"), - ("anticomp", "c"), - ("showhost", "h"), - ("killextra", "k"), - ("norefresh", "n"), - ("no-upstream-certs", "u"), - ), - self._change_options - ) - elif k == "t": - self.prompt( - "Sticky cookie filter: ", - self.stickycookie_txt, - self.set_stickycookie - ) - elif k == "u": - self.prompt( - "Sticky auth filter: ", - self.stickyauth_txt, - self.set_stickyauth - ) - self.statusbar.redraw() + self.palette = name + self.ui.register_palette( + palettes.palettes[name].palette(self.palette_transparent) + ) + self.ui.clear() def ticker(self, *userdata): changed = self.tick(self.masterq, timeout=0) if changed: self.loop.draw_screen() - self.statusbar.redraw() + signals.update_settings.send() self.loop.set_alarm_in(0.01, self.ticker) def run(self): self.ui = urwid.raw_display.Screen() self.ui.set_terminal_properties(256) - self.ui.register_palette(self.palette.palette()) - self.flow_list_walker = flowlist.FlowListWalker(self, self.state) - self.view = None - self.statusbar = None - self.header = None - self.body = None - self.help_context = None - self.prompting = False - self.onekey = False + self.set_palette(self.palette) self.loop = urwid.MainLoop( - self.view, + urwid.SolidFill("x"), screen = self.ui, - input_filter = self.input_filter ) - self.view_flowlist() - self.statusbar.redraw() self.server.start_slave( controller.Slave, @@ -801,6 +442,19 @@ class ConsoleMaster(flow.FlowMaster): sys.exit(1) self.loop.set_alarm_in(0.01, self.ticker) + + # It's not clear why we need to handle this explicitly - without this, + # mitmproxy hangs on keyboard interrupt. Remove if we ever figure it + # out. + def exit(s, f): + raise urwid.ExitMainLoop + signal.signal(signal.SIGINT, exit) + + self.loop.set_alarm_in( + 0.0001, + lambda *args: self.view_flowlist() + ) + try: self.loop.run() except Exception: @@ -814,43 +468,56 @@ class ConsoleMaster(flow.FlowMaster): sys.stderr.flush() self.shutdown() - def make_view(self): - self.view = urwid.Frame( - self.body, - header = self.header, - footer = self.statusbar + def view_help(self, helpctx): + signals.push_view_state.send( + self, + window = window.Window( + self, + help.HelpView(helpctx), + None, + statusbar.StatusBar(self, help.footer), + None + ) ) - self.view.set_focus("body") - return self.view - def view_help(self): - h = help.HelpView( + def view_options(self): + for i in self.view_stack: + if isinstance(i["body"], options.Options): + return + signals.push_view_state.send( self, - self.help_context, - (self.statusbar, self.body, self.header) + window = window.Window( + self, + options.Options(self), + None, + statusbar.StatusBar(self, options.footer), + options.help_context, + ) ) - self.statusbar = StatusBar(self, help.footer) - self.body = h - self.header = None - self.loop.widget = self.make_view() - def view_flowdetails(self, flow): - h = flowdetailview.FlowDetailsView( + def view_palette_picker(self): + signals.push_view_state.send( self, - flow, - (self.statusbar, self.body, self.header) + window = window.Window( + self, + palettepicker.PalettePicker(self), + None, + statusbar.StatusBar(self, palettepicker.footer), + palettepicker.help_context, + ) ) - self.statusbar = StatusBar(self, flowdetailview.footer) - self.body = h - self.header = None - self.loop.widget = self.make_view() def view_grideditor(self, ge): - self.body = ge - self.header = None - self.help_context = ge.make_help() - self.statusbar = StatusBar(self, grideditor.footer) - self.loop.widget = self.make_view() + signals.push_view_state.send( + self, + window = window.Window( + self, + ge, + None, + statusbar.StatusBar(self, grideditor.FOOTER), + ge.make_help() + ) + ) def view_flowlist(self): if self.ui.started: @@ -859,27 +526,35 @@ class ConsoleMaster(flow.FlowMaster): self.state.set_focus(self.state.flow_count()) if self.eventlog: - self.body = flowlist.BodyPile(self) + body = flowlist.BodyPile(self) else: - self.body = flowlist.FlowListBox(self) - self.statusbar = StatusBar(self, flowlist.footer) - self.header = None - self.state.view_mode = common.VIEW_LIST - - self.loop.widget = self.make_view() - self.help_context = flowlist.help_context - - def view_flow(self, flow): - self.body = flowview.FlowView(self, self.state, flow) - self.header = flowview.FlowViewHeader(self, flow) - self.statusbar = StatusBar(self, flowview.footer) + body = flowlist.FlowListBox(self) + + signals.push_view_state.send( + self, + window = window.Window( + self, + body, + None, + statusbar.StatusBar(self, flowlist.footer), + flowlist.help_context + ) + ) + + def view_flow(self, flow, tab_offset=0): self.state.set_focus_flow(flow) - self.state.view_mode = common.VIEW_FLOW - self.loop.widget = self.make_view() - self.help_context = flowview.help_context + signals.push_view_state.send( + self, + window = window.Window( + self, + flowview.FlowView(self, self.state, flow, tab_offset), + flowview.FlowViewHeader(self, flow), + statusbar.StatusBar(self, flowview.footer), + flowview.help_context + ) + ) def _write_flows(self, path, flows): - self.state.last_saveload = path if not path: return path = os.path.expanduser(path) @@ -889,8 +564,8 @@ class ConsoleMaster(flow.FlowMaster): for i in flows: fw.add(i) f.close() - except IOError, v: - self.statusbar.message(v.strerror) + except IOError as v: + signals.status_message.send(message=v.strerror) def save_one_flow(self, path, flow): return self._write_flows(path, [flow]) @@ -902,74 +577,23 @@ class ConsoleMaster(flow.FlowMaster): if not path: return ret = self.load_flows_path(path) - return ret or "Flows loaded from %s"%path + return ret or "Flows loaded from %s" % path def load_flows_path(self, path): - self.state.last_saveload = path reterr = None try: flow.FlowMaster.load_flows_file(self, path) - except flow.FlowReadError, v: + except flow.FlowReadError as v: reterr = str(v) - if self.flow_list_walker: - self.sync_list_view() + signals.flowlist_change.send(self) return reterr - def path_prompt(self, prompt, text, callback, *args): - self.statusbar.path_prompt(prompt, text) - self.view.set_focus("footer") - self.prompting = (callback, args) - - def prompt(self, prompt, text, callback, *args): - self.statusbar.prompt(prompt, text) - self.view.set_focus("footer") - self.prompting = (callback, args) - - def prompt_edit(self, prompt, text, callback): - self.statusbar.prompt(prompt + ": ", text) - self.view.set_focus("footer") - self.prompting = (callback, []) - - def prompt_onekey(self, prompt, keys, callback, *args): - """ - Keys are a set of (word, key) tuples. The appropriate key in the - word is highlighted. - """ - prompt = [prompt, " ("] - mkup = [] - for i, e in enumerate(keys): - mkup.extend(common.highlight_key(e[0], e[1])) - if i < len(keys)-1: - mkup.append(",") - prompt.extend(mkup) - prompt.append(")? ") - self.onekey = "".join(i[1] for i in keys) - self.prompt(prompt, "", callback, *args) - - def prompt_done(self): - self.prompting = False - self.onekey = False - self.view.set_focus("body") - self.statusbar.message("") - - def prompt_execute(self, txt=None): - if not txt: - txt = self.statusbar.get_edit_text() - p, args = self.prompting - self.prompt_done() - msg = p(txt, *args) - if msg: - self.statusbar.message(msg, 1000) - - def prompt_cancel(self): - self.prompt_done() - def accept_all(self): self.state.accept_all(self) def set_limit(self, txt): v = self.state.set_limit(txt) - self.sync_list_view() + signals.flowlist_change.send(self) return v def set_intercept(self, txt): @@ -980,12 +604,6 @@ class ConsoleMaster(flow.FlowMaster): self.state.default_body_view = v self.refresh_focus() - def pop_view(self): - if self.state.view_mode == common.VIEW_FLOW: - self.view_flow(self.state.view[self.state.focus]) - else: - self.view_flowlist() - def edit_scripts(self, scripts): commands = [x[0] for x in scripts] # remove outer array if commands == [s.command for s in self.scripts]: @@ -994,14 +612,7 @@ class ConsoleMaster(flow.FlowMaster): self.unload_scripts() for command in commands: self.load_script(command) - - def edit_ignore_filter(self, ignore): - patterns = (x[0] for x in ignore) - self.set_ignore_filter(patterns) - - def edit_tcp_filter(self, tcp): - patterns = (x[0] for x in tcp) - self.set_tcp_filter(patterns) + signals.update_settings.send(self) def stop_client_playback_prompt(self, a): if a != "n": @@ -1015,33 +626,13 @@ class ConsoleMaster(flow.FlowMaster): if a != "n": raise urwid.ExitMainLoop - def _change_options(self, a): - if a == "a": - self.anticache = not self.anticache - if a == "c": - self.anticomp = not self.anticomp - if a == "h": - self.showhost = not self.showhost - self.sync_list_view() - self.refresh_focus() - elif a == "k": - self.killextra = not self.killextra - elif a == "n": - self.refresh_server_playback = not self.refresh_server_playback - elif a == "u": - self.server.config.no_upstream_cert =\ - not self.server.config.no_upstream_cert - def shutdown(self): self.state.killall(self) flow.FlowMaster.shutdown(self) - def sync_list_view(self): - self.flow_list_walker._modified() - def clear_flows(self): self.state.clear() - self.sync_list_view() + signals.flowlist_change.send(self) def toggle_follow_flows(self): # toggle flow follow @@ -1049,31 +640,27 @@ class ConsoleMaster(flow.FlowMaster): # jump to most recent flow if follow is now on if self.state.follow_focus: self.state.set_focus(self.state.flow_count()) - self.sync_list_view() + signals.flowlist_change.send(self) def delete_flow(self, f): self.state.delete_flow(f) - self.sync_list_view() + signals.flowlist_change.send(self) def refresh_focus(self): if self.state.view: - self.refresh_flow(self.state.view[self.state.focus]) - - def refresh_flow(self, c): - if hasattr(self.header, "refresh_flow"): - self.header.refresh_flow(c) - if hasattr(self.body, "refresh_flow"): - self.body.refresh_flow(c) - if hasattr(self.statusbar, "refresh_flow"): - self.statusbar.refresh_flow(c) + signals.flow_change.send( + self, + flow = self.state.view[self.state.focus] + ) def process_flow(self, f): - if self.state.intercept and f.match(self.state.intercept) and not f.request.is_replay: + if self.state.intercept and f.match( + self.state.intercept) and not f.request.is_replay: f.intercept(self) else: f.reply() - self.sync_list_view() - self.refresh_flow(f) + signals.flowlist_change.send(self) + signals.flow_change.send(self, flow = f) def clear_events(self): self.eventlist[:] = [] @@ -1090,7 +677,7 @@ class ConsoleMaster(flow.FlowMaster): self.eventlist.append(e) if len(self.eventlist) > EVENTLOG_SIZE: self.eventlist.pop(0) - self.eventlist.set_focus(len(self.eventlist)-1) + self.eventlist.set_focus(len(self.eventlist) - 1) # Handlers def handle_error(self, f): diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index 3a708c7c..3180170d 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -6,15 +6,14 @@ import os from .. import utils from ..protocol.http import CONTENT_MISSING, decoded +from . import signals +import netlib.utils try: import pyperclip except: pyperclip = False -VIEW_LIST = 0 -VIEW_FLOW = 1 - VIEW_FLOW_REQUEST = 0 VIEW_FLOW_RESPONSE = 1 @@ -31,14 +30,22 @@ METHOD_OPTIONS = [ ] -def highlight_key(s, k): +def is_keypress(k): + """ + Is this input event a keypress? + """ + if isinstance(k, basestring): + return True + + +def highlight_key(str, key, textattr="text", keyattr="key"): l = [] - parts = s.split(k, 1) + parts = str.split(key, 1) if parts[0]: - l.append(("text", parts[0])) - l.append(("key", k)) + l.append((textattr, parts[0])) + l.append((keyattr, key)) if parts[1]: - l.append(("text", parts[1])) + l.append((textattr, parts[1])) return l @@ -60,20 +67,26 @@ def format_keyvals(lst, key="key", val="text", indent=0): if kv is None: ret.append(urwid.Text("")) else: - cols = [] - # This cumbersome construction process is here for a reason: - # Urwid < 1.0 barfs if given a fixed size column of size zero. - if indent: - cols.append(("fixed", indent, urwid.Text(""))) - cols.extend([ - ( - "fixed", - maxk, - urwid.Text([(key, kv[0] or "")]) - ), - kv[1] if isinstance(kv[1], urwid.Widget) else urwid.Text([(val, kv[1])]) - ]) - ret.append(urwid.Columns(cols, dividechars = 2)) + if isinstance(kv[1], urwid.Widget): + v = kv[1] + elif kv[1] is None: + v = urwid.Text("") + else: + v = urwid.Text([(val, kv[1])]) + ret.append( + urwid.Columns( + [ + ("fixed", indent, urwid.Text("")), + ( + "fixed", + maxk, + urwid.Text([(key, kv[0] or "")]) + ), + v + ], + dividechars = 2 + ) + ) return ret @@ -151,7 +164,7 @@ def raw_format_flow(f, focus, extended, padding): 4: "code_400", 5: "code_500", } - ccol = codes.get(f["resp_code"]/100, "code_other") + ccol = codes.get(f["resp_code"] / 100, "code_other") resp.append(fcol(SYMBOL_RETURN, ccol)) if f["resp_is_replay"]: resp.append(fcol(SYMBOL_REPLAY, "replay")) @@ -184,23 +197,39 @@ def raw_format_flow(f, focus, extended, padding): def save_data(path, data, master, state): if not path: return - state.last_saveload = path - path = os.path.expanduser(path) try: with file(path, "wb") as f: f.write(data) - except IOError, v: - master.statusbar.message(v.strerror) + except IOError as v: + signals.status_message.send(message=v.strerror) + + +def ask_save_overwite(path, data, master, state): + if not path: + return + path = os.path.expanduser(path) + if os.path.exists(path): + def save_overwite(k): + if k == "y": + save_data(path, data, master, state) + + signals.status_prompt_onekey.send( + prompt = "'" + path + "' already exists. Overwite?", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = save_overwite + ) + else: + save_data(path, data, master, state) def ask_save_path(prompt, data, master, state): - master.path_prompt( - prompt, - state.last_saveload, - save_data, - data, - master, - state + signals.status_prompt_path.send( + prompt = prompt, + callback = ask_save_overwite, + args = (data, master, state) ) @@ -210,6 +239,8 @@ def copy_flow_format_data(part, scope, flow): else: data = "" if scope in ("q", "a"): + if flow.request.content is None or flow.request.content == CONTENT_MISSING: + return None, "Request content is missing" with decoded(flow.request): if part == "h": data += flow.request.assemble() @@ -221,6 +252,8 @@ def copy_flow_format_data(part, scope, flow): # Add padding between request and response data += "\r\n" * 2 if scope in ("s", "a") and flow.response: + if flow.response.content is None or flow.response.content == CONTENT_MISSING: + return None, "Response content is missing" with decoded(flow.response): if part == "h": data += flow.response.assemble() @@ -228,40 +261,43 @@ def copy_flow_format_data(part, scope, flow): data += flow.response.content else: raise ValueError("Unknown part: {}".format(part)) - return data + return data, False def copy_flow(part, scope, flow, master, state): """ - part: _c_ontent, _a_ll, _u_rl + part: _c_ontent, _h_eaders+content, _u_rl scope: _a_ll, re_q_uest, re_s_ponse """ - data = copy_flow_format_data(part, scope, flow) + data, err = copy_flow_format_data(part, scope, flow) + + if err: + signals.status_message.send(message=err) + return if not data: if scope == "q": - master.statusbar.message("No request content to copy.") + signals.status_message.send(message="No request content to copy.") elif scope == "s": - master.statusbar.message("No response content to copy.") + signals.status_message.send(message="No response content to copy.") else: - master.statusbar.message("No contents to copy.") + signals.status_message.send(message="No contents to copy.") return try: master.add_event(str(len(data))) pyperclip.copy(data) - except RuntimeError: + except (RuntimeError, UnicodeDecodeError): def save(k): if k == "y": - ask_save_path("Save data: ", data, master, state) - - master.prompt_onekey( - "Cannot copy binary data to clipboard. Save as file?", - ( + ask_save_path("Save data", data, master, state) + signals.status_prompt_onekey.send( + prompt = "Cannot copy binary data to clipboard. Save as file?", + keys = ( ("yes", "y"), ("no", "n"), ), - save + callback = save ) @@ -273,14 +309,11 @@ def ask_copy_part(scope, flow, master, state): if scope != "s": choices.append(("url", "u")) - master.prompt_onekey( - "Copy", - choices, - copy_flow, - scope, - flow, - master, - state + signals.status_prompt_onekey.send( + prompt = "Copy", + keys = choices, + callback = copy_flow, + args = (scope, flow, master, state) ) @@ -297,16 +330,14 @@ def ask_save_body(part, master, state, flow): # We first need to determine whether we want to save the request or the # response content. if request_has_content and response_has_content: - master.prompt_onekey( - "Save", - ( + signals.status_prompt_onekey.send( + prompt = "Save", + keys = ( ("request", "q"), ("response", "s"), ), - ask_save_body, - master, - state, - flow + callback = ask_save_body, + args = (master, state, flow) ) elif response_has_content: ask_save_body("s", master, state, flow) @@ -315,27 +346,23 @@ def ask_save_body(part, master, state, flow): elif part == "q" and request_has_content: ask_save_path( - "Save request content: ", + "Save request content", flow.request.get_decoded_content(), master, state ) elif part == "s" and response_has_content: ask_save_path( - "Save response content: ", + "Save response content", flow.response.get_decoded_content(), master, state ) else: - master.statusbar.message("No content to save.") + signals.status_message.send(message="No content to save.") -class FlowCache: - @utils.LRUCache(200) - def format_flow(self, *args): - return raw_format_flow(*args) -flowcache = FlowCache() +flowcache = utils.LRUCache(800) def format_flow(f, focus, extended=False, hostheader=False, padding=2): @@ -353,7 +380,7 @@ def format_flow(f, focus, extended=False, hostheader=False, padding=2): ) if f.response: if f.response.content: - contentdesc = utils.pretty_size(len(f.response.content)) + contentdesc = netlib.utils.pretty_size(len(f.response.content)) elif f.response.content == CONTENT_MISSING: contentdesc = "[content missing]" else: @@ -374,6 +401,7 @@ def format_flow(f, focus, extended=False, hostheader=False, padding=2): d["resp_ctype"] = t[0].split(";")[0] else: d["resp_ctype"] = "" - return flowcache.format_flow( + return flowcache.get( + raw_format_flow, tuple(sorted(d.items())), focus, extended, padding ) diff --git a/libmproxy/console/contentview.py b/libmproxy/console/contentview.py index 95d908a4..2b3c6def 100644 --- a/libmproxy/console/contentview.py +++ b/libmproxy/console/contentview.py @@ -6,27 +6,27 @@ import lxml.html import lxml.etree from PIL import Image from PIL.ExifTags import TAGS -import re import subprocess import traceback import urwid import netlib.utils +from netlib import odict from . import common -from .. import utils, encoding, flow +from .. import utils, encoding from ..contrib import jsbeautifier, html2text from ..contrib.wbxml.ASCommandResponse import ASCommandResponse try: import pyamf from pyamf import remoting, flex -except ImportError: # pragma nocover +except ImportError: # pragma nocover pyamf = None try: import cssutils -except ImportError: # pragma nocover +except ImportError: # pragma nocover cssutils = None else: cssutils.log.setLevel(logging.CRITICAL) @@ -36,7 +36,7 @@ else: cssutils.ser.prefs.indentClosingBrace = False cssutils.ser.prefs.validOnly = False -VIEW_CUTOFF = 1024*50 +VIEW_CUTOFF = 1024 * 50 def _view_text(content, total, limit): @@ -59,7 +59,7 @@ def trailer(clen, txt, limit): txt.append( urwid.Text( [ - ("highlight", "... %s of data not shown. Press "%utils.pretty_size(rem)), + ("highlight", "... %s of data not shown. Press " % netlib.utils.pretty_size(rem)), ("key", "f"), ("highlight", " to load all data.") ] @@ -76,7 +76,7 @@ class ViewAuto: ctype = hdrs.get_first("content-type") if ctype: ct = utils.parse_content_type(ctype) if ctype else None - ct = "%s/%s"%(ct[0], ct[1]) + ct = "%s/%s" % (ct[0], ct[1]) if ct in content_types_map: return content_types_map[ct][0](hdrs, content, limit) elif utils.isXML(content): @@ -227,7 +227,7 @@ class ViewURLEncoded: lines = utils.urldecode(content) if lines: body = common.format_keyvals( - [(k+":", v) for (k, v) in lines], + [(k + ":", v) for (k, v) in lines], key = "header", val = "text" ) @@ -240,33 +240,13 @@ class ViewMultipart: content_types = ["multipart/form-data"] def __call__(self, hdrs, content, limit): - v = hdrs.get_first("content-type") + v = utils.multipartdecode(hdrs, content) if v: - v = utils.parse_content_type(v) - if not v: - return - boundary = v[2].get("boundary") - if not boundary: - return - - rx = re.compile(r'\bname="([^"]+)"') - keys = [] - vals = [] - - for i in content.split("--" + boundary): - parts = i.splitlines() - if len(parts) > 1 and parts[0][0:2] != "--": - match = rx.search(parts[1]) - if match: - keys.append(match.group(1) + ":") - vals.append(netlib.utils.cleanBin( - "\n".join(parts[3+parts[2:].index(""):]) - )) r = [ urwid.Text(("highlight", "Form data:\n")), ] r.extend(common.format_keyvals( - zip(keys, vals), + v, key = "header", val = "text" )) @@ -324,7 +304,6 @@ if pyamf: if not envelope: return None - txt = [] for target, message in iter(envelope): if isinstance(message, pyamf.remoting.Request): @@ -335,13 +314,13 @@ if pyamf: else: txt.append(urwid.Text([ ("header", "Response: "), - ("text", "%s, code %s"%(target, message.status)), + ("text", "%s, code %s" % (target, message.status)), ])) s = json.dumps(self.unpack(message), indent=4) txt.extend(_view_text(s[:limit], len(s), limit)) - return "AMF v%s"%envelope.amfVersion, txt + return "AMF v%s" % envelope.amfVersion, txt class ViewJavaScript: @@ -395,7 +374,7 @@ class ViewImage: return None parts = [ ("Format", str(img.format_description)), - ("Size", "%s x %s px"%img.size), + ("Size", "%s x %s px" % img.size), ("Mode", str(img.mode)), ] for i in sorted(img.info.keys()): @@ -421,7 +400,7 @@ class ViewImage: key = "header", val = "text" ) - return "%s image"%img.format, fmt + return "%s image" % img.format, fmt class ViewProtobuf: @@ -539,14 +518,14 @@ def get_content_view(viewmode, hdrItems, content, limit, logfunc, is_request): return "No content", "" msg = [] - hdrs = flow.ODictCaseless([list(i) for i in hdrItems]) + hdrs = odict.ODictCaseless([list(i) for i in hdrItems]) enc = hdrs.get_first("content-encoding") if enc and enc != "identity": decoded = encoding.decode(enc, content) if decoded: content = decoded - msg.append("[decoded %s]"%enc) + msg.append("[decoded %s]" % enc) try: ret = viewmode(hdrs, content, limit) # Third-party viewers can fail in unexpected ways... diff --git a/libmproxy/console/flowdetailview.py b/libmproxy/console/flowdetailview.py index f351bff1..40769c95 100644 --- a/libmproxy/console/flowdetailview.py +++ b/libmproxy/console/flowdetailview.py @@ -1,113 +1,154 @@ from __future__ import absolute_import import urwid -from . import common +from . import common, searchable from .. import utils -footer = [ - ('heading_key', "q"), ":back ", -] -class FlowDetailsView(urwid.ListBox): - def __init__(self, master, flow, state): - self.master, self.flow, self.state = master, flow, state - urwid.ListBox.__init__( - self, - self.flowtext() - ) +def maybe_timestamp(base, attr): + if base and getattr(base, attr): + return utils.format_timestamp_with_milli(getattr(base, attr)) + else: + return "active" + pass - def keypress(self, size, key): - key = common.shortcuts(key) - if key == "q": - self.master.statusbar = self.state[0] - self.master.body = self.state[1] - self.master.header = self.state[2] - self.master.loop.widget = self.master.make_view() - return None - elif key == "?": - key = None - return urwid.ListBox.keypress(self, size, key) - - def flowtext(self): - text = [] - - title = urwid.Text("Flow details") - title = urwid.Padding(title, align="left", width=("relative", 100)) - title = urwid.AttrWrap(title, "heading") - text.append(title) - - cc = self.flow.client_conn - sc = self.flow.server_conn - req = self.flow.request - resp = self.flow.response - - if sc: - text.append(urwid.Text([("head", "Server Connection:")])) - parts = [ - ["Address", "%s:%s" % sc.address()], - ] - text.extend(common.format_keyvals(parts, key="key", val="text", indent=4)) - - c = sc.cert - if c: - text.append(urwid.Text([("head", "Server Certificate:")])) - parts = [ - ["Type", "%s, %s bits"%c.keyinfo], - ["SHA1 digest", c.digest("sha1")], - ["Valid to", str(c.notafter)], - ["Valid from", str(c.notbefore)], - ["Serial", str(c.serial)], - [ - "Subject", - urwid.BoxAdapter( - urwid.ListBox(common.format_keyvals(c.subject, key="highlight", val="text")), - len(c.subject) - ) - ], - [ - "Issuer", - urwid.BoxAdapter( - urwid.ListBox(common.format_keyvals(c.issuer, key="highlight", val="text")), - len(c.issuer) - ) - ] - ] +def flowdetails(state, flow): + text = [] - if c.altnames: - parts.append( - [ - "Alt names", - ", ".join(c.altnames) - ] - ) - text.extend(common.format_keyvals(parts, key="key", val="text", indent=4)) + cc = flow.client_conn + sc = flow.server_conn + req = flow.request + resp = flow.response - if cc: - text.append(urwid.Text([("head", "Client Connection:")])) + if sc: + text.append(urwid.Text([("head", "Server Connection:")])) + parts = [ + ["Address", "%s:%s" % sc.address()], + ] + + text.extend( + common.format_keyvals(parts, key="key", val="text", indent=4) + ) + c = sc.cert + if c: + text.append(urwid.Text([("head", "Server Certificate:")])) parts = [ - ["Address", "%s:%s" % cc.address()], - # ["Requests", "%s"%cc.requestcount], + ["Type", "%s, %s bits" % c.keyinfo], + ["SHA1 digest", c.digest("sha1")], + ["Valid to", str(c.notafter)], + ["Valid from", str(c.notbefore)], + ["Serial", str(c.serial)], + [ + "Subject", + urwid.BoxAdapter( + urwid.ListBox( + common.format_keyvals( + c.subject, + key="highlight", + val="text" + ) + ), + len(c.subject) + ) + ], + [ + "Issuer", + urwid.BoxAdapter( + urwid.ListBox( + common.format_keyvals( + c.issuer, key="highlight", val="text" + ) + ), + len(c.issuer) + ) + ] ] - text.extend(common.format_keyvals(parts, key="key", val="text", indent=4)) + if c.altnames: + parts.append( + [ + "Alt names", + ", ".join(c.altnames) + ] + ) + text.extend( + common.format_keyvals(parts, key="key", val="text", indent=4) + ) - parts = [] + if cc: + text.append(urwid.Text([("head", "Client Connection:")])) - parts.append(["Client conn. established", utils.format_timestamp_with_milli(cc.timestamp_start) if (cc and cc.timestamp_start) else "active"]) - parts.append(["Server conn. initiated", utils.format_timestamp_with_milli(sc.timestamp_start) if sc else "active" ]) - parts.append(["Server conn. TCP handshake", utils.format_timestamp_with_milli(sc.timestamp_tcp_setup) if (sc and sc.timestamp_tcp_setup) else "active"]) - if sc.ssl_established: - parts.append(["Server conn. SSL handshake", utils.format_timestamp_with_milli(sc.timestamp_ssl_setup) if sc.timestamp_ssl_setup else "active"]) - parts.append(["Client conn. SSL handshake", utils.format_timestamp_with_milli(cc.timestamp_ssl_setup) if (cc and cc.timestamp_ssl_setup) else "active"]) - parts.append(["First request byte", utils.format_timestamp_with_milli(req.timestamp_start)]) - parts.append(["Request complete", utils.format_timestamp_with_milli(req.timestamp_end) if req.timestamp_end else "active"]) - parts.append(["First response byte", utils.format_timestamp_with_milli(resp.timestamp_start) if resp else "active"]) - parts.append(["Response complete", utils.format_timestamp_with_milli(resp.timestamp_end) if (resp and resp.timestamp_end) else "active"]) + parts = [ + ["Address", "%s:%s" % cc.address()], + # ["Requests", "%s"%cc.requestcount], + ] - # sort operations by timestamp - parts = sorted(parts, key=lambda p: p[1]) + text.extend( + common.format_keyvals(parts, key="key", val="text", indent=4) + ) - text.append(urwid.Text([("head", "Timing:")])) - text.extend(common.format_keyvals(parts, key="key", val="text", indent=4)) - return text + parts = [] + + parts.append( + [ + "Client conn. established", + maybe_timestamp(cc, "timestamp_start") + ] + ) + parts.append( + [ + "Server conn. initiated", + maybe_timestamp(sc, "timestamp_start") + ] + ) + parts.append( + [ + "Server conn. TCP handshake", + maybe_timestamp(sc, "timestamp_tcp_setup") + ] + ) + if sc.ssl_established: + parts.append( + [ + "Server conn. SSL handshake", + maybe_timestamp(sc, "timestamp_ssl_setup") + ] + ) + parts.append( + [ + "Client conn. SSL handshake", + maybe_timestamp(cc, "timestamp_ssl_setup") + ] + ) + parts.append( + [ + "First request byte", + maybe_timestamp(req, "timestamp_start") + ] + ) + parts.append( + [ + "Request complete", + maybe_timestamp(req, "timestamp_end") + ] + ) + parts.append( + [ + "First response byte", + maybe_timestamp(resp, "timestamp_start") + ] + ) + parts.append( + [ + "Response complete", + maybe_timestamp(resp, "timestamp_end") + ] + ) + + # sort operations by timestamp + parts = sorted(parts, key=lambda p: p[1]) + + text.append(urwid.Text([("head", "Timing:")])) + text.extend(common.format_keyvals(parts, key="key", val="text", indent=4)) + return searchable.Searchable(state, text) diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py index 5d8ad942..fd071569 100644 --- a/libmproxy/console/flowlist.py +++ b/libmproxy/console/flowlist.py @@ -1,7 +1,7 @@ from __future__ import absolute_import import urwid from netlib import http -from . import common +from . import common, signals def _mkhelp(): @@ -15,10 +15,10 @@ def _mkhelp(): ("D", "duplicate flow"), ("e", "toggle eventlog"), ("F", "toggle follow flow list"), - ("g", "copy flow to clipboard"), ("l", "set limit filter pattern"), ("L", "load saved flows"), ("n", "create a new request"), + ("P", "copy flow to clipboard"), ("r", "replay request"), ("V", "revert changes to request"), ("w", "save flows "), @@ -47,6 +47,10 @@ class EventListBox(urwid.ListBox): if key == "C": self.master.clear_events() key = None + elif key == "G": + self.set_focus(0) + elif key == "g": + self.set_focus(len(self.master.eventlist) - 1) return urwid.ListBox.keypress(self, size, key) @@ -72,7 +76,8 @@ class BodyPile(urwid.Pile): def keypress(self, size, key): if key == "tab": - self.focus_position = (self.focus_position + 1)%len(self.widget_list) + self.focus_position = ( + self.focus_position + 1) % len(self.widget_list) if self.focus_position == 1: self.widget_list[1].header = self.active_header else: @@ -111,17 +116,15 @@ class ConnectionItem(urwid.WidgetWrap): def save_flows_prompt(self, k): if k == "a": - self.master.path_prompt( - "Save all flows to: ", - self.state.last_saveload, - self.master.save_flows + signals.status_prompt_path.send( + prompt = "Save all flows to", + callback = self.master.save_flows ) else: - self.master.path_prompt( - "Save this flow to: ", - self.state.last_saveload, - self.master.save_one_flow, - self.flow + signals.status_prompt_path.send( + prompt = "Save this flow to", + callback = self.master.save_one_flow, + args = (self.flow,) ) def stop_server_playback_prompt(self, a): @@ -150,64 +153,65 @@ class ConnectionItem(urwid.WidgetWrap): self.master.options.replay_ignore_host ) else: - self.master.path_prompt( - "Server replay path: ", - self.state.last_saveload, - self.master.server_playback_path + signals.status_prompt_path.send( + prompt = "Server replay path", + callback = self.master.server_playback_path ) - def keypress(self, (maxcol,), key): + def keypress(self, xxx_todo_changeme, key): + (maxcol,) = xxx_todo_changeme key = common.shortcuts(key) if key == "a": self.flow.accept_intercept(self.master) - self.master.sync_list_view() + signals.flowlist_change.send(self) elif key == "d": self.flow.kill(self.master) self.state.delete_flow(self.flow) - self.master.sync_list_view() + signals.flowlist_change.send(self) elif key == "D": f = self.master.duplicate_flow(self.flow) self.master.view_flow(f) elif key == "r": r = self.master.replay_request(self.flow) if r: - self.master.statusbar.message(r) - self.master.sync_list_view() + signals.status_message.send(message=r) + signals.flowlist_change.send(self) elif key == "S": if not self.master.server_playback: - self.master.prompt_onekey( - "Server Replay", - ( + signals.status_prompt_onekey.send( + prompt = "Server Replay", + keys = ( ("all flows", "a"), ("this flow", "t"), ("file", "f"), ), - self.server_replay_prompt, + callback = self.server_replay_prompt, ) else: - self.master.prompt_onekey( - "Stop current server replay?", - ( + signals.status_prompt_onekey.send( + prompt = "Stop current server replay?", + keys = ( ("yes", "y"), ("no", "n"), ), - self.stop_server_playback_prompt, + callback = self.stop_server_playback_prompt, ) elif key == "V": if not self.flow.modified(): - self.master.statusbar.message("Flow not modified.") + signals.status_message.send(message="Flow not modified.") return self.state.revert(self.flow) - self.master.sync_list_view() - self.master.statusbar.message("Reverted.") + signals.flowlist_change.send(self) + signals.status_message.send(message="Reverted.") elif key == "w": - self.master.prompt_onekey( - "Save", - ( + signals.status_prompt_onekey.send( + self, + prompt = "Save", + keys = ( ("all flows", "a"), ("this flow", "t"), ), - self.save_flows_prompt, + callback = self.save_flows_prompt, ) elif key == "X": self.flow.kill(self.master) @@ -215,13 +219,12 @@ class ConnectionItem(urwid.WidgetWrap): if self.flow.request: self.master.view_flow(self.flow) elif key == "|": - self.master.path_prompt( - "Send flow to script: ", - self.state.last_script, - self.master.run_script_once, - self.flow + signals.status_prompt_path.send( + prompt = "Send flow to script", + callback = self.master.run_script_once, + args = (self.flow,) ) - elif key == "g": + elif key == "P": common.ask_copy_part("a", self.flow, self.master, self.state) elif key == "b": common.ask_save_body(None, self.master, self.state, self.flow) @@ -232,8 +235,10 @@ class ConnectionItem(urwid.WidgetWrap): class FlowListWalker(urwid.ListWalker): def __init__(self, master, state): self.master, self.state = master, state - if self.state.flow_count(): - self.set_focus(0) + signals.flowlist_change.connect(self.sig_flowlist_change) + + def sig_flowlist_change(self, sender): + self._modified() def get_focus(self): f, i = self.state.get_focus() @@ -258,7 +263,10 @@ class FlowListWalker(urwid.ListWalker): class FlowListBox(urwid.ListBox): def __init__(self, master): self.master = master - urwid.ListBox.__init__(self, master.flow_list_walker) + urwid.ListBox.__init__( + self, + FlowListWalker(master, master.state) + ) def get_method_raw(self, k): if k: @@ -266,7 +274,12 @@ class FlowListBox(urwid.ListBox): def get_method(self, k): if k == "e": - self.master.prompt("Method:", "", self.get_method_raw) + signals.status_prompt.send( + self, + prompt = "Method", + text = "", + callback = self.get_method_raw + ) else: method = "" for i in common.METHOD_OPTIONS: @@ -275,17 +288,17 @@ class FlowListBox(urwid.ListBox): self.get_url(method) def get_url(self, method): - self.master.prompt( - "URL:", - "http://www.example.com/", - self.new_request, - method + signals.status_prompt.send( + prompt = "URL", + text = "http://www.example.com/", + callback = self.new_request, + args = (method,) ) def new_request(self, url, method): parts = http.parse_url(str(url)) if not parts: - self.master.statusbar.message("Invalid Url") + signals.status_message.send(message="Invalid Url") return scheme, host, port, path = parts f = self.master.create_request(method, scheme, host, port, path) @@ -295,28 +308,34 @@ class FlowListBox(urwid.ListBox): key = common.shortcuts(key) if key == "A": self.master.accept_all() - self.master.sync_list_view() + signals.flowlist_change.send(self) elif key == "C": self.master.clear_flows() elif key == "e": self.master.toggle_eventlog() + elif key == "G": + self.master.state.set_focus(0) + signals.flowlist_change.send(self) + elif key == "g": + self.master.state.set_focus(self.master.state.flow_count()) + signals.flowlist_change.send(self) elif key == "l": - self.master.prompt( - "Limit: ", - self.master.state.limit_txt, - self.master.set_limit + signals.status_prompt.send( + prompt = "Limit", + text = self.master.state.limit_txt, + callback = self.master.set_limit ) elif key == "L": - self.master.path_prompt( - "Load flows: ", - self.master.state.last_saveload, - self.master.load_flows_callback + signals.status_prompt_path.send( + self, + prompt = "Load flows", + callback = self.master.load_flows_callback ) elif key == "n": - self.master.prompt_onekey( - "Method", - common.METHOD_OPTIONS, - self.get_method + signals.status_prompt_onekey.send( + prompt = "Method", + keys = common.METHOD_OPTIONS, + callback = self.get_method ) elif key == "F": self.master.toggle_follow_flows() @@ -324,10 +343,10 @@ class FlowListBox(urwid.ListBox): if self.master.stream: self.master.stop_stream() else: - self.master.path_prompt( - "Stream flows to: ", - self.master.state.last_saveload, - self.master.start_stream_to_path + signals.status_prompt_path.send( + self, + prompt = "Stream flows to", + callback = self.master.start_stream_to_path ) else: return urwid.ListBox.keypress(self, size, key) diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 89e75aad..43a40d69 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -1,12 +1,16 @@ from __future__ import absolute_import -import os, sys, copy +import os +import sys import urwid -from . import common, grideditor, contentview -from .. import utils, flow, controller +from netlib import odict +from . import common, grideditor, contentview, signals, searchable, tabs +from . import flowdetailview +from .. import utils, controller from ..protocol.http import HTTPRequest, HTTPResponse, CONTENT_MISSING, decoded -class SearchError(Exception): pass +class SearchError(Exception): + pass def _mkhelp(): @@ -19,58 +23,58 @@ def _mkhelp(): ("D", "duplicate flow"), ("e", "edit request/response"), ("f", "load full body data"), - ("g", "copy response(content/headers) to clipboard"), ("m", "change body display mode for this entity"), - (None, - common.highlight_key("automatic", "a") + - [("text", ": automatic detection")] - ), - (None, - common.highlight_key("hex", "e") + - [("text", ": Hex")] - ), - (None, - common.highlight_key("html", "h") + - [("text", ": HTML")] - ), - (None, - common.highlight_key("image", "i") + - [("text", ": Image")] - ), - (None, - common.highlight_key("javascript", "j") + - [("text", ": JavaScript")] - ), - (None, - common.highlight_key("json", "s") + - [("text", ": JSON")] - ), - (None, - common.highlight_key("urlencoded", "u") + - [("text", ": URL-encoded data")] - ), - (None, - common.highlight_key("raw", "r") + - [("text", ": raw data")] - ), - (None, - common.highlight_key("xml", "x") + - [("text", ": XML")] - ), + (None, + common.highlight_key("automatic", "a") + + [("text", ": automatic detection")] + ), + (None, + common.highlight_key("hex", "e") + + [("text", ": Hex")] + ), + (None, + common.highlight_key("html", "h") + + [("text", ": HTML")] + ), + (None, + common.highlight_key("image", "i") + + [("text", ": Image")] + ), + (None, + common.highlight_key("javascript", "j") + + [("text", ": JavaScript")] + ), + (None, + common.highlight_key("json", "s") + + [("text", ": JSON")] + ), + (None, + common.highlight_key("urlencoded", "u") + + [("text", ": URL-encoded data")] + ), + (None, + common.highlight_key("raw", "r") + + [("text", ": raw data")] + ), + (None, + common.highlight_key("xml", "x") + + [("text", ": XML")] + ), ("M", "change default body display mode"), ("p", "previous flow"), + ("P", "copy response(content/headers) to clipboard"), ("r", "replay request"), ("V", "revert changes to request"), ("v", "view body in external viewer"), ("w", "save all flows matching current limit"), ("W", "save this flow"), ("x", "delete body"), - ("X", "view flow details"), ("z", "encode/decode a request/response"), - ("tab", "toggle request/response view"), + ("tab", "next tab"), + ("h, l", "previous tab, next tab"), ("space", "next flow"), ("|", "run script on this flow"), - ("/", "search in response body (case sensitive)"), + ("/", "search (case sensitive)"), ("n", "repeat search forward"), ("N", "repeat search backwards"), ] @@ -87,417 +91,171 @@ footer = [ class FlowViewHeader(urwid.WidgetWrap): def __init__(self, master, f): self.master, self.flow = master, f - self._w = common.format_flow(f, False, extended=True, padding=0, hostheader=self.master.showhost) - - def refresh_flow(self, f): - if f == self.flow: - self._w = common.format_flow(f, False, extended=True, padding=0, hostheader=self.master.showhost) - + self._w = common.format_flow( + f, + False, + extended=True, + padding=0, + hostheader=self.master.showhost + ) + signals.flow_change.connect(self.sig_flow_change) + + def sig_flow_change(self, sender, flow): + if flow == self.flow: + self._w = common.format_flow( + flow, + False, + extended=True, + padding=0, + hostheader=self.master.showhost + ) -class CallbackCache: - @utils.LRUCache(200) - def _callback(self, method, *args, **kwargs): - return getattr(self.obj, method)(*args, **kwargs) - def callback(self, obj, method, *args, **kwargs): - # obj varies! - self.obj = obj - return self._callback(method, *args, **kwargs) -cache = CallbackCache() +cache = utils.LRUCache(200) +TAB_REQ = 0 +TAB_RESP = 1 -class FlowView(urwid.WidgetWrap): - REQ = 0 - RESP = 1 +class FlowView(tabs.Tabs): highlight_color = "focusfield" - def __init__(self, master, state, flow): + def __init__(self, master, state, flow, tab_offset): self.master, self.state, self.flow = master, state, flow + tabs.Tabs.__init__(self, + [ + (self.tab_request, self.view_request), + (self.tab_response, self.view_response), + (self.tab_details, self.view_details), + ], + tab_offset + ) + self.show() self.last_displayed_body = None - if self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE: - self.view_response() - else: - self.view_request() + signals.flow_change.connect(self.sig_flow_change) - def _cached_content_view(self, viewmode, hdrItems, content, limit, is_request): - return contentview.get_content_view(viewmode, hdrItems, content, limit, self.master.add_event, is_request) + def tab_request(self): + if self.flow.intercepted and not self.flow.reply.acked and not self.flow.response: + return "Request intercepted" + else: + return "Request" - def content_view(self, viewmode, conn): - full = self.state.get_flow_setting( - self.flow, - (self.state.view_flow_mode, "fullcontents"), - False - ) - if full: - limit = sys.maxint + def tab_response(self): + if self.flow.intercepted and not self.flow.reply.acked and self.flow.response: + return "Response intercepted" else: - limit = contentview.VIEW_CUTOFF - description, text_objects = cache.callback( - self, "_cached_content_view", - viewmode, - tuple(tuple(i) for i in conn.headers.lst), - conn.content, - limit, - isinstance(conn, HTTPRequest) - ) - return (description, text_objects) + return "Response" - def cont_view_handle_missing(self, conn, viewmode): - if conn.content == CONTENT_MISSING: - msg, body = "", [urwid.Text([("error", "[content missing]")])] - else: - msg, body = self.content_view(viewmode, conn) + def tab_details(self): + return "Detail" + + def view_request(self): + return self.conn_text(self.flow.request) + + def view_response(self): + return self.conn_text(self.flow.response) + + def view_details(self): + return flowdetailview.flowdetails(self.state, self.flow) + def sig_flow_change(self, sender, flow): + if flow == self.flow: + self.show() + + def content_view(self, viewmode, conn): + if conn.content == CONTENT_MISSING: + msg, body = "", [urwid.Text([("error", "[content missing]")])] return (msg, body) + else: + full = self.state.get_flow_setting( + self.flow, + (self.tab_offset, "fullcontents"), + False + ) + if full: + limit = sys.maxsize + else: + limit = contentview.VIEW_CUTOFF + description, text_objects = cache.get( + contentview.get_content_view, + viewmode, + tuple(tuple(i) for i in conn.headers.lst), + conn.content, + limit, + self.master.add_event, + isinstance(conn, HTTPRequest) + ) + return (description, text_objects) - def viewmode_get(self, override): + def viewmode_get(self): + override = self.state.get_flow_setting( + self.flow, + (self.tab_offset, "prettyview") + ) return self.state.default_body_view if override is None else override - def override_get(self): - return self.state.get_flow_setting(self.flow, - (self.state.view_flow_mode, "prettyview")) - - def conn_text_raw(self, conn): - """ - Based on a request/response, conn, returns the elements for - display. - """ - headers = common.format_keyvals( - [(h+":", v) for (h, v) in conn.headers.lst], + def conn_text(self, conn): + if conn: + txt = common.format_keyvals( + [(h + ":", v) for (h, v) in conn.headers.lst], key = "header", val = "text" ) - override = self.override_get() - viewmode = self.viewmode_get(override) - msg, body = self.cont_view_handle_missing(conn, viewmode) - return headers, msg, body - - def conn_text_merge(self, headers, msg, body): - """ - Grabs what is returned by conn_text_raw and merges them all - toghether, mainly used by conn_text and search - """ - override = self.override_get() - viewmode = self.viewmode_get(override) - - cols = [urwid.Text( - [ - ("heading", msg), - ] - ) - ] + viewmode = self.viewmode_get() + msg, body = self.content_view(viewmode, conn) - if override is not None: - cols.append(urwid.Text([ + cols = [ + urwid.Text( + [ + ("heading", msg), + ] + ) + ] + cols.append( + urwid.Text( + [ " ", ('heading', "["), ('heading_key', "m"), - ('heading', (":%s]"%viewmode.name)), + ('heading', (":%s]" % viewmode.name)), ], align="right" ) ) + title = urwid.AttrWrap(urwid.Columns(cols), "heading") - title = urwid.AttrWrap(urwid.Columns(cols), "heading") - headers.append(title) - headers.extend(body) - - return headers - - def conn_text(self, conn): - """ - Same as conn_text_raw, but returns result wrapped in a listbox ready for usage. - """ - headers, msg, body = self.conn_text_raw(conn) - merged = self.conn_text_merge(headers, msg, body) - return urwid.ListBox(merged) - - def _tab(self, content, attr): - p = urwid.Text(content) - p = urwid.Padding(p, align="left", width=("relative", 100)) - p = urwid.AttrWrap(p, attr) - return p - - def wrap_body(self, active, body): - parts = [] - - if self.flow.intercepted and not self.flow.reply.acked and not self.flow.response: - qt = "Request intercepted" - else: - qt = "Request" - if active == common.VIEW_FLOW_REQUEST: - parts.append(self._tab(qt, "heading")) + txt.append(title) + txt.extend(body) else: - parts.append(self._tab(qt, "heading_inactive")) - - if self.flow.intercepted and not self.flow.reply.acked and self.flow.response: - st = "Response intercepted" - else: - st = "Response" - if active == common.VIEW_FLOW_RESPONSE: - parts.append(self._tab(st, "heading")) - else: - parts.append(self._tab(st, "heading_inactive")) - - h = urwid.Columns(parts) - f = urwid.Frame( - body, - header=h + txt = [ + urwid.Text(""), + urwid.Text( + [ + ("highlight", "No response. Press "), + ("key", "e"), + ("highlight", " and edit any aspect to add one."), + ] ) - return f - - def search_wrapped_around(self, last_find_line, last_search_index, backwards): - """ - returns true if search wrapped around the bottom. - """ - - current_find_line = self.state.get_flow_setting(self.flow, - "last_find_line") - current_search_index = self.state.get_flow_setting(self.flow, - "last_search_index") - - if not backwards: - message = "search hit BOTTOM, continuing at TOP" - if current_find_line <= last_find_line: - return True, message - elif current_find_line == last_find_line: - if current_search_index <= last_search_index: - return True, message - else: - message = "search hit TOP, continuing at BOTTOM" - if current_find_line >= last_find_line: - return True, message - elif current_find_line == last_find_line: - if current_search_index >= last_search_index: - return True, message - - return False, "" - - def search_again(self, backwards=False): - """ - runs the previous search again, forwards or backwards. - """ - last_search_string = self.state.get_flow_setting(self.flow, "last_search_string") - if last_search_string: - message = self.search(last_search_string, backwards) - if message: - self.master.statusbar.message(message) - else: - message = "no previous searches have been made" - self.master.statusbar.message(message) - - return message - - def search(self, search_string, backwards=False): - """ - similar to view_response or view_request, but instead of just - displaying the conn, it highlights a word that the user is - searching for and handles all the logic surrounding that. - """ - - if not search_string: - search_string = self.state.get_flow_setting(self.flow, - "last_search_string") - if not search_string: - return - - if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - text = self.flow.request - const = common.VIEW_FLOW_REQUEST - else: - text = self.flow.response - const = common.VIEW_FLOW_RESPONSE - if not self.flow.response: - return "no response to search in" - - last_find_line = self.state.get_flow_setting(self.flow, - "last_find_line") - last_search_index = self.state.get_flow_setting(self.flow, - "last_search_index") - - # generate the body, highlight the words and get focus - headers, msg, body = self.conn_text_raw(text) - try: - body, focus_position = self.search_highlight_text(body, search_string, backwards=backwards) - except SearchError: - return "Search not supported in this view." - - if focus_position == None: - # no results found. - return "no matches for '%s'" % search_string - - # UI stuff. - merged = self.conn_text_merge(headers, msg, body) - list_box = urwid.ListBox(merged) - list_box.set_focus(focus_position + 2) - self._w = self.wrap_body(const, list_box) - self.master.statusbar.redraw() - - self.last_displayed_body = list_box - - wrapped, wrapped_message = self.search_wrapped_around(last_find_line, last_search_index, backwards) - - if wrapped: - return wrapped_message - - def search_get_start(self, search_string): - start_line = 0 - start_index = 0 - last_search_string = self.state.get_flow_setting(self.flow, "last_search_string") - if search_string == last_search_string: - start_line = self.state.get_flow_setting(self.flow, "last_find_line") - start_index = self.state.get_flow_setting(self.flow, - "last_search_index") - - if start_index == None: - start_index = 0 - else: - start_index += len(search_string) - - if start_line == None: - start_line = 0 - - else: - self.state.add_flow_setting(self.flow, "last_search_string", - search_string) - - return (start_line, start_index) - - def search_get_range(self, len_text_objects, start_line, backwards): - if not backwards: - loop_range = xrange(start_line, len_text_objects) - else: - loop_range = xrange(start_line, -1, -1) - - return loop_range - - def search_find(self, text, search_string, start_index, backwards): - if backwards == False: - find_index = text.find(search_string, start_index) - else: - if start_index != 0: - start_index -= len(search_string) - else: - start_index = None - - find_index = text.rfind(search_string, 0, start_index) - - return find_index - - def search_highlight_text(self, text_objects, search_string, looping = False, backwards = False): - start_line, start_index = self.search_get_start(search_string) - i = start_line - - found = False - text_objects = copy.deepcopy(text_objects) - loop_range = self.search_get_range(len(text_objects), start_line, backwards) - for i in loop_range: - text_object = text_objects[i] - - try: - text, style = text_object.get_text() - except AttributeError: - raise SearchError() - - if i != start_line: - start_index = 0 - - find_index = self.search_find(text, search_string, start_index, backwards) - - if find_index != -1: - new_text = self.search_highlight_object(text, find_index, search_string) - text_objects[i] = new_text - - found = True - self.state.add_flow_setting(self.flow, "last_search_index", - find_index) - self.state.add_flow_setting(self.flow, "last_find_line", i) - - break - - # handle search WRAP - if found: - focus_pos = i - else : - if looping: - focus_pos = None - else: - if not backwards: - self.state.add_flow_setting(self.flow, "last_search_index", 0) - self.state.add_flow_setting(self.flow, "last_find_line", 0) - else: - self.state.add_flow_setting(self.flow, "last_search_index", None) - self.state.add_flow_setting(self.flow, "last_find_line", len(text_objects) - 1) - - text_objects, focus_pos = self.search_highlight_text(text_objects, - search_string, looping=True, backwards=backwards) - - return text_objects, focus_pos - - def search_highlight_object(self, text_object, find_index, search_string): - """ - just a little abstraction - """ - before = text_object[:find_index] - after = text_object[find_index+len(search_string):] - - new_text = urwid.Text( - [ - before, - (self.highlight_color, search_string), - after, ] - ) - - return new_text - - def view_request(self): - self.state.view_flow_mode = common.VIEW_FLOW_REQUEST - body = self.conn_text(self.flow.request) - self._w = self.wrap_body(common.VIEW_FLOW_REQUEST, body) - self.master.statusbar.redraw() - - def view_response(self): - self.state.view_flow_mode = common.VIEW_FLOW_RESPONSE - if self.flow.response: - body = self.conn_text(self.flow.response) - else: - body = urwid.ListBox( - [ - urwid.Text(""), - urwid.Text( - [ - ("highlight", "No response. Press "), - ("key", "e"), - ("highlight", " and edit any aspect to add one."), - ] - ) - ] - ) - self._w = self.wrap_body(common.VIEW_FLOW_RESPONSE, body) - self.master.statusbar.redraw() - - def refresh_flow(self, c=None): - if c == self.flow: - if self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE and self.flow.response: - self.view_response() - else: - self.view_request() + return searchable.Searchable(self.state, txt) def set_method_raw(self, m): if m: self.flow.request.method = m - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def edit_method(self, m): if m == "e": - self.master.prompt_edit("Method", self.flow.request.method, self.set_method_raw) + signals.status_prompt.send( + prompt = "Method", + text = self.flow.request.method, + callback = self.set_method_raw + ) else: for i in common.METHOD_OPTIONS: if i[1] == m: self.flow.request.method = i[0].upper() - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def set_url(self, url): request = self.flow.request @@ -505,7 +263,7 @@ class FlowView(urwid.WidgetWrap): request.url = str(url) except ValueError: return "Invalid URL." - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def set_resp_code(self, code): response = self.flow.response @@ -514,87 +272,161 @@ class FlowView(urwid.WidgetWrap): except ValueError: return None import BaseHTTPServer - if BaseHTTPServer.BaseHTTPRequestHandler.responses.has_key(int(code)): - response.msg = BaseHTTPServer.BaseHTTPRequestHandler.responses[int(code)][0] - self.master.refresh_flow(self.flow) + if int(code) in BaseHTTPServer.BaseHTTPRequestHandler.responses: + response.msg = BaseHTTPServer.BaseHTTPRequestHandler.responses[ + int(code)][0] + signals.flow_change.send(self, flow = self.flow) def set_resp_msg(self, msg): response = self.flow.response response.msg = msg - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def set_headers(self, lst, conn): - conn.headers = flow.ODictCaseless(lst) + conn.headers = odict.ODictCaseless(lst) + signals.flow_change.send(self, flow = self.flow) def set_query(self, lst, conn): - conn.set_query(flow.ODict(lst)) + conn.set_query(odict.ODict(lst)) + signals.flow_change.send(self, flow = self.flow) def set_path_components(self, lst, conn): - conn.set_path_components([i[0] for i in lst]) + conn.set_path_components(lst) + signals.flow_change.send(self, flow = self.flow) def set_form(self, lst, conn): - conn.set_form_urlencoded(flow.ODict(lst)) + conn.set_form_urlencoded(odict.ODict(lst)) + signals.flow_change.send(self, flow = self.flow) def edit_form(self, conn): self.master.view_grideditor( - grideditor.URLEncodedFormEditor(self.master, conn.get_form_urlencoded().lst, self.set_form, conn) + grideditor.URLEncodedFormEditor( + self.master, + conn.get_form_urlencoded().lst, + self.set_form, + conn + ) ) def edit_form_confirm(self, key, conn): if key == "y": self.edit_form(conn) + def set_cookies(self, lst, conn): + od = odict.ODict(lst) + conn.set_cookies(od) + signals.flow_change.send(self, flow = self.flow) + + def set_setcookies(self, data, conn): + conn.set_cookies(data) + signals.flow_change.send(self, flow = self.flow) + def edit(self, part): - if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + if self.tab_offset == TAB_REQ: message = self.flow.request else: if not self.flow.response: self.flow.response = HTTPResponse( self.flow.request.httpversion, - 200, "OK", flow.ODictCaseless(), "" + 200, "OK", odict.ODictCaseless(), "" ) self.flow.response.reply = controller.DummyReply() message = self.flow.response self.flow.backup() + if message == self.flow.request and part == "c": + self.master.view_grideditor( + grideditor.CookieEditor( + self.master, + message.get_cookies().lst, + self.set_cookies, + message + ) + ) + if message == self.flow.response and part == "c": + self.master.view_grideditor( + grideditor.SetCookieEditor( + self.master, + message.get_cookies(), + self.set_setcookies, + message + ) + ) if part == "r": with decoded(message): - # Fix an issue caused by some editors when editing a request/response body. - # Many editors make it hard to save a file without a terminating newline on the last - # line. When editing message bodies, this can cause problems. For now, I just - # strip the newlines off the end of the body when we return from an editor. + # Fix an issue caused by some editors when editing a + # request/response body. Many editors make it hard to save a + # file without a terminating newline on the last line. When + # editing message bodies, this can cause problems. For now, I + # just strip the newlines off the end of the body when we return + # from an editor. c = self.master.spawn_editor(message.content or "") message.content = c.rstrip("\n") elif part == "f": if not message.get_form_urlencoded() and message.content: - self.master.prompt_onekey( - "Existing body is not a URL-encoded form. Clear and edit?", - [ + signals.status_prompt_onekey.send( + prompt = "Existing body is not a URL-encoded form. Clear and edit?", + keys = [ ("yes", "y"), ("no", "n"), ], - self.edit_form_confirm, - message + callback = self.edit_form_confirm, + args = (message,) ) else: self.edit_form(message) elif part == "h": - self.master.view_grideditor(grideditor.HeaderEditor(self.master, message.headers.lst, self.set_headers, message)) + self.master.view_grideditor( + grideditor.HeaderEditor( + self.master, + message.headers.lst, + self.set_headers, + message + ) + ) elif part == "p": p = message.get_path_components() - p = [[i] for i in p] - self.master.view_grideditor(grideditor.PathEditor(self.master, p, self.set_path_components, message)) + self.master.view_grideditor( + grideditor.PathEditor( + self.master, + p, + self.set_path_components, + message + ) + ) elif part == "q": - self.master.view_grideditor(grideditor.QueryEditor(self.master, message.get_query().lst, self.set_query, message)) - elif part == "u" and self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - self.master.prompt_edit("URL", message.url, self.set_url) - elif part == "m" and self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - self.master.prompt_onekey("Method", common.METHOD_OPTIONS, self.edit_method) - elif part == "c" and self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE: - self.master.prompt_edit("Code", str(message.code), self.set_resp_code) - elif part == "m" and self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE: - self.master.prompt_edit("Message", message.msg, self.set_resp_msg) - self.master.refresh_flow(self.flow) + self.master.view_grideditor( + grideditor.QueryEditor( + self.master, + message.get_query().lst, + self.set_query, message + ) + ) + elif part == "u": + signals.status_prompt.send( + prompt = "URL", + text = message.url, + callback = self.set_url + ) + elif part == "m": + signals.status_prompt_onekey.send( + prompt = "Method", + keys = common.METHOD_OPTIONS, + callback = self.edit_method + ) + elif part == "o": + signals.status_prompt.send( + prompt = "Code", + text = str(message.code), + callback = self.set_resp_code + ) + elif part == "m": + signals.status_prompt.send( + prompt = "Message", + text = message.msg, + callback = self.set_resp_msg + ) + signals.flow_change.send(self, flow = self.flow) def _view_nextprev_flow(self, np, flow): try: @@ -606,9 +438,10 @@ class FlowView(urwid.WidgetWrap): else: new_flow, new_idx = self.state.get_prev(idx) if new_flow is None: - self.master.statusbar.message("No more flows!") - return - self.master.view_flow(new_flow) + signals.status_message.send(message="No more flows!") + else: + signals.pop_view_state.send(self) + self.master.view_flow(new_flow, self.tab_offset) def view_next_flow(self, flow): return self._view_nextprev_flow("next", flow) @@ -619,42 +452,38 @@ class FlowView(urwid.WidgetWrap): def change_this_display_mode(self, t): self.state.add_flow_setting( self.flow, - (self.state.view_flow_mode, "prettyview"), + (self.tab_offset, "prettyview"), contentview.get_by_shortcut(t) ) - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def delete_body(self, t): if t == "m": val = CONTENT_MISSING else: val = None - if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + if self.tab_offset == TAB_REQ: self.flow.request.content = val else: self.flow.response.content = val - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def keypress(self, size, key): + key = super(self.__class__, self).keypress(size, key) + if key == " ": self.view_next_flow(self.flow) return key = common.shortcuts(key) - if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + if self.tab_offset == TAB_REQ: conn = self.flow.request - else: + elif self.tab_offset == TAB_RESP: conn = self.flow.response + else: + conn = None - if key == "q": - self.master.view_flowlist() - key = None - elif key == "tab": - if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - self.view_response() - else: - self.view_request() - elif key in ("up", "down", "page up", "page down"): + if key in ("up", "down", "page up", "page down"): # Why doesn't this just work?? self._w.keypress(size, key) elif key == "a": @@ -663,15 +492,10 @@ class FlowView(urwid.WidgetWrap): elif key == "A": self.master.accept_all() self.master.view_flow(self.flow) - elif key == "b": - if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - common.ask_save_body("q", self.master, self.state, self.flow) - else: - common.ask_save_body("s", self.master, self.state, self.flow) elif key == "d": if self.state.flow_count() == 1: self.master.view_flowlist() - elif self.state.view.index(self.flow) == len(self.state.view)-1: + elif self.state.view.index(self.flow) == len(self.state.view) - 1: self.view_prev_flow(self.flow) else: self.view_next_flow(self.flow) @@ -681,134 +505,143 @@ class FlowView(urwid.WidgetWrap): elif key == "D": f = self.master.duplicate_flow(self.flow) self.master.view_flow(f) - self.master.statusbar.message("Duplicated.") - elif key == "e": - if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - self.master.prompt_onekey( - "Edit request", - ( - ("query", "q"), - ("path", "p"), - ("url", "u"), - ("header", "h"), - ("form", "f"), - ("raw body", "r"), - ("method", "m"), - ), - self.edit - ) - else: - self.master.prompt_onekey( - "Edit response", - ( - ("code", "c"), - ("message", "m"), - ("header", "h"), - ("raw body", "r"), - ), - self.edit - ) - key = None - elif key == "f": - self.master.statusbar.message("Loading all body data...") - self.state.add_flow_setting( - self.flow, - (self.state.view_flow_mode, "fullcontents"), - True - ) - self.master.refresh_flow(self.flow) - self.master.statusbar.message("") - elif key == "g": - if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - scope = "q" - else: - scope = "s" - common.ask_copy_part(scope, self.flow, self.master, self.state) - elif key == "m": - p = list(contentview.view_prompts) - p.insert(0, ("Clear", "C")) - self.master.prompt_onekey( - "Display mode", - p, - self.change_this_display_mode - ) - key = None + signals.status_message.send(message="Duplicated.") elif key == "p": self.view_prev_flow(self.flow) elif key == "r": r = self.master.replay_request(self.flow) if r: - self.master.statusbar.message(r) - self.master.refresh_flow(self.flow) + signals.status_message.send(message=r) + signals.flow_change.send(self, flow = self.flow) elif key == "V": if not self.flow.modified(): - self.master.statusbar.message("Flow not modified.") + signals.status_message.send(message="Flow not modified.") return self.state.revert(self.flow) - self.master.refresh_flow(self.flow) - self.master.statusbar.message("Reverted.") + signals.flow_change.send(self, flow = self.flow) + signals.status_message.send(message="Reverted.") elif key == "W": - self.master.path_prompt( - "Save this flow: ", - self.state.last_saveload, - self.master.save_one_flow, - self.flow + signals.status_prompt_path.send( + prompt = "Save this flow", + callback = self.master.save_one_flow, + args = (self.flow,) ) - elif key == "v": - if conn and conn.content: - t = conn.headers["content-type"] or [None] - t = t[0] - if os.environ.has_key("EDITOR") or os.environ.has_key("PAGER"): - self.master.spawn_external_viewer(conn.content, t) - else: - self.master.statusbar.message("Error! Set $EDITOR or $PAGER.") elif key == "|": - self.master.path_prompt( - "Send flow to script: ", self.state.last_script, - self.master.run_script_once, self.flow + signals.status_prompt_path.send( + prompt = "Send flow to script", + callback = self.master.run_script_once, + args = (self.flow,) ) - elif key == "x": - self.master.prompt_onekey( - "Delete body", - ( - ("completely", "c"), - ("mark as missing", "m"), - ), - self.delete_body + + if not conn and key in set(list("befgmxvz")): + signals.status_message.send( + message = "Tab to the request or response", + expire = 1 ) - key = None - elif key == "X": - self.master.view_flowdetails(self.flow) - elif key == "z": - if conn: + elif conn: + if key == "b": + if self.tab_offset == TAB_REQ: + common.ask_save_body( + "q", self.master, self.state, self.flow + ) + else: + common.ask_save_body( + "s", self.master, self.state, self.flow + ) + elif key == "e": + if self.tab_offset == TAB_REQ: + signals.status_prompt_onekey.send( + prompt = "Edit request", + keys = ( + ("cookies", "c"), + ("query", "q"), + ("path", "p"), + ("url", "u"), + ("header", "h"), + ("form", "f"), + ("raw body", "r"), + ("method", "m"), + ), + callback = self.edit + ) + else: + signals.status_prompt_onekey.send( + prompt = "Edit response", + keys = ( + ("cookies", "c"), + ("code", "o"), + ("message", "m"), + ("header", "h"), + ("raw body", "r"), + ), + callback = self.edit + ) + key = None + elif key == "f": + signals.status_message.send(message="Loading all body data...") + self.state.add_flow_setting( + self.flow, + (self.tab_offset, "fullcontents"), + True + ) + signals.flow_change.send(self, flow = self.flow) + signals.status_message.send(message="") + elif key == "P": + if self.tab_offset == TAB_REQ: + scope = "q" + else: + scope = "s" + common.ask_copy_part(scope, self.flow, self.master, self.state) + elif key == "m": + p = list(contentview.view_prompts) + p.insert(0, ("Clear", "C")) + signals.status_prompt_onekey.send( + self, + prompt = "Display mode", + keys = p, + callback = self.change_this_display_mode + ) + key = None + elif key == "x": + signals.status_prompt_onekey.send( + prompt = "Delete body", + keys = ( + ("completely", "c"), + ("mark as missing", "m"), + ), + callback = self.delete_body + ) + key = None + elif key == "v": + if conn.content: + t = conn.headers["content-type"] or [None] + t = t[0] + if "EDITOR" in os.environ or "PAGER" in os.environ: + self.master.spawn_external_viewer(conn.content, t) + else: + signals.status_message.send( + message = "Error! Set $EDITOR or $PAGER." + ) + elif key == "z": self.flow.backup() e = conn.headers.get_first("content-encoding", "identity") if e != "identity": if not conn.decode(): - self.master.statusbar.message("Could not decode - invalid data?") + signals.status_message.send( + message = "Could not decode - invalid data?" + ) else: - self.master.prompt_onekey( - "Select encoding: ", - ( + signals.status_prompt_onekey.send( + prompt = "Select encoding: ", + keys = ( ("gzip", "z"), ("deflate", "d"), ), - self.encode_callback, - conn + callback = self.encode_callback, + args = (conn,) ) - self.master.refresh_flow(self.flow) - elif key == "/": - last_search_string = self.state.get_flow_setting(self.flow, "last_search_string") - search_prompt = "Search body ["+last_search_string+"]: " if last_search_string else "Search body: " - self.master.prompt(search_prompt, - None, - self.search) - elif key == "n": - self.search_again(backwards=False) - elif key == "N": - self.search_again(backwards=True) - else: - return key + signals.flow_change.send(self, flow = self.flow) + return key def encode_callback(self, key, conn): encoding_map = { @@ -816,4 +649,4 @@ class FlowView(urwid.WidgetWrap): "d": "deflate", } conn.encode(encoding_map[key]) - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index fe3df509..b20e54e4 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -5,31 +5,99 @@ import re import os import urwid -from . import common +from . import common, signals from .. import utils, filt, script -from netlib import http_uastrings +from netlib import http_uastrings, http_cookies, odict -footer = [ +FOOTER = [ ('heading_key', "enter"), ":edit ", ('heading_key', "q"), ":back ", ] -footer_editing = [ +FOOTER_EDITING = [ ('heading_key', "esc"), ":stop editing ", ] -class SText(urwid.WidgetWrap): - def __init__(self, txt, focused, error): +class TextColumn: + subeditor = None + + def __init__(self, heading): + self.heading = heading + + def text(self, obj): + return SEscaped(obj or "") + + def blank(self): + return "" + + def keypress(self, key, editor): + if key == "r": + if editor.walker.get_current_value() is not None: + signals.status_prompt_path.send( + self, + prompt = "Read file", + callback = editor.read_file + ) + elif key == "R": + if editor.walker.get_current_value() is not None: + signals.status_prompt_path.send( + editor, + prompt = "Read unescaped file", + callback = editor.read_file, + args = (True,) + ) + elif key == "e": + o = editor.walker.get_current_value() + if o is not None: + n = editor.master.spawn_editor(o.encode("string-escape")) + n = utils.clean_hanging_newline(n) + editor.walker.set_current_value(n, False) + editor.walker._modified() + elif key in ["enter"]: + editor.walker.start_edit() + else: + return key + + +class SubgridColumn: + def __init__(self, heading, subeditor): + self.heading = heading + self.subeditor = subeditor + + def text(self, obj): + p = http_cookies._format_pairs(obj, sep="\n") + return urwid.Text(p) + + def blank(self): + return [] + + def keypress(self, key, editor): + if key in "rRe": + signals.status_message.send( + self, + message = "Press enter to edit this field.", + expire = 1000 + ) + return + elif key in ["enter"]: + editor.master.view_grideditor( + self.subeditor( + editor.master, + editor.walker.get_current_value(), + editor.set_subeditor_value, + editor.walker.focus, + editor.walker.focus_col + ) + ) + else: + return key + + +class SEscaped(urwid.WidgetWrap): + def __init__(self, txt): txt = txt.encode("string-escape") w = urwid.Text(txt, wrap="any") - if focused: - if error: - w = urwid.AttrWrap(w, "focusfield_error") - else: - w = urwid.AttrWrap(w, "focusfield") - elif error: - w = urwid.AttrWrap(w, "field_error") urwid.WidgetWrap.__init__(self, w) def get_text(self): @@ -50,7 +118,7 @@ class SEdit(urwid.WidgetWrap): urwid.WidgetWrap.__init__(self, w) def get_text(self): - return self._w.get_text()[0] + return self._w.get_text()[0].strip() def selectable(self): return True @@ -67,9 +135,15 @@ class GridRow(urwid.WidgetWrap): self.editing = SEdit(v) self.fields.append(self.editing) else: - self.fields.append( - SText(v, True if focused == i else False, i in errors) - ) + w = self.editor.columns[i].text(v) + if focused == i: + if i in errors: + w = urwid.AttrWrap(w, "focusfield_error") + else: + w = urwid.AttrWrap(w, "focusfield") + elif i in errors: + w = urwid.AttrWrap(w, "field_error") + self.fields.append(w) fspecs = self.fields[:] if len(self.fields) > 1: @@ -101,6 +175,7 @@ class GridWalker(urwid.ListWalker): and errors is a set with an entry of each offset in rows that is an error. """ + def __init__(self, lst, editor): self.lst = [(i, set([])) for i in lst] self.editor = editor @@ -125,31 +200,43 @@ class GridWalker(urwid.ListWalker): try: val = val.decode("string-escape") except ValueError: - self.editor.master.statusbar.message( - "Invalid Python-style string encoding.", 1000 + signals.status_message.send( + self, + message = "Invalid Python-style string encoding.", + expire = 1000 ) return errors = self.lst[self.focus][1] emsg = self.editor.is_error(self.focus_col, val) if emsg: - self.editor.master.statusbar.message(emsg, 1000) + signals.status_message.send(message = emsg, expire = 1) errors.add(self.focus_col) else: errors.discard(self.focus_col) - - row = list(self.lst[self.focus][0]) - row[self.focus_col] = val - self.lst[self.focus] = [tuple(row), errors] + self.set_value(val, self.focus, self.focus_col, errors) + + def set_value(self, val, focus, focus_col, errors=None): + if not errors: + errors = set([]) + row = list(self.lst[focus][0]) + row[focus_col] = val + self.lst[focus] = [tuple(row), errors] + self._modified() def delete_focus(self): if self.lst: del self.lst[self.focus] - self.focus = min(len(self.lst)-1, self.focus) + self.focus = min(len(self.lst) - 1, self.focus) self._modified() def _insert(self, pos): self.focus = pos - self.lst.insert(self.focus, [[""]*self.editor.columns, set([])]) + self.lst.insert( + self.focus, + [ + [c.blank() for c in self.editor.columns], set([]) + ] + ) self.focus_col = 0 self.start_edit() @@ -160,16 +247,17 @@ class GridWalker(urwid.ListWalker): return self._insert(min(self.focus + 1, len(self.lst))) def start_edit(self): - if self.lst: + col = self.editor.columns[self.focus_col] + if self.lst and not col.subeditor: self.editing = GridRow( self.focus_col, True, self.editor, self.lst[self.focus] ) - self.editor.master.statusbar.update(footer_editing) + self.editor.master.loop.widget.footer.update(FOOTER_EDITING) self._modified() def stop_edit(self): if self.editing: - self.editor.master.statusbar.update(footer) + self.editor.master.loop.widget.footer.update(FOOTER) self.set_current_value(self.editing.get_edit_value(), False) self.editing = False self._modified() @@ -179,14 +267,14 @@ class GridWalker(urwid.ListWalker): self._modified() def right(self): - self.focus_col = min(self.focus_col + 1, self.editor.columns-1) + self.focus_col = min(self.focus_col + 1, len(self.editor.columns) - 1) self._modified() def tab_next(self): self.stop_edit() - if self.focus_col < self.editor.columns-1: + if self.focus_col < len(self.editor.columns) - 1: self.focus_col += 1 - elif self.focus != len(self.lst)-1: + elif self.focus != len(self.lst) - 1: self.focus_col = 0 self.focus += 1 self._modified() @@ -207,16 +295,17 @@ class GridWalker(urwid.ListWalker): def set_focus(self, focus): self.stop_edit() self.focus = focus + self._modified() def get_next(self, pos): - if pos+1 >= len(self.lst): + if pos + 1 >= len(self.lst): return None, None - return GridRow(None, False, self.editor, self.lst[pos+1]), pos+1 + return GridRow(None, False, self.editor, self.lst[pos + 1]), pos + 1 def get_prev(self, pos): - if pos-1 < 0: + if pos - 1 < 0: return None, None - return GridRow(None, False, self.editor, self.lst[pos-1]), pos-1 + return GridRow(None, False, self.editor, self.lst[pos - 1]), pos - 1 class GridListBox(urwid.ListBox): @@ -231,17 +320,16 @@ FIRST_WIDTH_MIN = 20 class GridEditor(urwid.WidgetWrap): title = None columns = None - headings = None def __init__(self, master, value, callback, *cb_args, **cb_kwargs): - value = copy.deepcopy(value) + value = self.data_in(copy.deepcopy(value)) self.master, self.value, self.callback = master, value, callback self.cb_args, self.cb_kwargs = cb_args, cb_kwargs first_width = 20 if value: for r in value: - assert len(r) == self.columns + assert len(r) == len(self.columns) first_width = max(len(r), first_width) self.first_width = min(first_width, FIRST_WIDTH_MAX) @@ -250,9 +338,9 @@ class GridEditor(urwid.WidgetWrap): title = urwid.AttrWrap(title, "heading") headings = [] - for i, h in enumerate(self.headings): - c = urwid.Text(h) - if i == 0 and len(self.headings) > 1: + for i, col in enumerate(self.columns): + c = urwid.Text(col.heading) + if i == 0 and len(self.columns) > 1: headings.append(("fixed", first_width + 2, c)) else: headings.append(c) @@ -268,7 +356,7 @@ class GridEditor(urwid.WidgetWrap): self.lb, header = urwid.Pile([title, h]) ) - self.master.statusbar.update("") + self.master.loop.widget.footer.update("") self.show_empty_msg() def show_empty_msg(self): @@ -300,9 +388,12 @@ class GridEditor(urwid.WidgetWrap): d = file(p, "rb").read() self.walker.set_current_value(d, unescaped) self.walker._modified() - except IOError, v: + except IOError as v: return str(v) + def set_subeditor_value(self, val, focus, focus_col): + self.walker.set_value(val, focus, focus_col) + def keypress(self, size, key): if self.walker.editing: if key in ["esc"]: @@ -317,13 +408,18 @@ class GridEditor(urwid.WidgetWrap): return None key = common.shortcuts(key) + column = self.columns[self.walker.focus_col] if key in ["q", "esc"]: res = [] for i in self.walker.lst: - if not i[1] and any([x.strip() for x in i[0]]): + if not i[1] and any([x for x in i[0]]): res.append(i[0]) - self.callback(res, *self.cb_args, **self.cb_kwargs) - self.master.pop_view() + self.callback(self.data_out(res), *self.cb_args, **self.cb_kwargs) + signals.pop_view_state.send(self) + elif key == "G": + self.walker.set_focus(0) + elif key == "g": + self.walker.set_focus(len(self.walker.lst) - 1) elif key in ["h", "left"]: self.walker.left() elif key in ["l", "right"]: @@ -336,26 +432,22 @@ class GridEditor(urwid.WidgetWrap): self.walker.insert() elif key == "d": self.walker.delete_focus() - elif key == "r": - if self.walker.get_current_value() is not None: - self.master.path_prompt("Read file: ", "", self.read_file) - elif key == "R": - if self.walker.get_current_value() is not None: - self.master.path_prompt( - "Read unescaped file: ", "", self.read_file, True - ) - elif key == "e": - o = self.walker.get_current_value() - if o is not None: - n = self.master.spawn_editor(o.encode("string-escape")) - n = utils.clean_hanging_newline(n) - self.walker.set_current_value(n, False) - self.walker._modified() - elif key in ["enter"]: - self.walker.start_edit() - elif not self.handle_key(key): + elif column.keypress(key, self) and not self.handle_key(key): return self._w.keypress(size, key) + def data_out(self, data): + """ + Called on raw list data, before data is returned through the + callback. + """ + return data + + def data_in(self, data): + """ + Called to prepare provided data. + """ + return data + def is_error(self, col, val): """ Return False, or a string error message. @@ -373,10 +465,10 @@ class GridEditor(urwid.WidgetWrap): ("a", "add row after cursor"), ("d", "delete row"), ("e", "spawn external editor on current field"), - ("q", "return to flow view"), + ("q", "save changes and exit editor"), ("r", "read value from file"), ("R", "read unescaped value from file"), - ("esc", "return to flow view/exit field edit mode"), + ("esc", "save changes and exit editor"), ("tab", "next field"), ("enter", "edit field"), ] @@ -396,14 +488,18 @@ class GridEditor(urwid.WidgetWrap): class QueryEditor(GridEditor): title = "Editing query" - columns = 2 - headings = ("Key", "Value") + columns = [ + TextColumn("Key"), + TextColumn("Value") + ] class HeaderEditor(GridEditor): title = "Editing headers" - columns = 2 - headings = ("Key", "Value") + columns = [ + TextColumn("Key"), + TextColumn("Value") + ] def make_help(self): h = GridEditor.make_help(self) @@ -431,24 +527,29 @@ class HeaderEditor(GridEditor): def handle_key(self, key): if key == "U": - self.master.prompt_onekey( - "Add User-Agent header:", - [(i[0], i[1]) for i in http_uastrings.UASTRINGS], - self.set_user_agent, + signals.status_prompt_onekey.send( + prompt = "Add User-Agent header:", + keys = [(i[0], i[1]) for i in http_uastrings.UASTRINGS], + callback = self.set_user_agent, ) return True class URLEncodedFormEditor(GridEditor): title = "Editing URL-encoded form" - columns = 2 - headings = ("Key", "Value") + columns = [ + TextColumn("Key"), + TextColumn("Value") + ] class ReplaceEditor(GridEditor): title = "Editing replacement patterns" - columns = 3 - headings = ("Filter", "Regex", "Replacement") + columns = [ + TextColumn("Filter"), + TextColumn("Regex"), + TextColumn("Replacement"), + ] def is_error(self, col, val): if col == 0: @@ -464,8 +565,11 @@ class ReplaceEditor(GridEditor): class SetHeadersEditor(GridEditor): title = "Editing header set patterns" - columns = 3 - headings = ("Filter", "Header", "Value") + columns = [ + TextColumn("Filter"), + TextColumn("Header"), + TextColumn("Value"), + ] def is_error(self, col, val): if col == 0: @@ -500,39 +604,105 @@ class SetHeadersEditor(GridEditor): def handle_key(self, key): if key == "U": - self.master.prompt_onekey( - "Add User-Agent header:", - [(i[0], i[1]) for i in http_uastrings.UASTRINGS], - self.set_user_agent, + signals.status_prompt_onekey.send( + prompt = "Add User-Agent header:", + keys = [(i[0], i[1]) for i in http_uastrings.UASTRINGS], + callback = self.set_user_agent, ) return True class PathEditor(GridEditor): title = "Editing URL path components" - columns = 1 - headings = ("Component",) + columns = [ + TextColumn("Component"), + ] + + def data_in(self, data): + return [[i] for i in data] + + def data_out(self, data): + return [i[0] for i in data] class ScriptEditor(GridEditor): title = "Editing scripts" - columns = 1 - headings = ("Command",) + columns = [ + TextColumn("Command"), + ] def is_error(self, col, val): try: script.Script.parse_command(val) - except script.ScriptError, v: + except script.ScriptError as v: return str(v) class HostPatternEditor(GridEditor): title = "Editing host patterns" - columns = 1 - headings = ("Regex (matched on hostname:port / ip:port)",) + columns = [ + TextColumn("Regex (matched on hostname:port / ip:port)") + ] def is_error(self, col, val): try: re.compile(val, re.IGNORECASE) except re.error as e: return "Invalid regex: %s" % str(e) + + def data_in(self, data): + return [[i] for i in data] + + def data_out(self, data): + return [i[0] for i in data] + + +class CookieEditor(GridEditor): + title = "Editing request Cookie header" + columns = [ + TextColumn("Name"), + TextColumn("Value"), + ] + + +class CookieAttributeEditor(GridEditor): + title = "Editing Set-Cookie attributes" + columns = [ + TextColumn("Name"), + TextColumn("Value"), + ] + + def data_out(self, data): + ret = [] + for i in data: + if not i[1]: + ret.append([i[0], None]) + else: + ret.append(i) + return ret + + +class SetCookieEditor(GridEditor): + title = "Editing response SetCookie header" + columns = [ + TextColumn("Name"), + TextColumn("Value"), + SubgridColumn("Attributes", CookieAttributeEditor), + ] + + def data_in(self, data): + flattened = [] + for k, v in data.items(): + flattened.append([k, v[0], v[1].lst]) + return flattened + + def data_out(self, data): + vals = [] + for i in data: + vals.append( + [ + i[0], + [i[1], odict.ODictCaseless(i[2])] + ] + ) + return odict.ODict(vals) diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py index 6bb49a92..4e81a566 100644 --- a/libmproxy/console/help.py +++ b/libmproxy/console/help.py @@ -2,18 +2,17 @@ from __future__ import absolute_import import urwid -from . import common +from . import common, signals from .. import filt, version footer = [ - ("heading", 'mitmproxy v%s '%version.VERSION), + ("heading", 'mitmproxy v%s ' % version.VERSION), ('heading_key', "q"), ":back ", ] class HelpView(urwid.ListBox): - def __init__(self, master, help_context, state): - self.master, self.state = master, state + def __init__(self, help_context): self.help_context = help_context or [] urwid.ListBox.__init__( self, @@ -29,101 +28,26 @@ class HelpView(urwid.ListBox): keys = [ ("j, k", "down, up"), ("h, l", "left, right (in some contexts)"), + ("g, G", "go to end, beginning"), ("space", "page down"), ("pg up/down", "page up/down"), ("arrows", "up, down, left, right"), ] - text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + text.extend( + common.format_keyvals( + keys, + key="key", + val="text", + indent=4)) text.append(urwid.Text([("head", "\n\nGlobal keys:\n")])) keys = [ ("c", "client replay"), - ("H", "edit global header set patterns"), - ("I", "set ignore pattern"), ("i", "set interception pattern"), - ("M", "change global default display mode"), - (None, - common.highlight_key("automatic", "a") + - [("text", ": automatic detection")] - ), - (None, - common.highlight_key("hex", "e") + - [("text", ": Hex")] - ), - (None, - common.highlight_key("html", "h") + - [("text", ": HTML")] - ), - (None, - common.highlight_key("image", "i") + - [("text", ": Image")] - ), - (None, - common.highlight_key("javascript", "j") + - [("text", ": JavaScript")] - ), - (None, - common.highlight_key("json", "s") + - [("text", ": JSON")] - ), - (None, - common.highlight_key("css", "c") + - [("text", ": CSS")] - ), - (None, - common.highlight_key("urlencoded", "u") + - [("text", ": URL-encoded data")] - ), - (None, - common.highlight_key("raw", "r") + - [("text", ": raw data")] - ), - (None, - common.highlight_key("xml", "x") + - [("text", ": XML")] - ), - (None, - common.highlight_key("wbxml", "w") + - [("text", ": WBXML")] - ), - (None, - common.highlight_key("amf", "f") + - [("text", ": AMF (requires PyAMF)")] - ), - ("o", "toggle options:"), - (None, - common.highlight_key("anticache", "a") + - [("text", ": prevent cached responses")] - ), - (None, - common.highlight_key("anticomp", "c") + - [("text", ": prevent compressed responses")] - ), - (None, - common.highlight_key("showhost", "h") + - [("text", ": use Host header for URL display")] - ), - (None, - common.highlight_key("killextra", "k") + - [("text", ": kill requests not part of server replay")] - ), - (None, - common.highlight_key("norefresh", "n") + - [("text", ": disable server replay response refresh")] - ), - (None, - common.highlight_key("upstream certs", "u") + - [("text", ": sniff cert info from upstream server")] - ), - - ("q", "quit / return to flow list"), + ("o", "options"), + ("q", "quit / return to previous page"), ("Q", "quit without confirm prompt"), - ("R", "edit replacement patterns"), - ("s", "add/remove scripts"), ("S", "server replay"), - ("t", "set sticky cookie expression"), - ("T", "set tcp proxying pattern"), - ("u", "set sticky auth expression"), ] text.extend( common.format_keyvals(keys, key="key", val="text", indent=4) @@ -133,15 +57,15 @@ class HelpView(urwid.ListBox): f = [] for i in filt.filt_unary: f.append( - ("~%s"%i.code, i.help) + ("~%s" % i.code, i.help) ) for i in filt.filt_rex: f.append( - ("~%s regex"%i.code, i.help) + ("~%s regex" % i.code, i.help) ) for i in filt.filt_int: f.append( - ("~%s int"%i.code, i.help) + ("~%s int" % i.code, i.help) ) f.sort() f.extend( @@ -156,7 +80,7 @@ class HelpView(urwid.ListBox): text.append( urwid.Text( - [ + [ "\n", ("text", " Regexes are Python-style.\n"), ("text", " Regexes can be specified as quoted strings.\n"), @@ -164,13 +88,13 @@ class HelpView(urwid.ListBox): ("text", " Expressions with no operators are regex matches against URL.\n"), ("text", " Default binary operator is &.\n"), ("head", "\n Examples:\n"), - ] + ] ) ) examples = [ - ("google\.com", "Url containing \"google.com"), - ("~q ~b test", "Requests where body contains \"test\""), - ("!(~q & ~t \"text/html\")", "Anything but requests with a text/html content type."), + ("google\.com", "Url containing \"google.com"), + ("~q ~b test", "Requests where body contains \"test\""), + ("!(~q & ~t \"text/html\")", "Anything but requests with a text/html content type."), ] text.extend( common.format_keyvals(examples, key="key", val="text", indent=4) @@ -180,11 +104,12 @@ class HelpView(urwid.ListBox): def keypress(self, size, key): key = common.shortcuts(key) if key == "q": - self.master.statusbar = self.state[0] - self.master.body = self.state[1] - self.master.header = self.state[2] - self.master.loop.widget = self.master.make_view() + signals.pop_view_state.send(self) return None elif key == "?": key = None + elif key == "G": + self.set_focus(0) + elif key == "g": + self.set_focus(len(self.body.contents)) return urwid.ListBox.keypress(self, size, key) diff --git a/libmproxy/console/options.py b/libmproxy/console/options.py new file mode 100644 index 00000000..58a4d469 --- /dev/null +++ b/libmproxy/console/options.py @@ -0,0 +1,269 @@ +import urwid + +from . import common, signals, grideditor, contentview +from . import select, palettes + +footer = [ + ('heading_key', "enter/space"), ":toggle ", + ('heading_key', "C"), ":clear all ", +] + + +def _mkhelp(): + text = [] + keys = [ + ("enter/space", "activate option"), + ("C", "clear all options"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + return text +help_context = _mkhelp() + + +class Options(urwid.WidgetWrap): + def __init__(self, master): + self.master = master + self.lb = select.Select( + [ + select.Heading("Traffic Manipulation"), + select.Option( + "Header Set Patterns", + "H", + lambda: master.setheaders.count(), + self.setheaders + ), + select.Option( + "Ignore Patterns", + "I", + lambda: master.server.config.check_ignore, + self.ignorepatterns + ), + select.Option( + "Replacement Patterns", + "R", + lambda: master.replacehooks.count(), + self.replacepatterns + ), + select.Option( + "Scripts", + "S", + lambda: master.scripts, + self.scripts + ), + + select.Heading("Interface"), + select.Option( + "Default Display Mode", + "M", + self.has_default_displaymode, + self.default_displaymode + ), + select.Option( + "Palette", + "P", + lambda: self.master.palette != palettes.DEFAULT, + self.palette + ), + select.Option( + "Show Host", + "w", + lambda: master.showhost, + self.toggle_showhost + ), + + select.Heading("Network"), + select.Option( + "No Upstream Certs", + "U", + lambda: master.server.config.no_upstream_cert, + self.toggle_upstream_cert + ), + select.Option( + "TCP Proxying", + "T", + lambda: master.server.config.check_tcp, + self.tcp_proxy + ), + + select.Heading("Utility"), + select.Option( + "Anti-Cache", + "a", + lambda: master.anticache, + self.toggle_anticache + ), + select.Option( + "Anti-Compression", + "o", + lambda: master.anticomp, + self.toggle_anticomp + ), + select.Option( + "Kill Extra", + "x", + lambda: master.killextra, + self.toggle_killextra + ), + select.Option( + "No Refresh", + "f", + lambda: not master.refresh_server_playback, + self.toggle_refresh_server_playback + ), + select.Option( + "Sticky Auth", + "A", + lambda: master.stickyauth_txt, + self.sticky_auth + ), + select.Option( + "Sticky Cookies", + "t", + lambda: master.stickycookie_txt, + self.sticky_cookie + ), + ] + ) + title = urwid.Text("Options") + title = urwid.Padding(title, align="left", width=("relative", 100)) + title = urwid.AttrWrap(title, "heading") + self._w = urwid.Frame( + self.lb, + header = title + ) + self.master.loop.widget.footer.update("") + signals.update_settings.connect(self.sig_update_settings) + + def sig_update_settings(self, sender): + self.lb.walker._modified() + + def keypress(self, size, key): + if key == "C": + self.clearall() + return None + return super(self.__class__, self).keypress(size, key) + + def clearall(self): + self.master.anticache = False + self.master.anticomp = False + self.master.killextra = False + self.master.showhost = False + self.master.refresh_server_playback = True + self.master.server.config.no_upstream_cert = False + self.master.setheaders.clear() + self.master.replacehooks.clear() + self.master.set_ignore_filter([]) + self.master.set_tcp_filter([]) + self.master.scripts = [] + self.master.set_stickyauth(None) + self.master.set_stickycookie(None) + self.master.state.default_body_view = contentview.get("Auto") + + signals.update_settings.send(self) + signals.status_message.send( + message = "All select.Options cleared", + expire = 1 + ) + + def toggle_anticache(self): + self.master.anticache = not self.master.anticache + + def toggle_anticomp(self): + self.master.anticomp = not self.master.anticomp + + def toggle_killextra(self): + self.master.killextra = not self.master.killextra + + def toggle_showhost(self): + self.master.showhost = not self.master.showhost + + def toggle_refresh_server_playback(self): + self.master.refresh_server_playback = not self.master.refresh_server_playback + + def toggle_upstream_cert(self): + self.master.server.config.no_upstream_cert = not self.master.server.config.no_upstream_cert + signals.update_settings.send(self) + + def setheaders(self): + def _set(*args, **kwargs): + self.master.setheaders.set(*args, **kwargs) + signals.update_settings.send(self) + self.master.view_grideditor( + grideditor.SetHeadersEditor( + self.master, + self.master.setheaders.get_specs(), + _set + ) + ) + + def ignorepatterns(self): + def _set(ignore): + self.master.set_ignore_filter(ignore) + signals.update_settings.send(self) + self.master.view_grideditor( + grideditor.HostPatternEditor( + self.master, + self.master.get_ignore_filter(), + _set + ) + ) + + def replacepatterns(self): + def _set(*args, **kwargs): + self.master.replacehooks.set(*args, **kwargs) + signals.update_settings.send(self) + self.master.view_grideditor( + grideditor.ReplaceEditor( + self.master, + self.master.replacehooks.get_specs(), + _set + ) + ) + + def scripts(self): + self.master.view_grideditor( + grideditor.ScriptEditor( + self.master, + [[i.command] for i in self.master.scripts], + self.master.edit_scripts + ) + ) + + def default_displaymode(self): + signals.status_prompt_onekey.send( + prompt = "Global default display mode", + keys = contentview.view_prompts, + callback = self.master.change_default_display_mode + ) + + def has_default_displaymode(self): + return self.master.state.default_body_view.name != "Auto" + + def tcp_proxy(self): + def _set(tcp): + self.master.set_tcp_filter(tcp) + signals.update_settings.send(self) + self.master.view_grideditor( + grideditor.HostPatternEditor( + self.master, + self.master.get_tcp_filter(), + _set + ) + ) + + def sticky_auth(self): + signals.status_prompt.send( + prompt = "Sticky auth filter", + text = self.master.stickyauth_txt, + callback = self.master.set_stickyauth + ) + + def sticky_cookie(self): + signals.status_prompt.send( + prompt = "Sticky cookie filter", + text = self.master.stickycookie_txt, + callback = self.master.set_stickycookie + ) + + def palette(self): + self.master.view_palette_picker() diff --git a/libmproxy/console/palettepicker.py b/libmproxy/console/palettepicker.py new file mode 100644 index 00000000..7e2c10cd --- /dev/null +++ b/libmproxy/console/palettepicker.py @@ -0,0 +1,81 @@ +import urwid + +from . import select, common, palettes, signals + +footer = [ + ('heading_key', "enter/space"), ":select", +] + + +def _mkhelp(): + text = [] + keys = [ + ("enter/space", "select"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + return text +help_context = _mkhelp() + + +class PalettePicker(urwid.WidgetWrap): + def __init__(self, master): + self.master = master + low, high = [], [] + for k, v in palettes.palettes.items(): + if v.high: + high.append(k) + else: + low.append(k) + high.sort() + low.sort() + + options = [ + select.Heading("High Colour") + ] + + def mkopt(name): + return select.Option( + i, + None, + lambda: self.master.palette == name, + lambda: self.select(name) + ) + + for i in high: + options.append(mkopt(i)) + options.append(select.Heading("Low Colour")) + for i in low: + options.append(mkopt(i)) + + options.extend( + [ + select.Heading("Options"), + select.Option( + "Transparent", + "T", + lambda: master.palette_transparent, + self.toggle_palette_transparent + ) + ] + ) + + self.lb = select.Select(options) + title = urwid.Text("Palettes") + title = urwid.Padding(title, align="left", width=("relative", 100)) + title = urwid.AttrWrap(title, "heading") + self._w = urwid.Frame( + self.lb, + header = title + ) + signals.update_settings.connect(self.sig_update_settings) + + def sig_update_settings(self, sender): + self.lb.walker._modified() + + def select(self, name): + self.master.set_palette(name) + + def toggle_palette_transparent(self): + self.master.palette_transparent = not self.master.palette_transparent + self.master.set_palette(self.master.palette) + signals.update_settings.send(self) diff --git a/libmproxy/console/palettes.py b/libmproxy/console/palettes.py index cfb2702c..ea3d1b62 100644 --- a/libmproxy/console/palettes.py +++ b/libmproxy/console/palettes.py @@ -1,4 +1,3 @@ - # Low-color themes should ONLY use the standard foreground and background # colours listed here: # @@ -6,9 +5,9 @@ # - class Palette: _fields = [ + 'background', 'title', # Status bar & heading @@ -17,6 +16,10 @@ class Palette: # Help 'key', 'head', 'text', + # Options + 'option_selected', 'option_active', 'option_active_selected', + 'option_selected_key', + # List and Connections 'method', 'focus', 'code_200', 'code_300', 'code_400', 'code_500', 'code_other', @@ -31,15 +34,33 @@ class Palette: ] high = None - def palette(self): + def palette(self, transparent): l = [] + highback, lowback = None, None + if not transparent: + if self.high and self.high.get("background"): + highback = self.high["background"][1] + lowback = self.low["background"][1] + for i in self._fields: - v = [i] - v.extend(self.low[i]) - if self.high and i in self.high: - v.append(None) - v.extend(self.high[i]) - l.append(tuple(v)) + if transparent and i == "background": + l.append(["background", "default", "default"]) + else: + v = [i] + low = list(self.low[i]) + if lowback and low[1] == "default": + low[1] = lowback + v.extend(low) + if self.high and i in self.high: + v.append(None) + high = list(self.high[i]) + if highback and high[1] == "default": + high[1] = highback + v.extend(high) + elif highback and self.low[i][1] == "default": + high = [None, low[0], highback] + v.extend(high) + l.append(tuple(v)) return l @@ -48,18 +69,25 @@ class LowDark(Palette): Low-color dark background """ low = dict( + background = ('white', 'black'), title = ('white,bold', 'default'), # Status bar & heading - heading = ('light gray', 'dark blue'), + heading = ('white', 'dark blue'), heading_key = ('light cyan', 'dark blue'), - heading_inactive = ('white', 'dark gray'), + heading_inactive = ('dark gray', 'light gray'), # Help key = ('light cyan', 'default'), head = ('white,bold', 'default'), text = ('light gray', 'default'), + # Options + option_selected = ('black', 'light gray'), + option_selected_key = ('light cyan', 'light gray'), + option_active = ('light red', 'default'), + option_active_selected = ('light red', 'light gray'), + # List and Connections method = ('dark cyan', 'default'), focus = ('yellow', 'default'), @@ -92,6 +120,10 @@ class Dark(LowDark): high = dict( heading_inactive = ('g58', 'g11'), intercept = ('#f60', 'default'), + + option_selected = ('g85', 'g45'), + option_selected_key = ('light cyan', 'g50'), + option_active_selected = ('light red', 'g50'), ) @@ -100,18 +132,25 @@ class LowLight(Palette): Low-color light background """ low = dict( - title = ('dark magenta,bold', 'light blue'), + background = ('black', 'white'), + title = ('dark magenta', 'default'), # Status bar & heading - heading = ('light gray', 'dark blue'), - heading_key = ('light cyan', 'dark blue'), + heading = ('white', 'black'), + heading_key = ('dark blue', 'black'), heading_inactive = ('black', 'light gray'), # Help - key = ('dark blue,bold', 'default'), - head = ('black,bold', 'default'), + key = ('dark blue', 'default'), + head = ('black', 'default'), text = ('dark gray', 'default'), + # Options + option_selected = ('black', 'light gray'), + option_selected_key = ('dark blue', 'light gray'), + option_active = ('light red', 'default'), + option_active_selected = ('light red', 'light gray'), + # List and Connections method = ('dark cyan', 'default'), focus = ('black', 'default'), @@ -142,10 +181,15 @@ class LowLight(Palette): class Light(LowLight): high = dict( + background = ('black', 'g100'), heading = ('g99', '#08f'), heading_key = ('#0ff,bold', '#08f'), heading_inactive = ('g35', 'g85'), replay = ('#0a0,bold', 'default'), + + option_selected = ('black', 'g85'), + option_selected_key = ('dark blue', 'g85'), + option_active_selected = ('light red', 'g85'), ) @@ -155,10 +199,10 @@ sol_base03 = "h234" sol_base02 = "h235" sol_base01 = "h240" sol_base00 = "h241" -sol_base0 = "h244" -sol_base1 = "h245" -sol_base2 = "h254" -sol_base3 = "h230" +sol_base0 = "h244" +sol_base1 = "h245" +sol_base2 = "h254" +sol_base3 = "h230" sol_yellow = "h136" sol_orange = "h166" sol_red = "h160" @@ -167,9 +211,12 @@ sol_violet = "h61" sol_blue = "h33" sol_cyan = "h37" sol_green = "h64" + + class SolarizedLight(LowLight): high = dict( - title = (sol_blue, 'default'), + background = (sol_base00, sol_base3), + title = (sol_cyan, 'default'), text = (sol_base00, 'default'), # Status bar & heading @@ -181,6 +228,12 @@ class SolarizedLight(LowLight): key = (sol_blue, 'default',), head = (sol_base00, 'default'), + # Options + option_selected = (sol_base03, sol_base2), + option_selected_key = (sol_blue, sol_base2), + option_active = (sol_orange, 'default'), + option_active_selected = (sol_orange, sol_base2), + # List and Connections method = (sol_cyan, 'default'), focus = (sol_base01, 'default'), @@ -193,7 +246,7 @@ class SolarizedLight(LowLight): error = (sol_red, 'default'), - header = (sol_base01, 'default'), + header = (sol_blue, 'default'), highlight = (sol_base01, 'default'), intercept = (sol_red, 'default',), replay = (sol_green, 'default',), @@ -211,17 +264,24 @@ class SolarizedLight(LowLight): class SolarizedDark(LowDark): high = dict( + background = (sol_base2, sol_base03), title = (sol_blue, 'default'), - text = (sol_base0, 'default'), + text = (sol_base1, 'default'), # Status bar & heading - heading = (sol_base03, sol_base1), - heading_key = (sol_blue+",bold", sol_base1), + heading = (sol_base2, sol_base01), + heading_key = (sol_blue + ",bold", sol_base01), heading_inactive = (sol_base1, sol_base02), # Help key = (sol_blue, 'default',), - head = (sol_base00, 'default'), + head = (sol_base2, 'default'), + + # Options + option_selected = (sol_base03, sol_base00), + option_selected_key = (sol_blue, sol_base00), + option_active = (sol_orange, 'default'), + option_active_selected = (sol_orange, sol_base00), # List and Connections method = (sol_cyan, 'default'), @@ -235,7 +295,7 @@ class SolarizedDark(LowDark): error = (sol_red, 'default'), - header = (sol_base01, 'default'), + header = (sol_blue, 'default'), highlight = (sol_base01, 'default'), intercept = (sol_red, 'default',), replay = (sol_green, 'default',), @@ -251,6 +311,7 @@ class SolarizedDark(LowDark): ) +DEFAULT = "dark" palettes = { "lowlight": LowLight(), "lowdark": LowDark(), diff --git a/libmproxy/console/pathedit.py b/libmproxy/console/pathedit.py new file mode 100644 index 00000000..dccec14a --- /dev/null +++ b/libmproxy/console/pathedit.py @@ -0,0 +1,69 @@ +import glob +import os.path + +import urwid + + +class _PathCompleter: + def __init__(self, _testing=False): + """ + _testing: disables reloading of the lookup table to make testing + possible. + """ + self.lookup, self.offset = None, None + self.final = None + self._testing = _testing + + def reset(self): + self.lookup = None + self.offset = -1 + + def complete(self, txt): + """ + Returns the next completion for txt, or None if there is no + completion. + """ + path = os.path.expanduser(txt) + if not self.lookup: + if not self._testing: + # Lookup is a set of (display value, actual value) tuples. + self.lookup = [] + if os.path.isdir(path): + files = glob.glob(os.path.join(path, "*")) + prefix = txt + else: + files = glob.glob(path + "*") + prefix = os.path.dirname(txt) + prefix = prefix or "./" + for f in files: + display = os.path.join(prefix, os.path.basename(f)) + if os.path.isdir(f): + display += "/" + self.lookup.append((display, f)) + if not self.lookup: + self.final = path + return path + self.lookup.sort() + self.offset = -1 + self.lookup.append((txt, txt)) + self.offset += 1 + if self.offset >= len(self.lookup): + self.offset = 0 + ret = self.lookup[self.offset] + self.final = ret[1] + return ret[0] + + +class PathEdit(urwid.Edit, _PathCompleter): + def __init__(self, *args, **kwargs): + urwid.Edit.__init__(self, *args, **kwargs) + _PathCompleter.__init__(self) + + def keypress(self, size, key): + if key == "tab": + comp = self.complete(self.get_edit_text()) + self.set_edit_text(comp) + self.set_edit_pos(len(comp)) + else: + self.reset() + return urwid.Edit.keypress(self, size, key) diff --git a/libmproxy/console/searchable.py b/libmproxy/console/searchable.py new file mode 100644 index 00000000..627d595d --- /dev/null +++ b/libmproxy/console/searchable.py @@ -0,0 +1,91 @@ +import urwid + +from . import signals + + +class Highlight(urwid.AttrMap): + def __init__(self, t): + urwid.AttrMap.__init__( + self, + urwid.Text(t.text), + "focusfield", + ) + self.backup = t + + +class Searchable(urwid.ListBox): + def __init__(self, state, contents): + self.walker = urwid.SimpleFocusListWalker(contents) + urwid.ListBox.__init__(self, self.walker) + self.state = state + self.search_offset = 0 + self.current_highlight = None + self.search_term = None + + def keypress(self, size, key): + if key == "/": + signals.status_prompt.send( + prompt = "Search for", + text = "", + callback = self.set_search + ) + elif key == "n": + self.find_next(False) + elif key == "N": + self.find_next(True) + elif key == "G": + self.set_focus(0) + self.walker._modified() + elif key == "g": + self.set_focus(len(self.walker) - 1) + self.walker._modified() + else: + return super(self.__class__, self).keypress(size, key) + + def set_search(self, text): + self.state.last_search = text + self.search_term = text or None + self.find_next(False) + + def set_highlight(self, offset): + if self.current_highlight is not None: + old = self.body[self.current_highlight] + self.body[self.current_highlight] = old.backup + if offset is None: + self.current_highlight = None + else: + self.body[offset] = Highlight(self.body[offset]) + self.current_highlight = offset + + def get_text(self, w): + if isinstance(w, urwid.Text): + return w.text + elif isinstance(w, Highlight): + return w.backup.text + else: + return None + + def find_next(self, backwards): + if not self.search_term: + if self.state.last_search: + self.search_term = self.state.last_search + else: + self.set_highlight(None) + return + # Start search at focus + 1 + if backwards: + rng = xrange(len(self.body) - 1, -1, -1) + else: + rng = xrange(1, len(self.body) + 1) + for i in rng: + off = (self.focus_position + i) % len(self.body) + w = self.body[off] + txt = self.get_text(w) + if txt and self.search_term in txt: + self.set_highlight(off) + self.set_focus(off, coming_from="above") + self.body._modified() + return + else: + self.set_highlight(None) + signals.status_message.send(message="Search not found.", expire=1) diff --git a/libmproxy/console/select.py b/libmproxy/console/select.py new file mode 100644 index 00000000..bf96a785 --- /dev/null +++ b/libmproxy/console/select.py @@ -0,0 +1,115 @@ +import urwid + +from . import common + + +class _OptionWidget(urwid.WidgetWrap): + def __init__(self, option, text, shortcut, active, focus): + self.option = option + textattr = "text" + keyattr = "key" + if focus and active: + textattr = "option_active_selected" + keyattr = "option_selected_key" + elif focus: + textattr = "option_selected" + keyattr = "option_selected_key" + elif active: + textattr = "option_active" + if shortcut: + text = common.highlight_key( + text, + shortcut, + textattr = textattr, + keyattr = keyattr + ) + opt = urwid.Text(text, align="left") + opt = urwid.AttrWrap(opt, textattr) + opt = urwid.Padding(opt, align = "center", width = 40) + urwid.WidgetWrap.__init__(self, opt) + + def keypress(self, size, key): + return key + + def selectable(self): + return True + + +class OptionWalker(urwid.ListWalker): + def __init__(self, options): + urwid.ListWalker.__init__(self) + self.options = options + self.focus = 0 + + def set_focus(self, pos): + self.focus = pos + + def get_focus(self): + return self.options[self.focus].render(True), self.focus + + def get_next(self, pos): + if pos >= len(self.options) - 1: + return None, None + return self.options[pos + 1].render(False), pos + 1 + + def get_prev(self, pos): + if pos <= 0: + return None, None + return self.options[pos - 1].render(False), pos - 1 + + +class Heading: + def __init__(self, text): + self.text = text + + def render(self, focus): + opt = urwid.Text("\n" + self.text, align="left") + opt = urwid.AttrWrap(opt, "title") + opt = urwid.Padding(opt, align = "center", width = 40) + return opt + + +_neg = lambda: False + + +class Option: + def __init__(self, text, shortcut, getstate=None, activate=None): + self.text = text + self.shortcut = shortcut + self.getstate = getstate or _neg + self.activate = activate or _neg + + def render(self, focus): + return _OptionWidget( + self, + self.text, + self.shortcut, + self.getstate(), + focus) + + +class Select(urwid.ListBox): + def __init__(self, options): + self.walker = OptionWalker(options) + urwid.ListBox.__init__( + self, + self.walker + ) + self.options = options + self.keymap = {} + for i in options: + if hasattr(i, "shortcut") and i.shortcut: + if i.shortcut in self.keymap: + raise ValueError("Duplicate shortcut key: %s" % i.shortcut) + self.keymap[i.shortcut] = i + + def keypress(self, size, key): + if key == "enter" or key == " ": + self.get_focus()[0].option.activate() + return None + key = common.shortcuts(key) + if key in self.keymap: + self.keymap[key].activate() + self.set_focus(self.options.index(self.keymap[key])) + return None + return super(self.__class__, self).keypress(size, key) diff --git a/libmproxy/console/signals.py b/libmproxy/console/signals.py new file mode 100644 index 00000000..c1bcf201 --- /dev/null +++ b/libmproxy/console/signals.py @@ -0,0 +1,32 @@ +import blinker + +# Show a status message in the action bar +status_message = blinker.Signal() + +# Prompt for input +status_prompt = blinker.Signal() + +# Prompt for a path +status_prompt_path = blinker.Signal() + +# Prompt for a single keystroke +status_prompt_onekey = blinker.Signal() + +# Call a callback in N seconds +call_in = blinker.Signal() + +# Focus the body, footer or header of the main window +focus = blinker.Signal() + +# Fired when settings change +update_settings = blinker.Signal() + +# Fired when a flow changes +flow_change = blinker.Signal() + +# Fired when the flow list or focus changes +flowlist_change = blinker.Signal() + +# Pop and push view state onto a stack +pop_view_state = blinker.Signal() +push_view_state = blinker.Signal() diff --git a/libmproxy/console/statusbar.py b/libmproxy/console/statusbar.py new file mode 100644 index 00000000..7eb2131b --- /dev/null +++ b/libmproxy/console/statusbar.py @@ -0,0 +1,254 @@ +import os.path + +import urwid + +import netlib.utils +from . import pathedit, signals, common +from .. import utils + + +class ActionBar(urwid.WidgetWrap): + def __init__(self): + urwid.WidgetWrap.__init__(self, None) + self.clear() + signals.status_message.connect(self.sig_message) + signals.status_prompt.connect(self.sig_prompt) + signals.status_prompt_path.connect(self.sig_path_prompt) + signals.status_prompt_onekey.connect(self.sig_prompt_onekey) + + self.last_path = "" + + self.prompting = False + self.onekey = False + self.pathprompt = False + + def sig_message(self, sender, message, expire=None): + w = urwid.Text(message) + self._w = w + if expire: + def cb(*args): + if w == self._w: + self.clear() + signals.call_in.send(seconds=expire, callback=cb) + + def prep_prompt(self, p): + return p.strip() + ": " + + def sig_prompt(self, sender, prompt, text, callback, args=()): + signals.focus.send(self, section="footer") + self._w = urwid.Edit(self.prep_prompt(prompt), text or "") + self.prompting = (callback, args) + + def sig_path_prompt(self, sender, prompt, callback, args=()): + signals.focus.send(self, section="footer") + self._w = pathedit.PathEdit( + self.prep_prompt(prompt), + os.path.dirname(self.last_path) + ) + self.pathprompt = True + self.prompting = (callback, args) + + def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): + """ + Keys are a set of (word, key) tuples. The appropriate key in the + word is highlighted. + """ + signals.focus.send(self, section="footer") + prompt = [prompt, " ("] + mkup = [] + for i, e in enumerate(keys): + mkup.extend(common.highlight_key(e[0], e[1])) + if i < len(keys) - 1: + mkup.append(",") + prompt.extend(mkup) + prompt.append(")? ") + self.onekey = set(i[1] for i in keys) + self._w = urwid.Edit(prompt, "") + self.prompting = (callback, args) + + def selectable(self): + return True + + def keypress(self, size, k): + if self.prompting: + if k == "esc": + self.prompt_done() + elif self.onekey: + if k == "enter": + self.prompt_done() + elif k in self.onekey: + self.prompt_execute(k) + elif k == "enter": + self.prompt_execute(self._w.get_edit_text()) + else: + if common.is_keypress(k): + self._w.keypress(size, k) + else: + return k + + def clear(self): + self._w = urwid.Text("") + + def prompt_done(self): + self.prompting = False + self.onekey = False + self.pathprompt = False + signals.status_message.send(message="") + signals.focus.send(self, section="body") + + def prompt_execute(self, txt): + if self.pathprompt: + self.last_path = txt + p, args = self.prompting + self.prompt_done() + msg = p(txt, *args) + if msg: + signals.status_message.send(message=msg, expire=1) + + +class StatusBar(urwid.WidgetWrap): + def __init__(self, master, helptext): + self.master, self.helptext = master, helptext + self.ab = ActionBar() + self.ib = urwid.WidgetWrap(urwid.Text("")) + self._w = urwid.Pile([self.ib, self.ab]) + signals.update_settings.connect(self.sig_update_settings) + signals.flowlist_change.connect(self.sig_update_settings) + self.redraw() + + def sig_update_settings(self, sender): + self.redraw() + + def keypress(self, *args, **kwargs): + return self.ab.keypress(*args, **kwargs) + + def get_status(self): + r = [] + + if self.master.setheaders.count(): + r.append("[") + r.append(("heading_key", "H")) + r.append("eaders]") + if self.master.replacehooks.count(): + r.append("[") + r.append(("heading_key", "R")) + r.append("eplacing]") + if self.master.client_playback: + r.append("[") + r.append(("heading_key", "cplayback")) + r.append(":%s to go]" % self.master.client_playback.count()) + if self.master.server_playback: + r.append("[") + r.append(("heading_key", "splayback")) + if self.master.nopop: + r.append(":%s in file]" % self.master.server_playback.count()) + else: + r.append(":%s to go]" % self.master.server_playback.count()) + if self.master.get_ignore_filter(): + r.append("[") + r.append(("heading_key", "I")) + r.append("gnore:%d]" % len(self.master.get_ignore_filter())) + if self.master.get_tcp_filter(): + r.append("[") + r.append(("heading_key", "T")) + r.append("CP:%d]" % len(self.master.get_tcp_filter())) + if self.master.state.intercept_txt: + r.append("[") + r.append(("heading_key", "i")) + r.append(":%s]" % self.master.state.intercept_txt) + if self.master.state.limit_txt: + r.append("[") + r.append(("heading_key", "l")) + r.append(":%s]" % self.master.state.limit_txt) + if self.master.stickycookie_txt: + r.append("[") + r.append(("heading_key", "t")) + r.append(":%s]" % self.master.stickycookie_txt) + if self.master.stickyauth_txt: + r.append("[") + r.append(("heading_key", "u")) + r.append(":%s]" % self.master.stickyauth_txt) + if self.master.state.default_body_view.name != "Auto": + r.append("[") + r.append(("heading_key", "M")) + r.append(":%s]" % self.master.state.default_body_view.name) + + opts = [] + if self.master.anticache: + opts.append("anticache") + if self.master.anticomp: + opts.append("anticomp") + if self.master.showhost: + opts.append("showhost") + if not self.master.refresh_server_playback: + opts.append("norefresh") + if self.master.killextra: + opts.append("killextra") + if self.master.server.config.no_upstream_cert: + opts.append("no-upstream-cert") + if self.master.state.follow_focus: + opts.append("following") + if self.master.stream_large_bodies: + opts.append( + "stream:%s" % netlib.utils.pretty_size( + self.master.stream_large_bodies.max_size + ) + ) + + if opts: + r.append("[%s]" % (":".join(opts))) + + if self.master.server.config.mode in ["reverse", "upstream"]: + dst = self.master.server.config.mode.dst + scheme = "https" if dst[0] else "http" + if dst[1] != dst[0]: + scheme += "2https" if dst[1] else "http" + r.append("[dest:%s]" % utils.unparse_url(scheme, *dst[2:])) + if self.master.scripts: + r.append("[") + r.append(("heading_key", "s")) + r.append("cripts:%s]" % len(self.master.scripts)) + # r.append("[lt:%0.3f]"%self.master.looptime) + + if self.master.stream: + r.append("[W:%s]" % self.master.stream_path) + + return r + + def redraw(self): + fc = self.master.state.flow_count() + if self.master.state.focus is None: + offset = 0 + else: + offset = min(self.master.state.focus + 1, fc) + t = [ + ('heading', ("[%s/%s]" % (offset, fc)).ljust(9)) + ] + + if self.master.server.bound: + host = self.master.server.address.host + if host == "0.0.0.0": + host = "*" + boundaddr = "[%s:%s]" % (host, self.master.server.address.port) + else: + boundaddr = "" + t.extend(self.get_status()) + status = urwid.AttrWrap(urwid.Columns([ + urwid.Text(t), + urwid.Text( + [ + self.helptext, + boundaddr + ], + align="right" + ), + ]), "heading") + self.ib._w = status + + def update(self, text): + self.helptext = text + self.redraw() + self.master.loop.draw_screen() + + def selectable(self): + return True diff --git a/libmproxy/console/tabs.py b/libmproxy/console/tabs.py new file mode 100644 index 00000000..953f6b12 --- /dev/null +++ b/libmproxy/console/tabs.py @@ -0,0 +1,39 @@ +import urwid + + +class Tabs(urwid.WidgetWrap): + def __init__(self, tabs, tab_offset=0): + urwid.WidgetWrap.__init__(self, "") + self.tab_offset = tab_offset + self.tabs = tabs + self.show() + + def _tab(self, content, attr): + p = urwid.Text(content, align="center") + p = urwid.Padding(p, align="center", width=("relative", 100)) + p = urwid.AttrWrap(p, attr) + return p + + def keypress(self, size, key): + if key in ["tab", "l"]: + self.tab_offset = (self.tab_offset + 1) % (len(self.tabs)) + self.show() + elif key == "h": + self.tab_offset = (self.tab_offset - 1) % (len(self.tabs)) + self.show() + return self._w.keypress(size, key) + + def show(self): + headers = [] + for i in range(len(self.tabs)): + txt = self.tabs[i][0]() + if i == self.tab_offset: + headers.append(self._tab(txt, "heading")) + else: + headers.append(self._tab(txt, "heading_inactive")) + headers = urwid.Columns(headers, dividechars=1) + self._w = urwid.Frame( + body = self.tabs[self.tab_offset][1](), + header = headers + ) + self._w.set_focus("body") diff --git a/libmproxy/console/window.py b/libmproxy/console/window.py new file mode 100644 index 00000000..d64e83df --- /dev/null +++ b/libmproxy/console/window.py @@ -0,0 +1,72 @@ +import urwid +from . import signals + + +class Window(urwid.Frame): + def __init__(self, master, body, header, footer, helpctx): + urwid.Frame.__init__( + self, + urwid.AttrWrap(body, "background"), + header = urwid.AttrWrap(header, "background") if header else None, + footer = urwid.AttrWrap(footer, "background") if footer else None + ) + self.master = master + self.helpctx = helpctx + signals.focus.connect(self.sig_focus) + + def sig_focus(self, sender, section): + self.focus_position = section + + def keypress(self, size, k): + k = super(self.__class__, self).keypress(size, k) + if k == "?": + self.master.view_help(self.helpctx) + elif k == "c": + if not self.master.client_playback: + signals.status_prompt_path.send( + self, + prompt = "Client replay", + callback = self.master.client_playback_path + ) + else: + signals.status_prompt_onekey.send( + self, + prompt = "Stop current client replay?", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = self.master.stop_client_playback_prompt, + ) + elif k == "i": + signals.status_prompt.send( + self, + prompt = "Intercept filter", + text = self.master.state.intercept_txt, + callback = self.master.set_intercept + ) + elif k == "o": + self.master.view_options() + elif k == "Q": + raise urwid.ExitMainLoop + elif k == "q": + signals.pop_view_state.send(self) + elif k == "S": + if not self.master.server_playback: + signals.status_prompt_path.send( + self, + prompt = "Server replay path", + callback = self.master.server_playback_path + ) + else: + signals.status_prompt_onekey.send( + self, + prompt = "Stop current server replay?", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = self.master.stop_server_playback_prompt, + ) + else: + return k diff --git a/libmproxy/controller.py b/libmproxy/controller.py index 9ca89184..98a3aec7 100644 --- a/libmproxy/controller.py +++ b/libmproxy/controller.py @@ -1,11 +1,14 @@ from __future__ import absolute_import -import Queue, threading +import Queue +import threading + class DummyReply: """ A reply object that does nothing. Useful when we need an object to seem like it has a channel, and during testing. """ + def __init__(self): self.acked = False @@ -19,6 +22,7 @@ class Reply: This object is used to respond to the message through the return channel. """ + def __init__(self, obj): self.obj = obj self.q = Queue.Queue() @@ -67,11 +71,13 @@ class Slave(threading.Thread): Slaves get a channel end-point through which they can send messages to the master. """ + def __init__(self, channel, server): self.channel, self.server = channel, server self.server.set_channel(channel) threading.Thread.__init__(self) - self.name = "SlaveThread (%s:%s)" % (self.server.address.host, self.server.address.port) + self.name = "SlaveThread (%s:%s)" % ( + self.server.address.host, self.server.address.port) def run(self): self.server.serve_forever() @@ -81,6 +87,7 @@ class Master(object): """ Masters get and respond to messages from slaves. """ + def __init__(self, server): """ server may be None if no server is needed. diff --git a/libmproxy/dump.py b/libmproxy/dump.py index c464644c..ee8c65a0 100644 --- a/libmproxy/dump.py +++ b/libmproxy/dump.py @@ -53,7 +53,7 @@ class Options(object): def str_response(resp): - r = "%s %s"%(resp.code, resp.msg) + r = "%s %s" % (resp.code, resp.msg) if resp.is_replay: r = "[replay] " + r return r @@ -64,7 +64,7 @@ def str_request(f, showhost): c = f.client_conn.address.host else: c = "[replay]" - r = "%s %s %s"%(c, f.request.method, f.request.pretty_url(showhost)) + r = "%s %s %s" % (c, f.request.method, f.request.pretty_url(showhost)) if f.request.stickycookie: r = "[stickycookie] " + r return r @@ -102,7 +102,7 @@ class DumpMaster(flow.FlowMaster): try: f = file(path, options.outfile[1]) self.start_stream(f, self.filt) - except IOError, v: + except IOError as v: raise DumpError(v.strerror) if options.replacements: @@ -140,7 +140,7 @@ class DumpMaster(flow.FlowMaster): if options.rfile: try: self.load_flows_file(options.rfile) - except flow.FlowReadError, v: + except flow.FlowReadError as v: self.add_event("Flow file corrupted.", "error") raise DumpError(v) @@ -171,7 +171,7 @@ class DumpMaster(flow.FlowMaster): def _print_message(self, message): if self.o.flow_detail >= 2: - print(self.indent(4, message.headers), file=self.outfile) + print(self.indent(4, message.headers.format()), file=self.outfile) if self.o.flow_detail >= 3: if message.content == http.CONTENT_MISSING: print(self.indent(4, "(content missing)"), file=self.outfile) @@ -181,12 +181,18 @@ class DumpMaster(flow.FlowMaster): if not utils.isBin(content): try: jsn = json.loads(content) - print(self.indent(4, json.dumps(jsn, indent=2)), file=self.outfile) + print( + self.indent( + 4, + json.dumps( + jsn, + indent=2)), + file=self.outfile) except ValueError: print(self.indent(4, content), file=self.outfile) else: d = netlib.utils.hexdump(content) - d = "\n".join("%s\t%s %s"%i for i in d) + d = "\n".join("%s\t%s %s" % i for i in d) print(self.indent(4, d), file=self.outfile) if self.o.flow_detail >= 2: print("", file=self.outfile) @@ -207,8 +213,13 @@ class DumpMaster(flow.FlowMaster): if f.response.content == http.CONTENT_MISSING: sz = "(content missing)" else: - sz = utils.pretty_size(len(f.response.content)) - print(" << %s %s" % (str_response(f.response), sz), file=self.outfile) + sz = netlib.utils.pretty_size(len(f.response.content)) + print( + " << %s %s" % + (str_response( + f.response), + sz), + file=self.outfile) self._print_message(f.response) if f.error: diff --git a/libmproxy/encoding.py b/libmproxy/encoding.py index 0fd90870..f107eb5f 100644 --- a/libmproxy/encoding.py +++ b/libmproxy/encoding.py @@ -3,12 +3,14 @@ """ from __future__ import absolute_import import cStringIO -import gzip, zlib +import gzip +import zlib __ALL__ = ["ENCODINGS"] ENCODINGS = set(["identity", "gzip", "deflate"]) + def decode(e, content): encoding_map = { "identity": identity, @@ -19,6 +21,7 @@ def decode(e, content): return None return encoding_map[e](content) + def encode(e, content): encoding_map = { "identity": identity, @@ -29,6 +32,7 @@ def encode(e, content): return None return encoding_map[e](content) + def identity(content): """ Returns content unchanged. Identity is the default value of @@ -36,6 +40,7 @@ def identity(content): """ return content + def decode_gzip(content): gfile = gzip.GzipFile(fileobj=cStringIO.StringIO(content)) try: @@ -43,6 +48,7 @@ def decode_gzip(content): except (IOError, EOFError): return None + def encode_gzip(content): s = cStringIO.StringIO() gf = gzip.GzipFile(fileobj=s, mode='wb') @@ -50,6 +56,7 @@ def encode_gzip(content): gf.close() return s.getvalue() + def decode_deflate(content): """ Returns decompressed data for DEFLATE. Some servers may respond with @@ -67,6 +74,7 @@ def decode_deflate(content): except zlib.error: return None + def encode_deflate(content): """ Returns compressed content, always including zlib header and checksum. diff --git a/libmproxy/filt.py b/libmproxy/filt.py index 40b2f6c9..3081eb94 100644 --- a/libmproxy/filt.py +++ b/libmproxy/filt.py @@ -32,16 +32,17 @@ rex Equivalent to ~u rex """ from __future__ import absolute_import -import re, sys +import re +import sys from .contrib import pyparsing as pp from .protocol.http import decoded class _Token: def dump(self, indent=0, fp=sys.stdout): - print >> fp, "\t"*indent, self.__class__.__name__, + print >> fp, "\t" * indent, self.__class__.__name__, if hasattr(self, "expr"): - print >> fp, "(%s)"%self.expr, + print >> fp, "(%s)" % self.expr, print >> fp @@ -54,6 +55,7 @@ class _Action(_Token): class FErr(_Action): code = "e" help = "Match error" + def __call__(self, f): return True if f.error else False @@ -61,6 +63,7 @@ class FErr(_Action): class FReq(_Action): code = "q" help = "Match request with no response" + def __call__(self, f): if not f.response: return True @@ -69,6 +72,7 @@ class FReq(_Action): class FResp(_Action): code = "s" help = "Match response" + def __call__(self, f): return True if f.response else False @@ -79,7 +83,7 @@ class _Rex(_Action): try: self.re = re.compile(self.expr) except: - raise ValueError, "Cannot compile expression." + raise ValueError("Cannot compile expression.") def _check_content_type(expr, o): @@ -100,6 +104,7 @@ class FAsset(_Action): "image/.*", "application/x-shockwave-flash" ] + def __call__(self, f): if f.response: for i in self.ASSET_TYPES: @@ -111,6 +116,7 @@ class FAsset(_Action): class FContentType(_Rex): code = "t" help = "Content-type header" + def __call__(self, f): if _check_content_type(self.expr, f.request): return True @@ -122,6 +128,7 @@ class FContentType(_Rex): class FRequestContentType(_Rex): code = "tq" help = "Request Content-Type header" + def __call__(self, f): return _check_content_type(self.expr, f.request) @@ -129,6 +136,7 @@ class FRequestContentType(_Rex): class FResponseContentType(_Rex): code = "ts" help = "Response Content-Type header" + def __call__(self, f): if f.response: return _check_content_type(self.expr, f.response) @@ -138,6 +146,7 @@ class FResponseContentType(_Rex): class FHead(_Rex): code = "h" help = "Header" + def __call__(self, f): if f.request.headers.match_re(self.expr): return True @@ -149,6 +158,7 @@ class FHead(_Rex): class FHeadRequest(_Rex): code = "hq" help = "Request header" + def __call__(self, f): if f.request.headers.match_re(self.expr): return True @@ -157,6 +167,7 @@ class FHeadRequest(_Rex): class FHeadResponse(_Rex): code = "hs" help = "Response header" + def __call__(self, f): if f.response and f.response.headers.match_re(self.expr): return True @@ -165,6 +176,7 @@ class FHeadResponse(_Rex): class FBod(_Rex): code = "b" help = "Body" + def __call__(self, f): if f.request and f.request.content: with decoded(f.request): @@ -180,6 +192,7 @@ class FBod(_Rex): class FBodRequest(_Rex): code = "bq" help = "Request body" + def __call__(self, f): if f.request and f.request.content: with decoded(f.request): @@ -190,6 +203,7 @@ class FBodRequest(_Rex): class FBodResponse(_Rex): code = "bs" help = "Response body" + def __call__(self, f): if f.response and f.response.content: with decoded(f.response): @@ -200,6 +214,7 @@ class FBodResponse(_Rex): class FMethod(_Rex): code = "m" help = "Method" + def __call__(self, f): return bool(re.search(self.expr, f.request.method, re.IGNORECASE)) @@ -207,6 +222,7 @@ class FMethod(_Rex): class FDomain(_Rex): code = "d" help = "Domain" + def __call__(self, f): return bool(re.search(self.expr, f.request.host, re.IGNORECASE)) @@ -215,6 +231,7 @@ class FUrl(_Rex): code = "u" help = "URL" # FUrl is special, because it can be "naked". + @classmethod def make(klass, s, loc, toks): if len(toks) > 1: @@ -233,6 +250,7 @@ class _Int(_Action): class FCode(_Int): code = "c" help = "HTTP response code" + def __call__(self, f): if f.response and f.response.code == self.num: return True @@ -243,9 +261,9 @@ class FAnd(_Token): self.lst = lst def dump(self, indent=0, fp=sys.stdout): - print >> fp, "\t"*indent, self.__class__.__name__ + print >> fp, "\t" * indent, self.__class__.__name__ for i in self.lst: - i.dump(indent+1, fp) + i.dump(indent + 1, fp) def __call__(self, f): return all(i(f) for i in self.lst) @@ -256,9 +274,9 @@ class FOr(_Token): self.lst = lst def dump(self, indent=0, fp=sys.stdout): - print >> fp, "\t"*indent, self.__class__.__name__ + print >> fp, "\t" * indent, self.__class__.__name__ for i in self.lst: - i.dump(indent+1, fp) + i.dump(indent + 1, fp) def __call__(self, f): return any(i(f) for i in self.lst) @@ -269,7 +287,7 @@ class FNot(_Token): self.itm = itm[0] def dump(self, indent=0, fp=sys.stdout): - print >> fp, "\t"*indent, self.__class__.__name__ + print >> fp, "\t" * indent, self.__class__.__name__ self.itm.dump(indent + 1, fp) def __call__(self, f): @@ -299,26 +317,28 @@ filt_rex = [ filt_int = [ FCode ] + + def _make(): # Order is important - multi-char expressions need to come before narrow # ones. parts = [] for klass in filt_unary: - f = pp.Literal("~%s"%klass.code) + f = pp.Literal("~%s" % klass.code) f.setParseAction(klass.make) parts.append(f) - simplerex = "".join(c for c in pp.printables if c not in "()~'\"") + simplerex = "".join(c for c in pp.printables if c not in "()~'\"") rex = pp.Word(simplerex) |\ - pp.QuotedString("\"", escChar='\\') |\ - pp.QuotedString("'", escChar='\\') + pp.QuotedString("\"", escChar='\\') |\ + pp.QuotedString("'", escChar='\\') for klass in filt_rex: - f = pp.Literal("~%s"%klass.code) + rex.copy() + f = pp.Literal("~%s" % klass.code) + rex.copy() f.setParseAction(klass.make) parts.append(f) for klass in filt_int: - f = pp.Literal("~%s"%klass.code) + pp.Word(pp.nums) + f = pp.Literal("~%s" % klass.code) + pp.Word(pp.nums) f.setParseAction(klass.make) parts.append(f) @@ -328,14 +348,20 @@ def _make(): parts.append(f) atom = pp.MatchFirst(parts) - expr = pp.operatorPrecedence( - atom, - [ - (pp.Literal("!").suppress(), 1, pp.opAssoc.RIGHT, lambda x: FNot(*x)), - (pp.Literal("&").suppress(), 2, pp.opAssoc.LEFT, lambda x: FAnd(*x)), - (pp.Literal("|").suppress(), 2, pp.opAssoc.LEFT, lambda x: FOr(*x)), - ] - ) + expr = pp.operatorPrecedence(atom, + [(pp.Literal("!").suppress(), + 1, + pp.opAssoc.RIGHT, + lambda x: FNot(*x)), + (pp.Literal("&").suppress(), + 2, + pp.opAssoc.LEFT, + lambda x: FAnd(*x)), + (pp.Literal("|").suppress(), + 2, + pp.opAssoc.LEFT, + lambda x: FOr(*x)), + ]) expr = pp.OneOrMore(expr) return expr.setParseAction(lambda x: FAnd(x) if len(x) != 1 else x) bnf = _make() @@ -355,15 +381,15 @@ def parse(s): help = [] for i in filt_unary: help.append( - ("~%s"%i.code, i.help) + ("~%s" % i.code, i.help) ) for i in filt_rex: help.append( - ("~%s regex"%i.code, i.help) + ("~%s regex" % i.code, i.help) ) for i in filt_int: help.append( - ("~%s int"%i.code, i.help) + ("~%s int" % i.code, i.help) ) help.sort() help.extend( @@ -373,4 +399,4 @@ help.extend( ("|", "or"), ("(...)", "grouping"), ] -)
\ No newline at end of file +) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 8343c183..6154e3d7 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -17,9 +17,6 @@ from .proxy.config import HostMatcher from .proxy.connection import ClientConnection, ServerConnection import urlparse -ODict = odict.ODict -ODictCaseless = odict.ODictCaseless - class AppRegistry: def __init__(self): @@ -165,7 +162,8 @@ class StreamLargeBodies(object): r.headers, is_request, flow.request.method, code ) if not (0 <= expected_size <= self.max_size): - r.stream = r.stream or True # r.stream may already be a callable, which we want to preserve. + # r.stream may already be a callable, which we want to preserve. + r.stream = r.stream or True class ClientPlaybackState: @@ -203,8 +201,16 @@ class ClientPlaybackState: class ServerPlaybackState: - def __init__(self, headers, flows, exit, nopop, ignore_params, ignore_content, - ignore_payload_params, ignore_host): + def __init__( + self, + headers, + flows, + exit, + nopop, + ignore_params, + ignore_content, + ignore_payload_params, + ignore_host): """ headers: Case-insensitive list of request headers that should be included in request-response matching. @@ -232,7 +238,7 @@ class ServerPlaybackState: r = flow.request _, _, path, _, query, _ = urlparse.urlparse(r.url) - queriesArray = urlparse.parse_qsl(query) + queriesArray = urlparse.parse_qsl(query, keep_blank_values=True) key = [ str(r.port), @@ -242,7 +248,7 @@ class ServerPlaybackState: ] if not self.ignore_content: - form_contents = r.get_form_urlencoded() + form_contents = r.get_form() if self.ignore_payload_params and form_contents: key.extend( p for p in form_contents @@ -271,7 +277,7 @@ class ServerPlaybackState: # to prevent a mismatch between unicode/non-unicode. v = [str(x) for x in v] hdrs.append((i, v)) - key.append(repr(hdrs)) + key.append(hdrs) return hashlib.sha256(repr(key)).digest() def next_flow(self, request): @@ -462,8 +468,9 @@ class FlowStore(FlowList): Notifies the state that a flow has been updated. The flow must be present in the state. """ - for view in self.views: - view._update(f) + if f in self: + for view in self.views: + view._update(f) def _remove(self, f): """ @@ -534,7 +541,8 @@ class State(object): def flow_count(self): return len(self.flows) - # TODO: All functions regarding flows that don't cause side-effects should be moved into FlowStore. + # TODO: All functions regarding flows that don't cause side-effects should + # be moved into FlowStore. def index(self, f): return self.flows.index(f) @@ -593,6 +601,10 @@ class State(object): def accept_all(self, master): self.flows.accept_all(master) + def backup(self, f): + f.backup() + self.update_flow(f) + def revert(self, f): f.revert() self.update_flow(f) @@ -658,7 +670,7 @@ class FlowMaster(controller.Master): """ try: s = script.Script(command, self) - except script.ScriptError, v: + except script.ScriptError as v: return v.args[0] self.scripts.append(s) @@ -722,8 +734,17 @@ class FlowMaster(controller.Master): def stop_client_playback(self): self.client_playback = None - def start_server_playback(self, flows, kill, headers, exit, nopop, ignore_params, - ignore_content, ignore_payload_params, ignore_host): + def start_server_playback( + self, + flows, + kill, + headers, + exit, + nopop, + ignore_params, + ignore_content, + ignore_payload_params, + ignore_host): """ flows: List of flows. kill: Boolean, should we kill requests not part of the replay? @@ -732,9 +753,15 @@ class FlowMaster(controller.Master): ignore_payload_params: list of content params to ignore in server replay ignore_host: true if request host should be ignored in server replay """ - self.server_playback = ServerPlaybackState(headers, flows, exit, nopop, - ignore_params, ignore_content, - ignore_payload_params, ignore_host) + self.server_playback = ServerPlaybackState( + headers, + flows, + exit, + nopop, + ignore_params, + ignore_content, + ignore_payload_params, + ignore_host) self.kill_nonreplay = kill def stop_server_playback(self): @@ -771,6 +798,8 @@ class FlowMaster(controller.Master): if all(e): self.shutdown() self.client_playback.tick(self) + if self.client_playback.done(): + self.client_playback = None return super(FlowMaster, self).tick(q, timeout) @@ -780,25 +809,38 @@ class FlowMaster(controller.Master): def create_request(self, method, scheme, host, port, path): """ this method creates a new artificial and minimalist request also adds it to flowlist - """ + """ c = ClientConnection.from_state(dict( - address=dict(address=(host, port), use_ipv6=False), - clientcert=None - )) + address=dict(address=(host, port), use_ipv6=False), + clientcert=None + )) s = ServerConnection.from_state(dict( - address=dict(address=(host, port), use_ipv6=False), - state=[], - source_address=None, #source_address=dict(address=(host, port), use_ipv6=False), - cert=None, - sni=host, - ssl_established=True - )) - f = http.HTTPFlow(c,s); + address=dict(address=(host, port), use_ipv6=False), + state=[], + source_address=None, + # source_address=dict(address=(host, port), use_ipv6=False), + cert=None, + sni=host, + ssl_established=True + )) + f = http.HTTPFlow(c, s) headers = ODictCaseless() - - req = http.HTTPRequest("absolute", method, scheme, host, port, path, (1, 1), headers, None, - None, None, None) + + req = http.HTTPRequest( + "absolute", + method, + scheme, + host, + port, + path, + (1, + 1), + headers, + None, + None, + None, + None) f.request = req return self.load_flow(f) @@ -809,7 +851,8 @@ class FlowMaster(controller.Master): if self.server and self.server.config.mode == "reverse": f.request.host, f.request.port = self.server.config.mode.dst[2:] - f.request.scheme = "https" if self.server.config.mode.dst[1] else "http" + f.request.scheme = "https" if self.server.config.mode.dst[ + 1] else "http" f.reply = controller.DummyReply() if f.request: @@ -836,7 +879,7 @@ class FlowMaster(controller.Master): try: f = file(path, "rb") freader = FlowReader(f) - except IOError, v: + except IOError as v: raise FlowReadError(v.strerror) return self.load_flows(freader) @@ -877,7 +920,8 @@ class FlowMaster(controller.Master): f.backup() f.request.is_replay = True if f.request.content: - f.request.headers["Content-Length"] = [str(len(f.request.content))] + f.request.headers[ + "Content-Length"] = [str(len(f.request.content))] f.response = None f.error = None self.process_new_request(f) @@ -1028,7 +1072,7 @@ class FlowReader: """ off = 0 try: - while 1: + while True: data = tnetstring.load(self.fo) if tuple(data["version"][:2]) != version.IVERSION[:2]: v = ".".join(str(i) for i in data["version"]) @@ -1037,7 +1081,7 @@ class FlowReader: ) off = self.fo.tell() yield handle.protocols[data["type"]]["flow"].from_state(data) - except ValueError, v: + except ValueError as v: # Error is due to EOF if self.fo.tell() == off and self.fo.read() == '': return diff --git a/libmproxy/main.py b/libmproxy/main.py index e5b7f56b..73e6c62b 100644 --- a/libmproxy/main.py +++ b/libmproxy/main.py @@ -70,27 +70,29 @@ def get_server(dummy_server, options): else: try: return ProxyServer(options) - except ProxyServerError, v: + except ProxyServerError as v: print(str(v), file=sys.stderr) sys.exit(1) -def mitmproxy(): # pragma: nocover +def mitmproxy(args=None): # pragma: nocover from . import console check_versions() assert_utf8_env() parser = cmdline.mitmproxy() - options = parser.parse_args() + options = parser.parse_args(args) if options.quiet: options.verbose = 0 proxy_config = process_proxy_options(parser, options) console_options = console.Options(**cmdline.get_common_options(options)) console_options.palette = options.palette + console_options.palette_transparent = options.palette_transparent console_options.eventlog = options.eventlog console_options.intercept = options.intercept + console_options.limit = options.limit server = get_server(console_options.no_server, proxy_config) @@ -101,13 +103,13 @@ def mitmproxy(): # pragma: nocover pass -def mitmdump(): # pragma: nocover +def mitmdump(args=None): # pragma: nocover from . import dump check_versions() parser = cmdline.mitmdump() - options = parser.parse_args() + options = parser.parse_args(args) if options.quiet: options.verbose = 0 options.flow_detail = 0 @@ -135,13 +137,13 @@ def mitmdump(): # pragma: nocover pass -def mitmweb(): # pragma: nocover +def mitmweb(args=None): # pragma: nocover from . import web check_versions() parser = cmdline.mitmweb() - options = parser.parse_args() + options = parser.parse_args(args) if options.quiet: options.verbose = 0 diff --git a/libmproxy/onboarding/app.py b/libmproxy/onboarding/app.py index 37f05e96..6edd74b1 100644 --- a/libmproxy/onboarding/app.py +++ b/libmproxy/onboarding/app.py @@ -45,7 +45,10 @@ class PEM(tornado.web.RequestHandler): def get(self): p = os.path.join(self.request.master.server.config.cadir, self.filename) self.set_header("Content-Type", "application/x-x509-ca-cert") - self.set_header("Content-Disposition", "inline; filename={}".format(self.filename)) + self.set_header( + "Content-Disposition", + "inline; filename={}".format( + self.filename)) with open(p, "rb") as f: self.write(f.read()) @@ -59,7 +62,10 @@ class P12(tornado.web.RequestHandler): def get(self): p = os.path.join(self.request.master.server.config.cadir, self.filename) self.set_header("Content-Type", "application/x-pkcs12") - self.set_header("Content-Disposition", "inline; filename={}".format(self.filename)) + self.set_header( + "Content-Disposition", + "inline; filename={}".format( + self.filename)) with open(p, "rb") as f: self.write(f.read()) @@ -78,7 +84,6 @@ application = tornado.web.Application( } ), ], - #debug=True + # debug=True ) mapp = Adapter(application) - diff --git a/libmproxy/platform/__init__.py b/libmproxy/platform/__init__.py index 1b2cf909..e1ff7c47 100644 --- a/libmproxy/platform/__init__.py +++ b/libmproxy/platform/__init__.py @@ -8,9 +8,9 @@ if sys.platform == "linux2": elif sys.platform == "darwin": from . import osx resolver = osx.Resolver -elif sys.platform == "freebsd10": +elif sys.platform.startswith("freebsd"): from . import osx resolver = osx.Resolver elif sys.platform == "win32": from . import windows - resolver = windows.Resolver
\ No newline at end of file + resolver = windows.Resolver diff --git a/libmproxy/platform/linux.py b/libmproxy/platform/linux.py index d5cfec90..e60a9950 100644 --- a/libmproxy/platform/linux.py +++ b/libmproxy/platform/linux.py @@ -1,4 +1,5 @@ -import socket, struct +import socket +import struct # Python socket module does not have this constant SO_ORIGINAL_DST = 80 diff --git a/libmproxy/platform/osx.py b/libmproxy/platform/osx.py index 810e5e5f..c5922850 100644 --- a/libmproxy/platform/osx.py +++ b/libmproxy/platform/osx.py @@ -21,6 +21,7 @@ class Resolver(object): peer = csock.getpeername() stxt = subprocess.check_output(self.STATECMD, stderr=subprocess.STDOUT) if "sudo: a password is required" in stxt: - raise RuntimeError("Insufficient privileges to access pfctl. " - "See http://mitmproxy.org/doc/transparent/osx.html for details.") + raise RuntimeError( + "Insufficient privileges to access pfctl. " + "See http://mitmproxy.org/doc/transparent/osx.html for details.") return pf.lookup(peer[0], peer[1], stxt) diff --git a/libmproxy/platform/pf.py b/libmproxy/platform/pf.py index 8c2f4678..97a4c192 100644 --- a/libmproxy/platform/pf.py +++ b/libmproxy/platform/pf.py @@ -13,7 +13,7 @@ def lookup(address, port, s): if "ESTABLISHED:ESTABLISHED" in i and spec in i: s = i.split() if len(s) > 4: - if sys.platform == "freebsd10": + if sys.platform.startswith("freebsd"): # strip parentheses for FreeBSD pfctl s = s[3][1:-1].split(":") else: @@ -21,4 +21,4 @@ def lookup(address, port, s): if len(s) == 2: return s[0], int(s[1]) - raise RuntimeError("Could not resolve original destination.")
\ No newline at end of file + raise RuntimeError("Could not resolve original destination.") diff --git a/libmproxy/platform/windows.py b/libmproxy/platform/windows.py index 98bfebcf..09a4422f 100644 --- a/libmproxy/platform/windows.py +++ b/libmproxy/platform/windows.py @@ -197,9 +197,12 @@ class TransparentProxy(object): self.driver = WinDivert() self.driver.register() - self.request_filter = custom_filter or " or ".join(("tcp.DstPort == %d" % p) for p in redirect_ports) + self.request_filter = custom_filter or " or ".join( + ("tcp.DstPort == %d" % + p) for p in redirect_ports) self.request_forward_handle = None - self.request_forward_thread = threading.Thread(target=self.request_forward) + self.request_forward_thread = threading.Thread( + target=self.request_forward) self.request_forward_thread.daemon = True self.addr_pid_map = dict() @@ -235,17 +238,25 @@ class TransparentProxy(object): # Block all ICMP requests (which are sent on Windows by default). # In layman's terms: If we don't do this, our proxy machine tells the client that it can directly connect to the # real gateway if they are on the same network. - self.icmp_handle = self.driver.open_handle(filter="icmp", layer=Layer.NETWORK, flags=Flag.DROP) - - self.response_handle = self.driver.open_handle(filter=self.response_filter, layer=Layer.NETWORK) + self.icmp_handle = self.driver.open_handle( + filter="icmp", + layer=Layer.NETWORK, + flags=Flag.DROP) + + self.response_handle = self.driver.open_handle( + filter=self.response_filter, + layer=Layer.NETWORK) self.response_thread.start() if self.mode == "forward" or self.mode == "both": - self.request_forward_handle = self.driver.open_handle(filter=self.request_filter, - layer=Layer.NETWORK_FORWARD) + self.request_forward_handle = self.driver.open_handle( + filter=self.request_filter, + layer=Layer.NETWORK_FORWARD) self.request_forward_thread.start() if self.mode == "local" or self.mode == "both": - self.request_local_handle = self.driver.open_handle(filter=self.request_filter, layer=Layer.NETWORK) + self.request_local_handle = self.driver.open_handle( + filter=self.request_filter, + layer=Layer.NETWORK) self.request_local_thread.start() def shutdown(self): @@ -266,14 +277,17 @@ class TransparentProxy(object): try: raw_packet, metadata = handle.recv() return self.driver.parse_packet(raw_packet), metadata - except WindowsError, e: + except WindowsError as e: if e.winerror == 995: return None, None else: raise def fetch_pids(self): - ret = windll.iphlpapi.GetTcpTable2(byref(self.tcptable2), byref(self.tcptable2_size), 0) + ret = windll.iphlpapi.GetTcpTable2( + byref( + self.tcptable2), byref( + self.tcptable2_size), 0) if ret == ERROR_INSUFFICIENT_BUFFER: self.tcptable2 = MIB_TCPTABLE2(self.tcptable2_size.value) self.fetch_pids() @@ -299,7 +313,8 @@ class TransparentProxy(object): self.fetch_pids() # If this fails, we most likely have a connection from an external client to - # a local server on 80/443. In this, case we always want to proxy the request. + # a local server on 80/443. In this, case we always want to proxy + # the request. pid = self.addr_pid_map.get(client, None) if pid not in self.trusted_pids: @@ -325,7 +340,8 @@ class TransparentProxy(object): server = (packet.dst_addr, packet.dst_port) if client in self.client_server_map: - del self.client_server_map[client] # Force re-add to mark as "newest" entry in the dict. + # Force re-add to mark as "newest" entry in the dict. + del self.client_server_map[client] while len(self.client_server_map) > self.connection_cache_size: self.client_server_map.popitem(False) @@ -335,7 +351,8 @@ class TransparentProxy(object): metadata.direction = Direction.INBOUND packet = self.driver.update_packet_checksums(packet) - # Use any handle thats on the NETWORK layer - request_local may be unavailable. + # Use any handle thats on the NETWORK layer - request_local may be + # unavailable. self.response_handle.send((packet.raw, metadata)) def response(self): @@ -361,15 +378,32 @@ class TransparentProxy(object): if __name__ == "__main__": - parser = configargparse.ArgumentParser(description="Windows Transparent Proxy") - parser.add_argument('--mode', choices=['forward', 'local', 'both'], default="both", - help='redirection operation mode: "forward" to only redirect forwarded packets, ' - '"local" to only redirect packets originating from the local machine') + parser = configargparse.ArgumentParser( + description="Windows Transparent Proxy") + parser.add_argument( + '--mode', + choices=[ + 'forward', + 'local', + 'both'], + default="both", + help='redirection operation mode: "forward" to only redirect forwarded packets, ' + '"local" to only redirect packets originating from the local machine') group = parser.add_mutually_exclusive_group() - group.add_argument("--redirect-ports", nargs="+", type=int, default=[80, 443], metavar="80", - help="ports that should be forwarded to the proxy") - group.add_argument("--custom-filter", default=None, metavar="WINDIVERT_FILTER", - help="Custom WinDivert interception rule.") + group.add_argument( + "--redirect-ports", + nargs="+", + type=int, + default=[ + 80, + 443], + metavar="80", + help="ports that should be forwarded to the proxy") + group.add_argument( + "--custom-filter", + default=None, + metavar="WINDIVERT_FILTER", + help="Custom WinDivert interception rule.") parser.add_argument("--proxy-addr", default=False, help="Proxy Server Address") parser.add_argument("--proxy-port", type=int, default=8080, diff --git a/libmproxy/protocol/__init__.py b/libmproxy/protocol/__init__.py index f5d6a2d0..bbc20dba 100644 --- a/libmproxy/protocol/__init__.py +++ b/libmproxy/protocol/__init__.py @@ -1 +1 @@ -from .primitives import *
\ No newline at end of file +from .primitives import * diff --git a/libmproxy/protocol/handle.py b/libmproxy/protocol/handle.py index 100c7368..49cb3c1b 100644 --- a/libmproxy/protocol/handle.py +++ b/libmproxy/protocol/handle.py @@ -6,6 +6,7 @@ protocols = { 'tcp': dict(handler=tcp.TCPHandler) } + def protocol_handler(protocol): """ @type protocol: str @@ -14,4 +15,6 @@ def protocol_handler(protocol): if protocol in protocols: return protocols[protocol]["handler"] - raise NotImplementedError("Unknown Protocol: %s" % protocol) # pragma: nocover
\ No newline at end of file + raise NotImplementedError( + "Unknown Protocol: %s" % + protocol) # pragma: nocover diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 49310ec3..91e74567 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -6,15 +6,16 @@ import time import copy from email.utils import parsedate_tz, formatdate, mktime_tz import threading -from netlib import http, tcp, http_status +from netlib import http, tcp, http_status, http_cookies import netlib.utils -from netlib.odict import ODict, ODictCaseless +from netlib import odict from .tcp import TCPHandler from .primitives import KILL, ProtocolHandler, Flow, Error from ..proxy.connection import ServerConnection from .. import encoding, utils, controller, stateobject, proxy HDR_FORM_URLENCODED = "application/x-www-form-urlencoded" +HDR_FORM_MULTIPART = "multipart/form-data" CONTENT_MISSING = 0 @@ -22,19 +23,6 @@ class KillSignal(Exception): pass -def get_line(fp): - """ - Get a line, possibly preceded by a blank. - """ - line = fp.readline() - if line == "\r\n" or line == "\n": - # Possible leftover from previous message - line = fp.readline() - if line == "": - raise tcp.NetLibDisconnect() - return line - - def send_connect_request(conn, host, port, update_state=True): upstream_request = HTTPRequest( "authority", @@ -44,7 +32,7 @@ def send_connect_request(conn, host, port, update_state=True): port, None, (1, 1), - ODictCaseless(), + odict.ODictCaseless(), "" ) conn.send(upstream_request.assemble()) @@ -99,7 +87,7 @@ class HTTPMessage(stateobject.StateObject): timestamp_end=None): self.httpversion = httpversion self.headers = headers - """@type: ODictCaseless""" + """@type: odict.ODictCaseless""" self.content = content self.timestamp_start = timestamp_start @@ -107,7 +95,7 @@ class HTTPMessage(stateobject.StateObject): _stateobject_attributes = dict( httpversion=tuple, - headers=ODictCaseless, + headers=odict.ODictCaseless, content=str, timestamp_start=float, timestamp_end=float @@ -119,6 +107,8 @@ class HTTPMessage(stateobject.StateObject): if short: if self.content: ret["contentLength"] = len(self.content) + elif self.content == CONTENT_MISSING: + ret["contentLength"] = None else: ret["contentLength"] = 0 return ret @@ -239,7 +229,7 @@ class HTTPRequest(HTTPMessage): httpversion: HTTP version tuple, e.g. (1,1) - headers: ODictCaseless object + headers: odict.ODictCaseless object content: Content of the request, None, or CONTENT_MISSING if there is content associated, but not present. CONTENT_MISSING evaluates @@ -277,7 +267,7 @@ class HTTPRequest(HTTPMessage): timestamp_end=None, form_out=None ): - assert isinstance(headers, ODictCaseless) or not headers + assert isinstance(headers, odict.ODictCaseless) or not headers HTTPMessage.__init__( self, httpversion, @@ -315,7 +305,18 @@ class HTTPRequest(HTTPMessage): @classmethod def from_state(cls, state): - f = cls(None, None, None, None, None, None, None, None, None, None, None) + f = cls( + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None) f.load_state(state) return f @@ -325,78 +326,56 @@ class HTTPRequest(HTTPMessage): ) @classmethod - def from_stream(cls, rfile, include_body=True, body_size_limit=None): + def from_stream( + cls, + rfile, + include_body=True, + body_size_limit=None, + wfile=None): """ Parse an HTTP request from a file stream + + Args: + rfile (file): Input file to read from + include_body (bool): Read response body as well + body_size_limit (bool): Maximum body size + wfile (file): If specified, HTTP Expect headers are handled automatically. + by writing a HTTP 100 CONTINUE response to the stream. + + Returns: + HTTPRequest: The HTTP request + + Raises: + HttpError: If the input is invalid. """ - httpversion, host, port, scheme, method, path, headers, content, timestamp_start, timestamp_end = ( - None, None, None, None, None, None, None, None, None, None) + timestamp_start, timestamp_end = None, None timestamp_start = utils.timestamp() - if hasattr(rfile, "reset_timestamps"): rfile.reset_timestamps() - request_line = get_line(rfile) + req = http.read_request( + rfile, + include_body = include_body, + body_size_limit = body_size_limit, + wfile = wfile + ) if hasattr(rfile, "first_byte_timestamp"): # more accurate timestamp_start timestamp_start = rfile.first_byte_timestamp - request_line_parts = http.parse_init(request_line) - if not request_line_parts: - raise http.HttpError( - 400, - "Bad HTTP request line: %s" % repr(request_line) - ) - method, path, httpversion = request_line_parts - - if path == '*' or path.startswith("/"): - form_in = "relative" - if not netlib.utils.isascii(path): - raise http.HttpError( - 400, - "Bad HTTP request line: %s" % repr(request_line) - ) - elif method.upper() == 'CONNECT': - form_in = "authority" - r = http.parse_init_connect(request_line) - if not r: - raise http.HttpError( - 400, - "Bad HTTP request line: %s" % repr(request_line) - ) - host, port, _ = r - path = None - else: - form_in = "absolute" - r = http.parse_init_proxy(request_line) - if not r: - raise http.HttpError( - 400, - "Bad HTTP request line: %s" % repr(request_line) - ) - _, scheme, host, port, path, _ = r - - headers = http.read_headers(rfile) - if headers is None: - raise http.HttpError(400, "Invalid headers") - - if include_body: - content = http.read_http_body(rfile, headers, body_size_limit, - method, None, True) - timestamp_end = utils.timestamp() - + timestamp_end = utils.timestamp() return HTTPRequest( - form_in, - method, - scheme, - host, - port, - path, - httpversion, - headers, - content, + req.form_in, + req.method, + req.scheme, + req.host, + req.port, + req.path, + req.httpversion, + req.headers, + req.content, timestamp_start, timestamp_end ) @@ -440,11 +419,12 @@ class HTTPRequest(HTTPMessage): self.host, self.port)] - # If content is defined (i.e. not None or CONTENT_MISSING), we always add a content-length header. + # If content is defined (i.e. not None or CONTENT_MISSING), we always + # add a content-length header. if self.content or self.content == "": headers["Content-Length"] = [str(len(self.content))] - return str(headers) + return headers.format() def _assemble_head(self, form=None): return "%s\r\n%s\r\n" % ( @@ -497,9 +477,9 @@ class HTTPRequest(HTTPMessage): decode appropriately. """ if self.headers["accept-encoding"]: - self.headers["accept-encoding"] = [', '.join( - e for e in encoding.ENCODINGS if e in self.headers["accept-encoding"][0] - )] + self.headers["accept-encoding"] = [ + ', '.join( + e for e in encoding.ENCODINGS if e in self.headers["accept-encoding"][0])] def update_host_header(self): """ @@ -507,15 +487,42 @@ class HTTPRequest(HTTPMessage): """ self.headers["Host"] = [self.host] + def get_form(self): + """ + Retrieves the URL-encoded or multipart form data, returning an ODict object. + Returns an empty ODict if there is no data or the content-type + indicates non-form data. + """ + if self.content: + if self.headers.in_any("content-type", HDR_FORM_URLENCODED, True): + return self.get_form_urlencoded() + elif self.headers.in_any("content-type", HDR_FORM_MULTIPART, True): + return self.get_form_multipart() + return odict.ODict([]) + def get_form_urlencoded(self): """ Retrieves the URL-encoded form data, returning an ODict object. Returns an empty ODict if there is no data or the content-type indicates non-form data. """ - if self.content and self.headers.in_any("content-type", HDR_FORM_URLENCODED, True): - return ODict(utils.urldecode(self.content)) - return ODict([]) + if self.content and self.headers.in_any( + "content-type", + HDR_FORM_URLENCODED, + True): + return odict.ODict(utils.urldecode(self.content)) + return odict.ODict([]) + + def get_form_multipart(self): + if self.content and self.headers.in_any( + "content-type", + HDR_FORM_MULTIPART, + True): + return odict.ODict( + utils.multipartdecode( + self.headers, + self.content)) + return odict.ODict([]) def set_form_urlencoded(self, odict): """ @@ -556,8 +563,8 @@ class HTTPRequest(HTTPMessage): """ _, _, _, _, query, _ = urlparse.urlparse(self.url) if query: - return ODict(utils.urldecode(query)) - return ODict([]) + return odict.ODict(utils.urldecode(query)) + return odict.ODict([]) def set_query(self, odict): """ @@ -588,8 +595,10 @@ class HTTPRequest(HTTPMessage): host = self.headers.get_first("host") if not host: host = self.host - host = host.encode("idna") - return host + if host: + return host.encode("idna") + else: + return None def pretty_url(self, hostheader): if self.form_out == "authority": # upstream proxy mode @@ -625,15 +634,22 @@ class HTTPRequest(HTTPMessage): self.scheme, self.host, self.port, self.path = parts def get_cookies(self): - cookie_headers = self.headers.get("cookie") - if not cookie_headers: - return None + """ + + Returns a possibly empty netlib.odict.ODict object. + """ + ret = odict.ODict() + for i in self.headers["cookie"]: + ret.extend(http_cookies.parse_cookie_header(i)) + return ret - cookies = [] - for header in cookie_headers: - pairs = [pair.partition("=") for pair in header.split(';')] - cookies.extend((pair[0], (pair[2], {})) for pair in pairs) - return dict(cookies) + def set_cookies(self, odict): + """ + Takes an netlib.odict.ODict object. Over-writes any existing Cookie + headers. + """ + v = http_cookies.format_cookie_header(odict) + self.headers["Cookie"] = [v] def replace(self, pattern, repl, *args, **kwargs): """ @@ -674,9 +690,16 @@ class HTTPResponse(HTTPMessage): timestamp_end: Timestamp indicating when request transmission ended """ - def __init__(self, httpversion, code, msg, headers, content, timestamp_start=None, - timestamp_end=None): - assert isinstance(headers, ODictCaseless) or headers is None + def __init__( + self, + httpversion, + code, + msg, + headers, + content, + timestamp_start=None, + timestamp_end=None): + assert isinstance(headers, odict.ODictCaseless) or headers is None HTTPMessage.__init__( self, httpversion, @@ -706,7 +729,10 @@ class HTTPResponse(HTTPMessage): return f def __repr__(self): - size = utils.pretty_size(len(self.content)) if self.content else "content missing" + if self.content: + size = netlib.utils.pretty_size(len(self.content)) + else: + size = "content missing" return "<HTTPResponse: {code} {msg} ({contenttype}, {size})>".format( code=self.code, msg=self.msg, @@ -717,7 +743,12 @@ class HTTPResponse(HTTPMessage): ) @classmethod - def from_stream(cls, rfile, request_method, include_body=True, body_size_limit=None): + def from_stream( + cls, + rfile, + request_method, + include_body=True, + body_size_limit=None): """ Parse an HTTP response from a file stream """ @@ -767,11 +798,12 @@ class HTTPResponse(HTTPMessage): if not preserve_transfer_encoding: del headers['Transfer-Encoding'] - # If content is defined (i.e. not None or CONTENT_MISSING), we always add a content-length header. + # If content is defined (i.e. not None or CONTENT_MISSING), we always + # add a content-length header. if self.content or self.content == "": headers["Content-Length"] = [str(len(self.content))] - return str(headers) + return headers.format() def _assemble_head(self, preserve_transfer_encoding=False): return '%s\r\n%s\r\n' % ( @@ -850,20 +882,39 @@ class HTTPResponse(HTTPMessage): self.headers["set-cookie"] = c def get_cookies(self): - cookie_headers = self.headers.get("set-cookie") - if not cookie_headers: - return None + """ + Get the contents of all Set-Cookie headers. + + Returns a possibly empty ODict, where keys are cookie name strings, + and values are [value, attr] lists. Value is a string, and attr is + an ODictCaseless containing cookie attributes. Within attrs, unary + attributes (e.g. HTTPOnly) are indicated by a Null value. + """ + ret = [] + for header in self.headers["set-cookie"]: + v = http_cookies.parse_set_cookie_header(header) + if v: + name, value, attrs = v + ret.append([name, [value, attrs]]) + return odict.ODict(ret) + + def set_cookies(self, odict): + """ + Set the Set-Cookie headers on this response, over-writing existing + headers. - cookies = [] - for header in cookie_headers: - pairs = [pair.partition("=") for pair in header.split(';')] - cookie_name = pairs[0][0] # the key of the first key/value pairs - cookie_value = pairs[0][2] # the value of the first key/value pairs - cookie_parameters = { - key.strip().lower(): value.strip() for key, sep, value in pairs[1:] - } - cookies.append((cookie_name, (cookie_value, cookie_parameters))) - return dict(cookies) + Accepts an ODict of the same format as that returned by get_cookies. + """ + values = [] + for i in odict.lst: + values.append( + http_cookies.format_set_cookie_header( + i[0], + i[1][0], + i[1][1] + ) + ) + self.headers["Set-Cookie"] = values class HTTPFlow(Flow): @@ -996,7 +1047,7 @@ class HTTPHandler(ProtocolHandler): include_body=False ) break - except (tcp.NetLibError, http.HttpErrorConnClosed), v: + except (tcp.NetLibError, http.HttpErrorConnClosed) as v: self.c.log( "error in server communication: %s" % repr(v), level="debug" @@ -1041,7 +1092,8 @@ class HTTPHandler(ProtocolHandler): try: req = HTTPRequest.from_stream( self.c.client_conn.rfile, - body_size_limit=self.c.config.body_size_limit + body_size_limit=self.c.config.body_size_limit, + wfile=self.c.client_conn.wfile ) except tcp.NetLibError: # don't throw an error for disconnects that happen @@ -1066,7 +1118,8 @@ class HTTPHandler(ProtocolHandler): if request_reply is None or request_reply == KILL: raise KillSignal() - self.process_server_address(flow) # The inline script may have changed request.host + # The inline script may have changed request.host + self.process_server_address(flow) if isinstance(request_reply, HTTPResponse): flow.response = request_reply @@ -1077,7 +1130,9 @@ class HTTPHandler(ProtocolHandler): # we can safely set it as the final attribute value here. flow.server_conn = self.c.server_conn - self.c.log("response", "debug", [flow.response._assemble_first_line()]) + self.c.log( + "response", "debug", [ + flow.response._assemble_first_line()]) response_reply = self.c.channel.ask("response", flow) if response_reply is None or response_reply == KILL: raise KillSignal() @@ -1104,7 +1159,8 @@ class HTTPHandler(ProtocolHandler): } ) ) - if not self.process_connect_request((flow.request.host, flow.request.port)): + if not self.process_connect_request( + (flow.request.host, flow.request.port)): return False # If the user has changed the target server on this connection, @@ -1117,7 +1173,7 @@ class HTTPHandler(ProtocolHandler): http.HttpError, proxy.ProxyError, tcp.NetLibError, - ), e: + ) as e: self.handle_error(e, flow) except KillSignal: self.c.log("Connection killed", "info") @@ -1213,7 +1269,8 @@ class HTTPHandler(ProtocolHandler): # Determine .scheme, .host and .port attributes # For absolute-form requests, they are directly given in the request. # For authority-form requests, we only need to determine the request scheme. - # For relative-form requests, we need to determine host and port as well. + # For relative-form requests, we need to determine host and port as + # well. if not request.scheme: request.scheme = "https" if flow.server_conn and flow.server_conn.ssl_established else "http" if not request.host: @@ -1240,7 +1297,8 @@ class HTTPHandler(ProtocolHandler): flow.server_conn = self.c.server_conn self.c.establish_server_connection() self.c.client_conn.send( - 'HTTP/1.1 200 Connection established\r\n' + + ('HTTP/%s.%s 200 ' % (request.httpversion[0], request.httpversion[1])) + + 'Connection established\r\n' + 'Content-Length: 0\r\n' + ('Proxy-agent: %s\r\n' % self.c.config.server_version) + '\r\n' @@ -1304,7 +1362,8 @@ class HTTPHandler(ProtocolHandler): ) if needs_server_change: - # force create new connection to the proxy server to reset state + # force create new connection to the proxy server to reset + # state self.live.change_server(self.c.server_conn.address, force=True) if ssl: send_connect_request( @@ -1314,8 +1373,9 @@ class HTTPHandler(ProtocolHandler): ) self.c.establish_ssl(server=True) else: - # If we're not in upstream mode, we just want to update the host and - # possibly establish TLS. This is a no op if the addresses match. + # If we're not in upstream mode, we just want to update the host + # and possibly establish TLS. This is a no op if the addresses + # match. self.live.change_server(address, ssl=ssl) flow.server_conn = self.c.server_conn @@ -1323,8 +1383,8 @@ class HTTPHandler(ProtocolHandler): def send_response_to_client(self, flow): if not flow.response.stream: # no streaming: - # we already received the full response from the server and can send - # it to the client straight away. + # we already received the full response from the server and can + # send it to the client straight away. self.c.client_conn.send(flow.response.assemble()) else: # streaming: @@ -1356,14 +1416,21 @@ class HTTPHandler(ProtocolHandler): semantics. Returns True, if so. """ close_connection = ( - http.connection_close(flow.request.httpversion, flow.request.headers) or - http.connection_close(flow.response.httpversion, flow.response.headers) or - http.expected_http_body_size(flow.response.headers, False, flow.request.method, - flow.response.code) == -1) + http.connection_close( + flow.request.httpversion, + flow.request.headers) or http.connection_close( + flow.response.httpversion, + flow.response.headers) or http.expected_http_body_size( + flow.response.headers, + False, + flow.request.method, + flow.response.code) == -1) if close_connection: if flow.request.form_in == "authority" and flow.response.code == 200: - # Workaround for https://github.com/mitmproxy/mitmproxy/issues/313: - # Some proxies (e.g. Charles) send a CONNECT response with HTTP/1.0 and no Content-Length header + # Workaround for + # https://github.com/mitmproxy/mitmproxy/issues/313: Some + # proxies (e.g. Charles) send a CONNECT response with HTTP/1.0 + # and no Content-Length header pass else: return True @@ -1385,14 +1452,16 @@ class HTTPHandler(ProtocolHandler): self.expected_form_out = "relative" self.skip_authentication = True - # In practice, nobody issues a CONNECT request to send unencrypted HTTP requests afterwards. - # If we don't delegate to TCP mode, we should always negotiate a SSL connection. + # In practice, nobody issues a CONNECT request to send unencrypted + # HTTP requests afterwards. If we don't delegate to TCP mode, we + # should always negotiate a SSL connection. # - # FIXME: - # Turns out the previous statement isn't entirely true. Chrome on Windows CONNECTs to :80 - # if an explicit proxy is configured and a websocket connection should be established. - # We don't support websocket at the moment, so it fails anyway, but we should come up with - # a better solution to this if we start to support WebSockets. + # FIXME: Turns out the previous statement isn't entirely true. + # Chrome on Windows CONNECTs to :80 if an explicit proxy is + # configured and a websocket connection should be established. We + # don't support websocket at the moment, so it fails anyway, but we + # should come up with a better solution to this if we start to + # support WebSockets. should_establish_ssl = ( address.port in self.c.config.ssl_ports or @@ -1400,12 +1469,18 @@ class HTTPHandler(ProtocolHandler): ) if should_establish_ssl: - self.c.log("Received CONNECT request to SSL port. Upgrading to SSL...", "debug") + self.c.log( + "Received CONNECT request to SSL port. " + "Upgrading to SSL...", "debug" + ) self.c.establish_ssl(server=True, client=True) self.c.log("Upgrade to SSL completed.", "debug") if self.c.config.check_tcp(address): - self.c.log("Generic TCP mode for host: %s:%s" % address(), "info") + self.c.log( + "Generic TCP mode for host: %s:%s" % address(), + "info" + ) TCPHandler(self.c).handle_messages() return False @@ -1426,7 +1501,8 @@ class RequestReplayThread(threading.Thread): def __init__(self, config, flow, masterq, should_exit): """ - masterqueue can be a queue or None, if no scripthooks should be processed. + masterqueue can be a queue or None, if no scripthooks should be + processed. """ self.config, self.flow = config, flow if masterq: @@ -1452,12 +1528,17 @@ class RequestReplayThread(threading.Thread): if not self.flow.response: # In all modes, we directly connect to the server displayed if self.config.mode == "upstream": - server_address = self.config.mode.get_upstream_server(self.flow.client_conn)[2:] + server_address = self.config.mode.get_upstream_server( + self.flow.client_conn + )[2:] server = ServerConnection(server_address) server.connect() if r.scheme == "https": send_connect_request(server, r.host, r.port) - server.establish_ssl(self.config.clientcerts, sni=self.flow.server_conn.sni) + server.establish_ssl( + self.config.clientcerts, + sni=self.flow.server_conn.sni + ) r.form_out = "relative" else: r.form_out = "absolute" @@ -1466,12 +1547,18 @@ class RequestReplayThread(threading.Thread): server = ServerConnection(server_address) server.connect() if r.scheme == "https": - server.establish_ssl(self.config.clientcerts, sni=self.flow.server_conn.sni) + server.establish_ssl( + self.config.clientcerts, + sni=self.flow.server_conn.sni + ) r.form_out = "relative" server.send(r.assemble()) self.flow.server_conn = server - self.flow.response = HTTPResponse.from_stream(server.rfile, r.method, - body_size_limit=self.config.body_size_limit) + self.flow.response = HTTPResponse.from_stream( + server.rfile, + r.method, + body_size_limit=self.config.body_size_limit + ) if self.channel: response_reply = self.channel.ask("response", self.flow) if response_reply is None or response_reply == KILL: @@ -1481,7 +1568,8 @@ class RequestReplayThread(threading.Thread): if self.channel: self.channel.ask("error", self.flow) except KillSignal: - # KillSignal should only be raised if there's a channel in the first place. + # KillSignal should only be raised if there's a channel in the + # first place. self.channel.tell("log", proxy.Log("Connection killed", "info")) finally: r.form_out = form_out_backup diff --git a/libmproxy/protocol/primitives.py b/libmproxy/protocol/primitives.py index f9c22e1a..2f8ea3e0 100644 --- a/libmproxy/protocol/primitives.py +++ b/libmproxy/protocol/primitives.py @@ -24,6 +24,7 @@ class Error(stateobject.StateObject): msg: Message describing the error timestamp: Seconds since the epoch """ + def __init__(self, msg, timestamp=None): """ @type msg: str @@ -59,6 +60,7 @@ class Flow(stateobject.StateObject): A Flow is a collection of objects representing a single transaction. This class is usually subclassed for each protocol, e.g. HTTPFlow. """ + def __init__(self, type, client_conn, server_conn, live=None): self.type = type self.id = str(uuid.uuid4()) @@ -165,12 +167,12 @@ class Flow(stateobject.StateObject): master.handle_accept_intercept(self) - class ProtocolHandler(object): """ A ProtocolHandler implements an application-layer protocol, e.g. HTTP. See: libmproxy.protocol.http.HTTPHandler """ + def __init__(self, c): self.c = c """@type: libmproxy.proxy.server.ConnectionHandler""" @@ -209,13 +211,20 @@ class LiveConnection(object): interface with a live connection, without exposing the internals of the ConnectionHandler. """ + def __init__(self, c): self.c = c """@type: libmproxy.proxy.server.ConnectionHandler""" self._backup_server_conn = None """@type: libmproxy.proxy.connection.ServerConnection""" - def change_server(self, address, ssl=None, sni=None, force=False, persistent_change=False): + def change_server( + self, + address, + ssl=None, + sni=None, + force=False, + persistent_change=False): """ Change the server connection to the specified address. @returns: diff --git a/libmproxy/protocol/tcp.py b/libmproxy/protocol/tcp.py index 5314b577..0feb77c6 100644 --- a/libmproxy/protocol/tcp.py +++ b/libmproxy/protocol/tcp.py @@ -79,7 +79,8 @@ class TCPHandler(ProtocolHandler): ), "info" ) - # Do not use dst.connection.send here, which may raise OpenSSL-specific errors. + # Do not use dst.connection.send here, which may raise + # OpenSSL-specific errors. dst.send(contents) else: # socket.socket.send supports raw bytearrays/memoryviews diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index dfde2958..3f579669 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -81,16 +81,27 @@ class ProxyConfig: self.check_tcp = HostMatcher(tcp_hosts) self.authenticator = authenticator self.cadir = os.path.expanduser(cadir) - self.certstore = certutils.CertStore.from_store(self.cadir, CONF_BASENAME) + self.certstore = certutils.CertStore.from_store( + self.cadir, + CONF_BASENAME) for spec, cert in certs: self.certstore.add_cert_file(spec, cert) self.certforward = certforward - self.openssl_method_client, self.openssl_options_client = version_to_openssl(ssl_version_client) - self.openssl_method_server, self.openssl_options_server = version_to_openssl(ssl_version_server) + self.openssl_method_client, self.openssl_options_client = version_to_openssl( + ssl_version_client) + self.openssl_method_server, self.openssl_options_server = version_to_openssl( + ssl_version_server) self.ssl_ports = ssl_ports -sslversion_choices = ("all", "secure", "SSLv2", "SSLv3", "TLSv1", "TLSv1_1", "TLSv1_2") +sslversion_choices = ( + "all", + "secure", + "SSLv2", + "SSLv3", + "TLSv1", + "TLSv1_1", + "TLSv1_2") def version_to_openssl(version): @@ -119,7 +130,8 @@ def process_proxy_options(parser, options): if options.transparent_proxy: c += 1 if not platform.resolver: - return parser.error("Transparent mode not supported on this platform.") + return parser.error( + "Transparent mode not supported on this platform.") mode = "transparent" if options.socks_proxy: c += 1 @@ -133,28 +145,33 @@ def process_proxy_options(parser, options): mode = "upstream" upstream_server = options.upstream_proxy if c > 1: - return parser.error("Transparent, SOCKS5, reverse and upstream proxy mode " - "are mutually exclusive.") + return parser.error( + "Transparent, SOCKS5, reverse and upstream proxy mode " + "are mutually exclusive.") if options.clientcerts: options.clientcerts = os.path.expanduser(options.clientcerts) - if not os.path.exists(options.clientcerts) or not os.path.isdir(options.clientcerts): + if not os.path.exists( + options.clientcerts) or not os.path.isdir( + options.clientcerts): return parser.error( - "Client certificate directory does not exist or is not a directory: %s" % options.clientcerts - ) + "Client certificate directory does not exist or is not a directory: %s" % + options.clientcerts) if (options.auth_nonanonymous or options.auth_singleuser or options.auth_htpasswd): if options.auth_singleuser: if len(options.auth_singleuser.split(':')) != 2: - return parser.error("Invalid single-user specification. Please use the format username:password") + return parser.error( + "Invalid single-user specification. Please use the format username:password") username, password = options.auth_singleuser.split(':') password_manager = http_auth.PassManSingleUser(username, password) elif options.auth_nonanonymous: password_manager = http_auth.PassManNonAnon() elif options.auth_htpasswd: try: - password_manager = http_auth.PassManHtpasswd(options.auth_htpasswd) - except ValueError, v: + password_manager = http_auth.PassManHtpasswd( + options.auth_htpasswd) + except ValueError as v: return parser.error(v.message) authenticator = http_auth.BasicProxyAuth(password_manager, "mitmproxy") else: @@ -203,15 +220,18 @@ def process_proxy_options(parser, options): def ssl_option_group(parser): group = parser.add_argument_group("SSL") group.add_argument( - "--cert", dest='certs', default=[], type=str, - metavar="SPEC", action="append", + "--cert", + dest='certs', + default=[], + type=str, + metavar="SPEC", + action="append", help='Add an SSL certificate. SPEC is of the form "[domain=]path". ' - 'The domain may include a wildcard, and is equal to "*" if not specified. ' - 'The file at path is a certificate in PEM format. If a private key is included in the PEM, ' - 'it is used, else the default key in the conf dir is used. ' - 'The PEM file should contain the full certificate chain, with the leaf certificate as the first entry. ' - 'Can be passed multiple times.' - ) + 'The domain may include a wildcard, and is equal to "*" if not specified. ' + 'The file at path is a certificate in PEM format. If a private key is included in the PEM, ' + 'it is used, else the default key in the conf dir is used. ' + 'The PEM file should contain the full certificate chain, with the leaf certificate as the first entry. ' + 'Can be passed multiple times.') group.add_argument( "--cert-forward", action="store_true", dest="certforward", default=False, @@ -238,11 +258,15 @@ def ssl_option_group(parser): help="Don't connect to upstream server to look up certificate details." ) group.add_argument( - "--ssl-port", action="append", type=int, dest="ssl_ports", default=list(TRANSPARENT_SSL_PORTS), + "--ssl-port", + action="append", + type=int, + dest="ssl_ports", + default=list(TRANSPARENT_SSL_PORTS), metavar="PORT", help="Can be passed multiple times. Specify destination ports which are assumed to be SSL. " - "Defaults to %s." % str(TRANSPARENT_SSL_PORTS) - ) + "Defaults to %s." % + str(TRANSPARENT_SSL_PORTS)) group.add_argument( "--ssl-version-client", dest="ssl_version_client", default="secure", action="store", diff --git a/libmproxy/proxy/connection.py b/libmproxy/proxy/connection.py index 1eeae16f..5219023b 100644 --- a/libmproxy/proxy/connection.py +++ b/libmproxy/proxy/connection.py @@ -7,7 +7,9 @@ from .. import stateobject, utils class ClientConnection(tcp.BaseHandler, stateobject.StateObject): def __init__(self, client_connection, address, server): - if client_connection: # Eventually, this object is restored from state. We don't have a connection then. + # Eventually, this object is restored from state. We don't have a + # connection then. + if client_connection: tcp.BaseHandler.__init__(self, client_connection, address, server) else: self.connection = None @@ -39,15 +41,18 @@ class ClientConnection(tcp.BaseHandler, stateobject.StateObject): def get_state(self, short=False): d = super(ClientConnection, self).get_state(short) d.update( - address={"address": self.address(), "use_ipv6": self.address.use_ipv6}, - clientcert=self.cert.to_pem() if self.clientcert else None - ) + address={ + "address": self.address(), + "use_ipv6": self.address.use_ipv6}, + clientcert=self.cert.to_pem() if self.clientcert else None) return d def load_state(self, state): super(ClientConnection, self).load_state(state) - self.address = tcp.Address(**state["address"]) if state["address"] else None - self.clientcert = certutils.SSLCert.from_pem(state["clientcert"]) if state["clientcert"] else None + self.address = tcp.Address( + **state["address"]) if state["address"] else None + self.clientcert = certutils.SSLCert.from_pem( + state["clientcert"]) if state["clientcert"] else None def copy(self): return copy.copy(self) @@ -114,7 +119,7 @@ class ServerConnection(tcp.TCPClient, stateobject.StateObject): address={"address": self.address(), "use_ipv6": self.address.use_ipv6}, source_address= ({"address": self.source_address(), - "use_ipv6": self.source_address.use_ipv6} if self.source_address else None), + "use_ipv6": self.source_address.use_ipv6} if self.source_address else None), cert=self.cert.to_pem() if self.cert else None ) return d @@ -122,9 +127,12 @@ class ServerConnection(tcp.TCPClient, stateobject.StateObject): def load_state(self, state): super(ServerConnection, self).load_state(state) - self.address = tcp.Address(**state["address"]) if state["address"] else None - self.source_address = tcp.Address(**state["source_address"]) if state["source_address"] else None - self.cert = certutils.SSLCert.from_pem(state["cert"]) if state["cert"] else None + self.address = tcp.Address( + **state["address"]) if state["address"] else None + self.source_address = tcp.Address( + **state["source_address"]) if state["source_address"] else None + self.cert = certutils.SSLCert.from_pem( + state["cert"]) if state["cert"] else None @classmethod def from_state(cls, state): @@ -147,7 +155,9 @@ class ServerConnection(tcp.TCPClient, stateobject.StateObject): def establish_ssl(self, clientcerts, sni, **kwargs): clientcert = None if clientcerts: - path = os.path.join(clientcerts, self.address.host.encode("idna")) + ".pem" + path = os.path.join( + clientcerts, + self.address.host.encode("idna")) + ".pem" if os.path.exists(path): clientcert = path self.convert_to_ssl(cert=clientcert, sni=sni, **kwargs) diff --git a/libmproxy/proxy/primitives.py b/libmproxy/proxy/primitives.py index c0ae424d..9e7dae9a 100644 --- a/libmproxy/proxy/primitives.py +++ b/libmproxy/proxy/primitives.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from netlib import socks + class ProxyError(Exception): def __init__(self, code, message, headers=None): super(ProxyError, self).__init__(message) @@ -61,7 +62,7 @@ class TransparentProxyMode(ProxyMode): def get_upstream_server(self, client_conn): try: dst = self.resolver.original_addr(client_conn.connection) - except Exception, e: + except Exception as e: raise ProxyError(502, "Transparent mode failure: %s" % str(e)) if dst[1] in self.sslports: @@ -87,7 +88,9 @@ class Socks5ProxyMode(ProxyMode): guess = "" raise socks.SocksError( socks.REP.GENERAL_SOCKS_SERVER_FAILURE, - guess + "Invalid SOCKS version. Expected 0x05, got 0x%x" % msg.ver) + guess + + "Invalid SOCKS version. Expected 0x05, got 0x%x" % + msg.ver) def get_upstream_server(self, client_conn): try: @@ -117,13 +120,15 @@ class Socks5ProxyMode(ProxyMode): "mitmproxy only supports SOCKS5 CONNECT." ) - # We do not connect here yet, as the clientconnect event has not been handled yet. + # We do not connect here yet, as the clientconnect event has not + # been handled yet. connect_reply = socks.Message( socks.VERSION.SOCKS5, socks.REP.SUCCEEDED, socks.ATYP.DOMAINNAME, - client_conn.address # dummy value, we don't have an upstream connection yet. + # dummy value, we don't have an upstream connection yet. + client_conn.address ) connect_reply.to_file(client_conn.wfile) client_conn.wfile.flush() @@ -161,4 +166,4 @@ class UpstreamProxyMode(_ConstDestinationProxyMode): class Log: def __init__(self, msg, level="info"): self.msg = msg - self.level = level
\ No newline at end of file + self.level = level diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index a72f9aba..e1587df1 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -34,7 +34,7 @@ class ProxyServer(tcp.TCPServer): self.config = config try: tcp.TCPServer.__init__(self, (config.host, config.port)) - except socket.error, v: + except socket.error as v: raise ProxyServerError('Error starting proxy server: ' + repr(v)) self.channel = None @@ -46,16 +46,30 @@ class ProxyServer(tcp.TCPServer): self.channel = channel def handle_client_connection(self, conn, client_address): - h = ConnectionHandler(self.config, conn, client_address, self, self.channel) + h = ConnectionHandler( + self.config, + conn, + client_address, + self, + self.channel) h.handle() h.finish() class ConnectionHandler: - def __init__(self, config, client_connection, client_address, server, channel): + def __init__( + self, + config, + client_connection, + client_address, + server, + channel): self.config = config """@type: libmproxy.proxy.config.ProxyConfig""" - self.client_conn = ClientConnection(client_connection, client_address, server) + self.client_conn = ClientConnection( + client_connection, + client_address, + server) """@type: libmproxy.proxy.connection.ClientConnection""" self.server_conn = None """@type: libmproxy.proxy.connection.ServerConnection""" @@ -70,17 +84,23 @@ class ConnectionHandler: # Can we already identify the target server and connect to it? client_ssl, server_ssl = False, False conn_kwargs = dict() - upstream_info = self.config.mode.get_upstream_server(self.client_conn) + upstream_info = self.config.mode.get_upstream_server( + self.client_conn) if upstream_info: self.set_server_address(upstream_info[2:]) client_ssl, server_ssl = upstream_info[:2] if self.config.check_ignore(self.server_conn.address): - self.log("Ignore host: %s:%s" % self.server_conn.address(), "info") + self.log( + "Ignore host: %s:%s" % + self.server_conn.address(), + "info") self.conntype = "tcp" conn_kwargs["log"] = False client_ssl, server_ssl = False, False else: - pass # No upstream info from the metadata: upstream info in the protocol (e.g. HTTP absolute-form) + # No upstream info from the metadata: upstream info in the + # protocol (e.g. HTTP absolute-form) + pass self.channel.ask("clientconnect", self) @@ -92,11 +112,17 @@ class ConnectionHandler: self.establish_ssl(client=client_ssl, server=server_ssl) if self.config.check_tcp(self.server_conn.address): - self.log("Generic TCP mode for host: %s:%s" % self.server_conn.address(), "info") + self.log( + "Generic TCP mode for host: %s:%s" % + self.server_conn.address(), + "info") self.conntype = "tcp" # Delegate handling to the protocol handler - protocol_handler(self.conntype)(self, **conn_kwargs).handle_messages() + protocol_handler( + self.conntype)( + self, + **conn_kwargs).handle_messages() self.log("clientdisconnect", "info") self.channel.tell("clientdisconnect", self) @@ -104,7 +130,8 @@ class ConnectionHandler: except ProxyError as e: protocol_handler(self.conntype)(self, **conn_kwargs).handle_error(e) except Exception: - import traceback, sys + import traceback + import sys self.log(traceback.format_exc(), "error") print >> sys.stderr, traceback.format_exc() @@ -112,7 +139,8 @@ class ConnectionHandler: print >> sys.stderr, "Please lodge a bug report at: https://github.com/mitmproxy/mitmproxy" finally: # Make sure that we close the server connection in any case. - # The client connection is closed by the ProxyServer and does not have be handled here. + # The client connection is closed by the ProxyServer and does not + # have be handled here. self.del_server_connection() def del_server_connection(self): @@ -122,8 +150,10 @@ class ConnectionHandler: if self.server_conn and self.server_conn.connection: self.server_conn.finish() self.server_conn.close() - self.log("serverdisconnect", "debug", ["%s:%s" % (self.server_conn.address.host, - self.server_conn.address.port)]) + self.log( + "serverdisconnect", "debug", [ + "%s:%s" % + (self.server_conn.address.host, self.server_conn.address.port)]) self.channel.tell("serverdisconnect", self) self.server_conn = None @@ -141,7 +171,9 @@ class ConnectionHandler: if self.server_conn: self.del_server_connection() - self.log("Set new server address: %s:%s" % (address.host, address.port), "debug") + self.log( + "Set new server address: %s:%s" % + (address.host, address.port), "debug") self.server_conn = ServerConnection(address) def establish_server_connection(self, ask=True): @@ -155,12 +187,16 @@ class ConnectionHandler: """ if self.server_conn.connection: return - self.log("serverconnect", "debug", ["%s:%s" % self.server_conn.address()[:2]]) + self.log( + "serverconnect", "debug", [ + "%s:%s" % + self.server_conn.address()[ + :2]]) if ask: self.channel.ask("serverconnect", self) try: self.server_conn.connect() - except tcp.NetLibError, v: + except tcp.NetLibError as v: raise ProxyError(502, v) def establish_ssl(self, client=False, server=False, sni=None): @@ -237,7 +273,8 @@ class ConnectionHandler: self.server_conn.state = state # Receiving new_sni where had_ssl is False is a weird case that happens when the workaround for - # https://github.com/mitmproxy/mitmproxy/issues/427 is active. In this case, we want to establish SSL as well. + # https://github.com/mitmproxy/mitmproxy/issues/427 is active. In this + # case, we want to establish SSL as well. if had_ssl or new_sni: self.establish_ssl(server=True, sni=sni) @@ -246,8 +283,10 @@ class ConnectionHandler: def log(self, msg, level, subs=()): msg = [ - "%s:%s: %s" % (self.client_conn.address.host, self.client_conn.address.port, msg) - ] + "%s:%s: %s" % + (self.client_conn.address.host, + self.client_conn.address.port, + msg)] for i in subs: msg.append(" -> " + i) msg = "\n".join(msg) @@ -255,11 +294,13 @@ class ConnectionHandler: def find_cert(self): if self.config.certforward and self.server_conn.ssl_established: - return self.server_conn.cert, self.config.certstore.gen_pkey(self.server_conn.cert), None + return self.server_conn.cert, self.config.certstore.gen_pkey( + self.server_conn.cert), None else: host = self.server_conn.address.host sans = [] - if self.server_conn.ssl_established and (not self.config.no_upstream_cert): + if self.server_conn.ssl_established and ( + not self.config.no_upstream_cert): upstream_cert = self.server_conn.cert sans.extend(upstream_cert.altnames) if upstream_cert.cn: @@ -291,8 +332,11 @@ class ConnectionHandler: # - We established SSL with the server previously # - We initially wanted to establish SSL with the server, # but the server refused to negotiate without SNI. - if self.server_conn.ssl_established or hasattr(self.server_conn, "may_require_sni"): - self.server_reconnect(sni) # reconnect to upstream server with SNI + if self.server_conn.ssl_established or hasattr( + self.server_conn, + "may_require_sni"): + # reconnect to upstream server with SNI + self.server_reconnect(sni) # Now, change client context to reflect changed certificate: cert, key, chain_file = self.find_cert() new_context = self.client_conn.create_ssl_context( @@ -308,4 +352,7 @@ class ConnectionHandler: # make dang sure it doesn't happen. except: # pragma: no cover import traceback - self.log("Error in handle_sni:\r\n" + traceback.format_exc(), "error") + self.log( + "Error in handle_sni:\r\n" + + traceback.format_exc(), + "error") diff --git a/libmproxy/script.py b/libmproxy/script.py index be226004..4c550342 100644 --- a/libmproxy/script.py +++ b/libmproxy/script.py @@ -1,7 +1,11 @@ from __future__ import absolute_import -import os, traceback, threading, shlex +import os +import traceback +import threading +import shlex from . import controller + class ScriptError(Exception): pass @@ -56,6 +60,7 @@ class Script: s = Script(argv, master) s.load() """ + def __init__(self, command, master): self.command = command self.argv = self.parse_command(command) @@ -73,9 +78,11 @@ class Script: args = shlex.split(command) args[0] = os.path.expanduser(args[0]) if not os.path.exists(args[0]): - raise ScriptError(("Script file not found: %s.\r\n" - "If you script path contains spaces, " - "make sure to wrap it in additional quotes, e.g. -s \"'./foo bar/baz.py' --args\".") % args[0]) + raise ScriptError( + ("Script file not found: %s.\r\n" + "If you script path contains spaces, " + "make sure to wrap it in additional quotes, e.g. -s \"'./foo bar/baz.py' --args\".") % + args[0]) elif not os.path.isfile(args[0]): raise ScriptError("Not a file: %s" % args[0]) return args @@ -90,7 +97,7 @@ class Script: ns = {} try: execfile(self.argv[0], ns, ns) - except Exception, v: + except Exception as v: raise ScriptError(traceback.format_exc(v)) self.ns = ns r = self.run("start", self.argv) @@ -114,7 +121,7 @@ class Script: if f: try: return (True, f(self.ctx, *args, **kwargs)) - except Exception, v: + except Exception as v: return (False, (v, traceback.format_exc(v))) else: return (False, None) @@ -133,7 +140,7 @@ class ReplyProxy(object): return self.original_reply(*args, **kwargs) - def __getattr__ (self, k): + def __getattr__(self, k): return getattr(self.original_reply, k) @@ -145,7 +152,8 @@ def _handle_concurrent_reply(fn, o, *args, **kwargs): def run(): fn(*args, **kwargs) - reply_proxy() # If the script did not call .reply(), we have to do it now. + # If the script did not call .reply(), we have to do it now. + reply_proxy() ScriptThread(target=run).start() @@ -154,8 +162,15 @@ class ScriptThread(threading.Thread): def concurrent(fn): - if fn.func_name in ("request", "response", "error", "clientconnect", "serverconnect", "clientdisconnect"): + if fn.func_name in ( + "request", + "response", + "error", + "clientconnect", + "serverconnect", + "clientdisconnect"): def _concurrent(ctx, obj): _handle_concurrent_reply(fn, obj, ctx, obj) return _concurrent - raise NotImplementedError("Concurrent decorator not supported for this method.") + raise NotImplementedError( + "Concurrent decorator not supported for this method.") diff --git a/libmproxy/tnetstring.py b/libmproxy/tnetstring.py index 91b3ba2a..c5c185c6 100644 --- a/libmproxy/tnetstring.py +++ b/libmproxy/tnetstring.py @@ -72,13 +72,14 @@ __ver_major__ = 0 __ver_minor__ = 2 __ver_patch__ = 0 __ver_sub__ = "" -__version__ = "%d.%d.%d%s" % (__ver_major__,__ver_minor__,__ver_patch__,__ver_sub__) +__version__ = "%d.%d.%d%s" % ( + __ver_major__, __ver_minor__, __ver_patch__, __ver_sub__) from collections import deque -def dumps(value,encoding=None): +def dumps(value, encoding=None): """dumps(object,encoding=None) -> string This function dumps a python object as a tnetstring. @@ -90,21 +91,21 @@ def dumps(value,encoding=None): # consider the _gdumps() function instead; it's a standard top-down # generator that's simpler to understand but much less efficient. q = deque() - _rdumpq(q,0,value,encoding) + _rdumpq(q, 0, value, encoding) return "".join(q) -def dump(value,file,encoding=None): +def dump(value, file, encoding=None): """dump(object,file,encoding=None) This function dumps a python object as a tnetstring and writes it to the given file. """ - file.write(dumps(value,encoding)) + file.write(dumps(value, encoding)) file.flush() -def _rdumpq(q,size,value,encoding=None): +def _rdumpq(q, size, value, encoding=None): """Dump value as a tnetstring, to a deque instance, last chunks first. This function generates the tnetstring representation of the given value, @@ -129,7 +130,7 @@ def _rdumpq(q,size,value,encoding=None): if value is False: write("5:false!") return size + 8 - if isinstance(value,(int,long)): + if isinstance(value, (int, long)): data = str(value) ldata = len(data) span = str(ldata) @@ -138,7 +139,7 @@ def _rdumpq(q,size,value,encoding=None): write(":") write(span) return size + 2 + len(span) + ldata - if isinstance(value,(float,)): + if isinstance(value, (float,)): # Use repr() for float rather than str(). # It round-trips more accurately. # Probably unnecessary in later python versions that @@ -151,7 +152,7 @@ def _rdumpq(q,size,value,encoding=None): write(":") write(span) return size + 2 + len(span) + ldata - if isinstance(value,str): + if isinstance(value, str): lvalue = len(value) span = str(lvalue) write(",") @@ -159,26 +160,26 @@ def _rdumpq(q,size,value,encoding=None): write(":") write(span) return size + 2 + len(span) + lvalue - if isinstance(value,(list,tuple,)): + if isinstance(value, (list, tuple,)): write("]") init_size = size = size + 1 for item in reversed(value): - size = _rdumpq(q,size,item,encoding) + size = _rdumpq(q, size, item, encoding) span = str(size - init_size) write(":") write(span) return size + 1 + len(span) - if isinstance(value,dict): + if isinstance(value, dict): write("}") init_size = size = size + 1 - for (k,v) in value.iteritems(): - size = _rdumpq(q,size,v,encoding) - size = _rdumpq(q,size,k,encoding) + for (k, v) in value.iteritems(): + size = _rdumpq(q, size, v, encoding) + size = _rdumpq(q, size, k, encoding) span = str(size - init_size) write(":") write(span) return size + 1 + len(span) - if isinstance(value,unicode): + if isinstance(value, unicode): if encoding is None: raise ValueError("must specify encoding to dump unicode strings") value = value.encode(encoding) @@ -192,7 +193,7 @@ def _rdumpq(q,size,value,encoding=None): raise ValueError("unserializable object") -def _gdumps(value,encoding): +def _gdumps(value, encoding): """Generate fragments of value dumped as a tnetstring. This is the naive dumping algorithm, implemented as a generator so that @@ -207,24 +208,24 @@ def _gdumps(value,encoding): yield "4:true!" elif value is False: yield "5:false!" - elif isinstance(value,(int,long)): + elif isinstance(value, (int, long)): data = str(value) yield str(len(data)) yield ":" yield data yield "#" - elif isinstance(value,(float,)): + elif isinstance(value, (float,)): data = repr(value) yield str(len(data)) yield ":" yield data yield "^" - elif isinstance(value,(str,)): + elif isinstance(value, (str,)): yield str(len(value)) yield ":" yield value yield "," - elif isinstance(value,(list,tuple,)): + elif isinstance(value, (list, tuple,)): sub = [] for item in value: sub.extend(_gdumps(item)) @@ -233,9 +234,9 @@ def _gdumps(value,encoding): yield ":" yield sub yield "]" - elif isinstance(value,(dict,)): + elif isinstance(value, (dict,)): sub = [] - for (k,v) in value.iteritems(): + for (k, v) in value.iteritems(): sub.extend(_gdumps(k)) sub.extend(_gdumps(v)) sub = "".join(sub) @@ -243,7 +244,7 @@ def _gdumps(value,encoding): yield ":" yield sub yield "}" - elif isinstance(value,(unicode,)): + elif isinstance(value, (unicode,)): if encoding is None: raise ValueError("must specify encoding to dump unicode strings") value = value.encode(encoding) @@ -255,7 +256,7 @@ def _gdumps(value,encoding): raise ValueError("unserializable object") -def loads(string,encoding=None): +def loads(string, encoding=None): """loads(string,encoding=None) -> object This function parses a tnetstring into a python object. @@ -263,10 +264,10 @@ def loads(string,encoding=None): # No point duplicating effort here. In the C-extension version, # loads() is measurably faster then pop() since it can avoid # the overhead of building a second string. - return pop(string,encoding)[0] + return pop(string, encoding)[0] -def load(file,encoding=None): +def load(file, encoding=None): """load(file,encoding=None) -> object This function reads a tnetstring from a file and parses it into a @@ -324,21 +325,20 @@ def load(file,encoding=None): if type == "]": l = [] while data: - (item,data) = pop(data,encoding) + (item, data) = pop(data, encoding) l.append(item) return l if type == "}": d = {} while data: - (key,data) = pop(data,encoding) - (val,data) = pop(data,encoding) + (key, data) = pop(data, encoding) + (val, data) = pop(data, encoding) d[key] = val return d raise ValueError("unknown type tag") - -def pop(string,encoding=None): +def pop(string, encoding=None): """pop(string,encoding=None) -> (object, remain) This function parses a tnetstring into a python object. @@ -347,12 +347,12 @@ def pop(string,encoding=None): """ # Parse out data length, type and remaining string. try: - (dlen,rest) = string.split(":",1) + (dlen, rest) = string.split(":", 1) dlen = int(dlen) except ValueError: raise ValueError("not a tnetstring: missing or invalid length prefix") try: - (data,type,remain) = (rest[:dlen],rest[dlen],rest[dlen+1:]) + (data, type, remain) = (rest[:dlen], rest[dlen], rest[dlen + 1:]) except IndexError: # This fires if len(rest) < dlen, meaning we don't need # to further validate that data is the right length. @@ -360,40 +360,40 @@ def pop(string,encoding=None): # Parse the data based on the type tag. if type == ",": if encoding is not None: - return (data.decode(encoding),remain) - return (data,remain) + return (data.decode(encoding), remain) + return (data, remain) if type == "#": try: - return (int(data),remain) + return (int(data), remain) except ValueError: raise ValueError("not a tnetstring: invalid integer literal") if type == "^": try: - return (float(data),remain) + return (float(data), remain) except ValueError: raise ValueError("not a tnetstring: invalid float literal") if type == "!": if data == "true": - return (True,remain) + return (True, remain) elif data == "false": - return (False,remain) + return (False, remain) else: raise ValueError("not a tnetstring: invalid boolean literal") if type == "~": if data: raise ValueError("not a tnetstring: invalid null literal") - return (None,remain) + return (None, remain) if type == "]": l = [] while data: - (item,data) = pop(data,encoding) + (item, data) = pop(data, encoding) l.append(item) - return (l,remain) + return (l, remain) if type == "}": d = {} while data: - (key,data) = pop(data,encoding) - (val,data) = pop(data,encoding) + (key, data) = pop(data, encoding) + (val, data) = pop(data, encoding) d[key] = val - return (d,remain) + return (d, remain) raise ValueError("unknown type tag") diff --git a/libmproxy/utils.py b/libmproxy/utils.py index 51f2dc26..a29a53f5 100644 --- a/libmproxy/utils.py +++ b/libmproxy/utils.py @@ -1,8 +1,14 @@ from __future__ import absolute_import -import os, datetime, urllib, re -import time, functools, cgi +import os +import datetime +import urllib +import re +import time +import functools +import cgi import json + def timestamp(): """ Returns a serializable UTC timestamp. @@ -69,20 +75,33 @@ def urlencode(s): return urllib.urlencode(s, False) -def pretty_size(size): - suffixes = [ - ("B", 2**10), - ("kB", 2**20), - ("MB", 2**30), - ] - for suf, lim in suffixes: - if size >= lim: - continue - else: - x = round(size/float(lim/2**10), 2) - if x == int(x): - x = int(x) - return str(x) + suf +def multipartdecode(hdrs, content): + """ + Takes a multipart boundary encoded string and returns list of (key, value) tuples. + """ + v = hdrs.get_first("content-type") + if v: + v = parse_content_type(v) + if not v: + return [] + boundary = v[2].get("boundary") + if not boundary: + return [] + + rx = re.compile(r'\bname="([^"]+)"') + r = [] + + for i in content.split("--" + boundary): + parts = i.splitlines() + if len(parts) > 1 and parts[0][0:2] != "--": + match = rx.search(parts[1]) + if match: + key = match.group(1) + value = "".join(parts[3 + parts[2:].index(""):]) + r.append((key, value)) + return r + return [] + def pretty_duration(secs): formatters = [ @@ -94,8 +113,9 @@ def pretty_duration(secs): for limit, formatter in formatters: if secs >= limit: return formatter.format(secs) - #less than 1 sec - return "{:.0f}ms".format(secs*1000) + # less than 1 sec + return "{:.0f}ms".format(secs * 1000) + class Data: def __init__(self, name): @@ -112,47 +132,41 @@ class Data: """ fullpath = os.path.join(self.dirname, path) if not os.path.exists(fullpath): - raise ValueError, "dataPath: %s does not exist."%fullpath + raise ValueError("dataPath: %s does not exist." % fullpath) return fullpath pkg_data = Data(__name__) class LRUCache: """ - A decorator that implements a self-expiring LRU cache for class - methods (not functions!). - - Cache data is tracked as attributes on the object itself. There is - therefore a separate cache for each object instance. + A simple LRU cache for generated values. """ + def __init__(self, size=100): self.size = size + self.cache = {} + self.cacheList = [] + + def get(self, gen, *args): + """ + gen: A (presumably expensive) generator function. The identity of + gen is NOT taken into account by the cache. + *args: A list of immutable arguments, used to establish identiy by + *the cache, and passed to gen to generate values. + """ + if args in self.cache: + self.cacheList.remove(args) + self.cacheList.insert(0, args) + return self.cache[args] + else: + ret = gen(*args) + self.cacheList.insert(0, args) + self.cache[args] = ret + if len(self.cacheList) > self.size: + d = self.cacheList.pop() + self.cache.pop(d) + return ret - def __call__(self, f): - cacheName = "_cached_%s"%f.__name__ - cacheListName = "_cachelist_%s"%f.__name__ - size = self.size - - @functools.wraps(f) - def wrap(self, *args): - if not hasattr(self, cacheName): - setattr(self, cacheName, {}) - setattr(self, cacheListName, []) - cache = getattr(self, cacheName) - cacheList = getattr(self, cacheListName) - if cache.has_key(args): - cacheList.remove(args) - cacheList.insert(0, args) - return cache[args] - else: - ret = f(self, *args) - cacheList.insert(0, args) - cache[args] = ret - if len(cacheList) > size: - d = cacheList.pop() - cache.pop(d) - return ret - return wrap def parse_content_type(c): """ @@ -188,14 +202,14 @@ def hostport(scheme, host, port): if (port, scheme) in [(80, "http"), (443, "https")]: return host else: - return "%s:%s"%(host, port) + return "%s:%s" % (host, port) def unparse_url(scheme, host, port, path=""): """ Returns a URL string, constructed from the specified compnents. """ - return "%s://%s%s"%(scheme, hostport(scheme, host, port), path) + return "%s://%s%s" % (scheme, hostport(scheme, host, port), path) def clean_hanging_newline(t): @@ -236,7 +250,7 @@ def parse_size(s): try: return int(s) * mult except ValueError: - raise ValueError("Invalid size specification: %s"%s) + raise ValueError("Invalid size specification: %s" % s) def safe_subn(pattern, repl, target, *args, **kwargs): diff --git a/libmproxy/version.py b/libmproxy/version.py index babc5ec5..6d802202 100644 --- a/libmproxy/version.py +++ b/libmproxy/version.py @@ -1,4 +1,4 @@ -IVERSION = (0, 11, 4) +IVERSION = (0, 12, 1) VERSION = ".".join(str(i) for i in IVERSION) MINORVERSION = ".".join(str(i) for i in IVERSION[:2]) NAME = "mitmproxy" diff --git a/libmproxy/web/__init__.py b/libmproxy/web/__init__.py index 173ddf9f..a0af7315 100644 --- a/libmproxy/web/__init__.py +++ b/libmproxy/web/__init__.py @@ -79,6 +79,7 @@ class WebState(flow.State): data=[] ) + class Options(object): attributes = [ "app", @@ -128,11 +129,13 @@ class WebMaster(flow.FlowMaster): if options.rfile: try: self.load_flows_file(options.rfile) - except flow.FlowReadError, v: + except flow.FlowReadError as v: self.add_event( - "Could not read flow file: %s"%v, + "Could not read flow file: %s" % v, "error" ) + if self.options.app: + self.start_app(self.options.app_host, self.options.app_port) def tick(self): flow.FlowMaster.tick(self, self.masterq, timeout=0) @@ -154,7 +157,8 @@ class WebMaster(flow.FlowMaster): self.shutdown() def _process_flow(self, f): - if self.state.intercept and self.state.intercept(f) and not f.request.is_replay: + if self.state.intercept and self.state.intercept( + f) and not f.request.is_replay: f.intercept(self) else: f.reply() @@ -173,4 +177,4 @@ class WebMaster(flow.FlowMaster): def add_event(self, e, level="info"): super(WebMaster, self).add_event(e, level) - self.state.add_event(e, level)
\ No newline at end of file + self.state.add_event(e, level) diff --git a/libmproxy/web/app.py b/libmproxy/web/app.py index 2fc849f9..29ae9e7a 100644 --- a/libmproxy/web/app.py +++ b/libmproxy/web/app.py @@ -26,6 +26,13 @@ class RequestHandler(tornado.web.RequestHandler): ) @property + def json(self): + if not self.request.headers.get( + "Content-Type").startswith("application/json"): + return None + return json.loads(self.request.body) + + @property def state(self): return self.application.master.state @@ -61,8 +68,10 @@ class FiltHelp(RequestHandler): commands=filt.help )) + class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler): - connections = None # raise an error if inherited class doesn't specify its own instance. + # raise an error if inherited class doesn't specify its own instance. + connections = None def open(self): self.connections.add(self) @@ -111,6 +120,42 @@ class FlowHandler(RequestHandler): self.flow.kill(self.master) self.state.delete_flow(self.flow) + def put(self, flow_id): + flow = self.flow + flow.backup() + for a, b in self.json.iteritems(): + + if a == "request": + request = flow.request + for k, v in b.iteritems(): + if k in ["method", "scheme", "host", "path"]: + setattr(request, k, str(v)) + elif k == "port": + request.port = int(v) + elif k == "httpversion": + request.httpversion = tuple(int(x) for x in v) + elif k == "headers": + request.headers.load_state(v) + else: + print "Warning: Unknown update {}.{}: {}".format(a, k, v) + + elif a == "response": + response = flow.response + for k, v in b.iteritems(): + if k == "msg": + response.msg = str(v) + elif k == "code": + response.code = int(v) + elif k == "httpversion": + response.httpversion = tuple(int(x) for x in v) + elif k == "headers": + response.headers.load_state(v) + else: + print "Warning: Unknown update {}.{}: {}".format(a, k, v) + else: + print "Warning: Unknown update {}: {}".format(a, b) + self.state.update_flow(flow) + class DuplicateFlow(RequestHandler): def post(self, flow_id): @@ -124,6 +169,10 @@ class RevertFlow(RequestHandler): class ReplayFlow(RequestHandler): def post(self, flow_id): + self.flow.backup() + self.flow.response = None + self.state.update_flow(self.flow) + r = self.master.replay_request(self.flow) if r: raise APIError(400, r) @@ -176,18 +225,12 @@ class Settings(RequestHandler): ) )) - def put(self, *update, **kwargs): + def put(self): update = {} - for k, v in self.request.arguments.iteritems(): - if len(v) != 1: - print("Warning: Unknown length for setting {}: {}".format(k, v)) - continue - - if k == "_xsrf": - continue - elif k == "intercept": - self.state.set_intercept(v[0]) - update[k] = v[0] + for k, v in self.json.iteritems(): + if k == "intercept": + self.state.set_intercept(v) + update[k] = v else: print("Warning: Unknown setting {}: {}".format(k, v)) diff --git a/libmproxy/web/static/app.css b/libmproxy/web/static/app.css index 4f24ddd9..a7dc4f00 100644 --- a/libmproxy/web/static/app.css +++ b/libmproxy/web/static/app.css @@ -50,6 +50,7 @@ body, #container { display: flex; flex-direction: column; + outline: none; } #container > header, #container > footer, @@ -60,7 +61,6 @@ body, flex: 1 1 auto; display: flex; flex-direction: row; - outline: 0; } .main-view.vertical { flex-direction: column; @@ -145,7 +145,7 @@ header .menu { padding-left: 2.5px; padding-right: 2.5px; } -@media (min-width: 992px) { +@media (min-width: 768px) { .filter-input { float: left; width: 25%; @@ -155,6 +155,7 @@ header .menu { top: 27px; display: block; max-width: none; + opacity: 0.9; } .filter-input .popover .popover-content { max-height: 500px; @@ -271,7 +272,12 @@ header .menu { } .flow-detail { width: 100%; - overflow: auto; + overflow-x: auto; + overflow-y: scroll; + /*.request .response-line, + .response .request-line { + opacity: 0.7; + }*/ } .flow-detail nav { background-color: #F2F2F2; @@ -290,6 +296,25 @@ header .menu { max-height: 100px; overflow-y: auto; } +.flow-detail .request-line { + margin-bottom: 2px; +} +.flow-detail hr { + margin: 0 0 5px; +} +.inline-input { + margin: 0 -5px; + padding: 0 5px; +} +.inline-input[contenteditable] { + background-color: rgba(255, 255, 255, 0.2); +} +.inline-input[contenteditable].has-warning { + color: #ffb8b8; +} +.view-options { + margin-top: 10px; +} .flow-detail table { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; width: 100%; @@ -306,6 +331,9 @@ header .menu { width: 50%; padding-right: 1em; } +.header-table td { + line-height: 1.3em; +} .header-table .header-name { width: 33%; padding-right: 1em; @@ -316,6 +344,38 @@ header .menu { text-overflow: ellipsis; white-space: nowrap; } +.flowview-image { + text-align: center; +} +.flowview-image img { + max-width: 100%; + max-height: 100%; +} +.prompt-dialog { + top: 0; + bottom: 0; + left: 0; + right: 0; + position: fixed; + z-index: 100; + background-color: rgba(0, 0, 0, 0.1); +} +.prompt-content { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 25px; + padding: 2px 5px; + background-color: white; + box-shadow: 0 -1px 3px lightgray; +} +.prompt-content .option { + cursor: pointer; +} +.prompt-content .option:not(:last-child)::after { + content: ", "; +} .eventlog { height: 200px; flex: 0 0 auto; diff --git a/libmproxy/web/static/app.js b/libmproxy/web/static/app.js index dae10a34..b9767153 100644 --- a/libmproxy/web/static/app.js +++ b/libmproxy/web/static/app.js @@ -303,6 +303,8 @@ function isUndefined(arg) { },{}],2:[function(require,module,exports){ var $ = require("jquery"); +var _ = require("lodash"); +var AppDispatcher = require("./dispatcher.js").AppDispatcher; var ActionTypes = { // Connection @@ -347,7 +349,8 @@ var SettingsActions = { $.ajax({ type: "PUT", url: "/settings", - data: settings + contentType: 'application/json', + data: JSON.stringify(settings) }); /* @@ -398,11 +401,22 @@ var FlowActions = { revert: function(flow){ $.post("/flows/" + flow.id + "/revert"); }, - update: function (flow) { + update: function (flow, nextProps) { + /* + //Facebook Flux: We do an optimistic update on the client already. + var nextFlow = _.cloneDeep(flow); + _.merge(nextFlow, nextProps); AppDispatcher.dispatchViewAction({ type: ActionTypes.FLOW_STORE, cmd: StoreCmds.UPDATE, - data: flow + data: nextFlow + }); + */ + $.ajax({ + type: "PUT", + url: "/flows/" + flow.id, + contentType: 'application/json', + data: JSON.stringify(nextProps) }); }, clear: function(){ @@ -411,7 +425,7 @@ var FlowActions = { }; var Query = { - FILTER: "f", + SEARCH: "s", HIGHLIGHT: "h", SHOW_EVENTLOG: "e" }; @@ -421,20 +435,26 @@ module.exports = { ConnectionActions: ConnectionActions, FlowActions: FlowActions, StoreCmds: StoreCmds, + SettingsActions: SettingsActions, + EventLogActions: EventLogActions, Query: Query }; -},{"jquery":"jquery"}],3:[function(require,module,exports){ - +},{"./dispatcher.js":21,"jquery":"jquery","lodash":"lodash"}],3:[function(require,module,exports){ var React = require("react"); var ReactRouter = require("react-router"); var $ = require("jquery"); var Connection = require("./connection"); var proxyapp = require("./components/proxyapp.js"); +var EventLogActions = require("./actions.js").EventLogActions; $(function () { window.ws = new Connection("/updates"); + window.onerror = function (msg) { + EventLogActions.add_event(msg); + }; + ReactRouter.run(proxyapp.routes, function (Handler, state) { React.render(React.createElement(Handler, null), document.body); }); @@ -442,7 +462,7 @@ $(function () { -},{"./components/proxyapp.js":12,"./connection":14,"jquery":"jquery","react":"react","react-router":"react-router"}],4:[function(require,module,exports){ +},{"./actions.js":2,"./components/proxyapp.js":18,"./connection":20,"jquery":"jquery","react":"react","react-router":"react-router"}],4:[function(require,module,exports){ var React = require("react"); var ReactRouter = require("react-router"); var _ = require("lodash"); @@ -452,8 +472,8 @@ var AutoScrollMixin = { componentWillUpdate: function () { var node = this.getDOMNode(); this._shouldScrollBottom = ( - node.scrollTop !== 0 && - node.scrollTop + node.clientHeight === node.scrollHeight + node.scrollTop !== 0 && + node.scrollTop + node.clientHeight === node.scrollHeight ); }, componentDidUpdate: function () { @@ -474,34 +494,79 @@ var StickyHeadMixin = { } }; +var SettingsState = { + contextTypes: { + settingsStore: React.PropTypes.object.isRequired + }, + getInitialState: function () { + return { + settings: this.context.settingsStore.dict + }; + }, + componentDidMount: function () { + this.context.settingsStore.addListener("recalculate", this.onSettingsChange); + }, + componentWillUnmount: function () { + this.context.settingsStore.removeListener("recalculate", this.onSettingsChange); + }, + onSettingsChange: function () { + this.setState({ + settings: this.context.settingsStore.dict + }); + }, +}; + + +var ChildFocus = { + contextTypes: { + returnFocus: React.PropTypes.func + }, + returnFocus: function(){ + React.findDOMNode(this).blur(); + window.getSelection().removeAllRanges(); + this.context.returnFocus(); + } +}; + var Navigation = _.extend({}, ReactRouter.Navigation, { setQuery: function (dict) { - var q = this.context.getCurrentQuery(); - for(var i in dict){ - if(dict.hasOwnProperty(i)){ + var q = this.context.router.getCurrentQuery(); + for (var i in dict) { + if (dict.hasOwnProperty(i)) { q[i] = dict[i] || undefined; //falsey values shall be removed. } } - q._ = "_"; // workaround for https://github.com/rackt/react-router/pull/957 - this.replaceWith(this.context.getCurrentPath(), this.context.getCurrentParams(), q); + this.replaceWith(this.context.router.getCurrentPath(), this.context.router.getCurrentParams(), q); }, - replaceWith: function(routeNameOrPath, params, query) { - if(routeNameOrPath === undefined){ - routeNameOrPath = this.context.getCurrentPath(); + replaceWith: function (routeNameOrPath, params, query) { + if (routeNameOrPath === undefined) { + routeNameOrPath = this.context.router.getCurrentPath(); } - if(params === undefined){ - params = this.context.getCurrentParams(); + if (params === undefined) { + params = this.context.router.getCurrentParams(); } - if(query === undefined) { - query = this.context.getCurrentQuery(); + if (query === undefined) { + query = this.context.router.getCurrentQuery(); } - // FIXME: react-router is just broken. - ReactRouter.Navigation.replaceWith.call(this, routeNameOrPath, params, query); + this.context.router.replaceWith(routeNameOrPath, params, query); + } +}); + +// react-router is fairly good at changing its API regularly. +// We keep the old method for now - if it should turn out that their changes are permanent, +// we may remove this mixin and access react-router directly again. +var RouterState = _.extend({}, ReactRouter.State, { + getQuery: function () { + // For whatever reason, react-router always returns the same object, which makes comparing + // the current props with nextProps impossible. As a workaround, we just clone the query object. + return _.clone(this.context.router.getCurrentQuery()); + }, + getParams: function () { + return _.clone(this.context.router.getCurrentParams()); } }); -_.extend(Navigation.contextTypes, ReactRouter.State.contextTypes); var Splitter = React.createClass({displayName: "Splitter", getDefaultProps: function () { @@ -609,19 +674,218 @@ var Splitter = React.createClass({displayName: "Splitter", }); module.exports = { - State: ReactRouter.State, // keep here - react-router is pretty buggy, we may need workarounds in the future. + ChildFocus: ChildFocus, + RouterState: RouterState, Navigation: Navigation, StickyHeadMixin: StickyHeadMixin, AutoScrollMixin: AutoScrollMixin, - Splitter: Splitter + Splitter: Splitter, + SettingsState: SettingsState }; },{"lodash":"lodash","react":"react","react-router":"react-router"}],5:[function(require,module,exports){ var React = require("react"); var common = require("./common.js"); +var utils = require("../utils.js"); + +var contentToHtml = function (content) { + return _.escape(content); +}; +var nodeToContent = function (node) { + return node.textContent; +}; + +/* +Basic Editor Functionality + */ +var EditorBase = React.createClass({displayName: "EditorBase", + propTypes: { + content: React.PropTypes.string.isRequired, + onDone: React.PropTypes.func.isRequired, + contentToHtml: React.PropTypes.func, + nodeToContent: React.PropTypes.func, // content === nodeToContent( Node<innerHTML=contentToHtml(content)> ) + submitOnEnter: React.PropTypes.bool, + className: React.PropTypes.string, + tag: React.PropTypes.string + }, + getDefaultProps: function () { + return { + contentToHtml: contentToHtml, + nodeToContent: nodeToContent, + submitOnEnter: true, + className: "", + tag: "div" + }; + }, + getInitialState: function () { + return { + editable: false + }; + }, + render: function () { + var className = "inline-input " + this.props.className; + var html = {__html: this.props.contentToHtml(this.props.content)}; + var Tag = this.props.tag; + return React.createElement(Tag, React.__spread({}, + this.props, + {tabIndex: "0", + className: className, + contentEditable: this.state.editable || undefined, // workaround: use undef instead of false to remove attr + onFocus: this.onFocus, + onBlur: this._stop, + onKeyDown: this.onKeyDown, + onInput: this.onInput, + onPaste: this.onPaste, + dangerouslySetInnerHTML: html}) + ); + }, + onPaste: function(e){ + e.preventDefault(); + var content = e.clipboardData.getData("text/plain"); + document.execCommand("insertHTML", false, content); + }, + onFocus: function (e) { + this.setState({editable: true}, function () { + React.findDOMNode(this).focus(); + var range = document.createRange(); + range.selectNodeContents(this.getDOMNode()); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + }); + this.props.onFocus && this.props.onFocus(e); + }, + stop: function () { + // a stop would cause a blur as a side-effect. + // but a blur event must trigger a stop as well. + // to fix this, make stop = blur and do the actual stop in the onBlur handler. + React.findDOMNode(this).blur(); + }, + _stop: function (e) { + window.getSelection().removeAllRanges(); //make sure that selection is cleared on blur + var node = React.findDOMNode(this); + var content = this.props.nodeToContent(node); + this.setState({editable: false}); + this.props.onDone(content); + this.props.onBlur && this.props.onBlur(e); + }, + cancel: function () { + React.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content); + this.stop(); + }, + onKeyDown: function (e) { + e.stopPropagation(); + switch (e.keyCode) { + case utils.Key.ESC: + e.preventDefault(); + this.cancel(); + break; + case utils.Key.ENTER: + if (this.props.submitOnEnter && !e.shiftKey) { + e.preventDefault(); + this.stop(); + } + break; + default: + break; + } + }, + onInput: function () { + var node = React.findDOMNode(this); + var content = this.props.nodeToContent(node); + this.props.onInput && this.props.onInput(content); + } +}); + +/* +Add Validation to EditorBase + */ +var ValidateEditor = React.createClass({displayName: "ValidateEditor", + propTypes: { + content: React.PropTypes.string.isRequired, + onDone: React.PropTypes.func.isRequired, + onInput: React.PropTypes.func, + isValid: React.PropTypes.func, + className: React.PropTypes.string, + }, + getInitialState: function(){ + return { + currentContent: this.props.content + }; + }, + componentWillReceiveProps: function(){ + this.setState({currentContent: this.props.content}); + }, + onInput: function(content){ + this.setState({currentContent: content}); + this.props.onInput && this.props.onInput(content); + }, + render: function () { + var className = this.props.className || ""; + if (this.props.isValid) { + if (this.props.isValid(this.state.currentContent)) { + className += " has-success"; + } else { + className += " has-warning" + } + } + return React.createElement(EditorBase, React.__spread({}, + this.props, + {ref: "editor", + className: className, + onDone: this.onDone, + onInput: this.onInput}) + ); + }, + onDone: function (content) { + if(this.props.isValid && !this.props.isValid(content)){ + this.refs.editor.cancel(); + content = this.props.content; + } + this.props.onDone(content); + } +}); + +/* +Text Editor with mitmweb-specific convenience features + */ +var ValueEditor = React.createClass({displayName: "ValueEditor", + mixins: [common.ChildFocus], + propTypes: { + content: React.PropTypes.string.isRequired, + onDone: React.PropTypes.func.isRequired, + inline: React.PropTypes.bool, + }, + render: function () { + var tag = this.props.inline ? "span" : "div"; + return React.createElement(ValidateEditor, React.__spread({}, + this.props, + {onBlur: this.onBlur, + tag: tag}) + ); + }, + focus: function () { + React.findDOMNode(this).focus(); + }, + onBlur: function(e){ + if(!e.relatedTarget){ + this.returnFocus(); + } + this.props.onBlur && this.props.onBlur(e); + } +}); + +module.exports = { + ValueEditor: ValueEditor +}; + +},{"../utils.js":26,"./common.js":4,"react":"react"}],6:[function(require,module,exports){ +var React = require("react"); +var common = require("./common.js"); var Query = require("../actions.js").Query; var VirtualScrollMixin = require("./virtualscroll.js"); var views = require("../store/view.js"); +var _ = require("lodash"); var LogMessage = React.createClass({displayName: "LogMessage", render: function () { @@ -639,7 +903,7 @@ var LogMessage = React.createClass({displayName: "LogMessage", } return ( React.createElement("div", null, - indicator, " ", entry.message + indicator, " ", entry.message ) ); }, @@ -649,46 +913,36 @@ var LogMessage = React.createClass({displayName: "LogMessage", }); var EventLogContents = React.createClass({displayName: "EventLogContents", + contextTypes: { + eventStore: React.PropTypes.object.isRequired + }, mixins: [common.AutoScrollMixin, VirtualScrollMixin], getInitialState: function () { - return { - log: [] - }; - }, - componentWillMount: function () { - this.openView(this.props.eventStore); - }, - componentWillUnmount: function () { - this.closeView(); - }, - openView: function (store) { - var view = new views.StoreView(store, function (entry) { + var filterFn = function (entry) { return this.props.filter[entry.level]; - }.bind(this)); - this.setState({ - view: view - }); - + }; + var view = new views.StoreView(this.context.eventStore, filterFn.bind(this)); view.addListener("add", this.onEventLogChange); view.addListener("recalculate", this.onEventLogChange); + + return { + view: view + }; }, - closeView: function () { + componentWillUnmount: function () { this.state.view.close(); }, + filter: function (entry) { + return this.props.filter[entry.level]; + }, onEventLogChange: function () { - this.setState({ - log: this.state.view.list - }); + this.forceUpdate(); }, componentWillReceiveProps: function (nextProps) { if (nextProps.filter !== this.props.filter) { this.props.filter = nextProps.filter; // Dirty: Make sure that view filter sees the update. this.state.view.recalculate(); } - if (nextProps.eventStore !== this.props.eventStore) { - this.closeView(); - this.openView(nextProps.eventStore); - } }, getDefaultProps: function () { return { @@ -701,12 +955,13 @@ var EventLogContents = React.createClass({displayName: "EventLogContents", return React.createElement(LogMessage, {key: elem.id, entry: elem}); }, render: function () { - var rows = this.renderRows(this.state.log); + var entries = this.state.view.list; + var rows = this.renderRows(entries); return React.createElement("pre", {onScroll: this.onScroll}, - this.getPlaceholderTop(this.state.log.length), + this.getPlaceholderTop(entries.length), rows, - this.getPlaceholderBottom(this.state.log.length) + this.getPlaceholderBottom(entries.length) ); } }); @@ -767,7 +1022,7 @@ var EventLog = React.createClass({displayName: "EventLog", ) ), - React.createElement(EventLogContents, {filter: this.state.filter, eventStore: this.props.eventStore}) + React.createElement(EventLogContents, {filter: this.state.filter}) ) ); } @@ -775,408 +1030,7 @@ var EventLog = React.createClass({displayName: "EventLog", module.exports = EventLog; -},{"../actions.js":2,"../store/view.js":19,"./common.js":4,"./virtualscroll.js":13,"react":"react"}],6:[function(require,module,exports){ -var React = require("react"); -var _ = require("lodash"); - -var common = require("./common.js"); -var actions = require("../actions.js"); -var flowutils = require("../flow/utils.js"); -var toputils = require("../utils.js"); - -var NavAction = React.createClass({displayName: "NavAction", - onClick: function (e) { - e.preventDefault(); - this.props.onClick(); - }, - render: function () { - return ( - React.createElement("a", {title: this.props.title, - href: "#", - className: "nav-action", - onClick: this.onClick}, - React.createElement("i", {className: "fa fa-fw " + this.props.icon}) - ) - ); - } -}); - -var FlowDetailNav = React.createClass({displayName: "FlowDetailNav", - render: function () { - var flow = this.props.flow; - - var tabs = this.props.tabs.map(function (e) { - var str = e.charAt(0).toUpperCase() + e.slice(1); - var className = this.props.active === e ? "active" : ""; - var onClick = function (event) { - this.props.selectTab(e); - event.preventDefault(); - }.bind(this); - return React.createElement("a", {key: e, - href: "#", - className: className, - onClick: onClick}, str); - }.bind(this)); - - var acceptButton = null; - if(flow.intercepted){ - acceptButton = React.createElement(NavAction, {title: "[a]ccept intercepted flow", icon: "fa-play", onClick: actions.FlowActions.accept.bind(null, flow)}); - } - var revertButton = null; - if(flow.modified){ - revertButton = React.createElement(NavAction, {title: "revert changes to flow [V]", icon: "fa-history", onClick: actions.FlowActions.revert.bind(null, flow)}); - } - - return ( - React.createElement("nav", {ref: "head", className: "nav-tabs nav-tabs-sm"}, - tabs, - React.createElement(NavAction, {title: "[d]elete flow", icon: "fa-trash", onClick: actions.FlowActions.delete.bind(null, flow)}), - React.createElement(NavAction, {title: "[D]uplicate flow", icon: "fa-copy", onClick: actions.FlowActions.duplicate.bind(null, flow)}), - React.createElement(NavAction, {disabled: true, title: "[r]eplay flow", icon: "fa-repeat", onClick: actions.FlowActions.replay.bind(null, flow)}), - acceptButton, - revertButton - ) - ); - } -}); - -var Headers = React.createClass({displayName: "Headers", - render: function () { - var rows = this.props.message.headers.map(function (header, i) { - return ( - React.createElement("tr", {key: i}, - React.createElement("td", {className: "header-name"}, header[0] + ":"), - React.createElement("td", {className: "header-value"}, header[1]) - ) - ); - }); - return ( - React.createElement("table", {className: "header-table"}, - React.createElement("tbody", null, - rows - ) - ) - ); - } -}); - -var FlowDetailRequest = React.createClass({displayName: "FlowDetailRequest", - render: function () { - var flow = this.props.flow; - var first_line = [ - flow.request.method, - flowutils.RequestUtils.pretty_url(flow.request), - "HTTP/" + flow.request.httpversion.join(".") - ].join(" "); - var content = null; - if (flow.request.contentLength > 0) { - content = "Request Content Size: " + toputils.formatSize(flow.request.contentLength); - } else { - content = React.createElement("div", {className: "alert alert-info"}, "No Content"); - } - - //TODO: Styling - - return ( - React.createElement("section", null, - React.createElement("div", {className: "first-line"}, first_line ), - React.createElement(Headers, {message: flow.request}), - React.createElement("hr", null), - content - ) - ); - } -}); - -var FlowDetailResponse = React.createClass({displayName: "FlowDetailResponse", - render: function () { - var flow = this.props.flow; - var first_line = [ - "HTTP/" + flow.response.httpversion.join("."), - flow.response.code, - flow.response.msg - ].join(" "); - var content = null; - if (flow.response.contentLength > 0) { - content = "Response Content Size: " + toputils.formatSize(flow.response.contentLength); - } else { - content = React.createElement("div", {className: "alert alert-info"}, "No Content"); - } - - //TODO: Styling - - return ( - React.createElement("section", null, - React.createElement("div", {className: "first-line"}, first_line ), - React.createElement(Headers, {message: flow.response}), - React.createElement("hr", null), - content - ) - ); - } -}); - -var FlowDetailError = React.createClass({displayName: "FlowDetailError", - render: function () { - var flow = this.props.flow; - return ( - React.createElement("section", null, - React.createElement("div", {className: "alert alert-warning"}, - flow.error.msg, - React.createElement("div", null, - React.createElement("small", null, toputils.formatTimeStamp(flow.error.timestamp) ) - ) - ) - ) - ); - } -}); - -var TimeStamp = React.createClass({displayName: "TimeStamp", - render: function () { - - if (!this.props.t) { - //should be return null, but that triggers a React bug. - return React.createElement("tr", null); - } - - var ts = toputils.formatTimeStamp(this.props.t); - - var delta; - if (this.props.deltaTo) { - delta = toputils.formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); - delta = React.createElement("span", {className: "text-muted"}, "(" + delta + ")"); - } else { - delta = null; - } - - return React.createElement("tr", null, - React.createElement("td", null, this.props.title + ":"), - React.createElement("td", null, ts, " ", delta) - ); - } -}); - -var ConnectionInfo = React.createClass({displayName: "ConnectionInfo", - - render: function () { - var conn = this.props.conn; - var address = conn.address.address.join(":"); - - var sni = React.createElement("tr", {key: "sni"}); //should be null, but that triggers a React bug. - if (conn.sni) { - sni = React.createElement("tr", {key: "sni"}, - React.createElement("td", null, - React.createElement("abbr", {title: "TLS Server Name Indication"}, "TLS SNI:") - ), - React.createElement("td", null, conn.sni) - ); - } - return ( - React.createElement("table", {className: "connection-table"}, - React.createElement("tbody", null, - React.createElement("tr", {key: "address"}, - React.createElement("td", null, "Address:"), - React.createElement("td", null, address) - ), - sni - ) - ) - ); - } -}); - -var CertificateInfo = React.createClass({displayName: "CertificateInfo", - render: function () { - //TODO: We should fetch human-readable certificate representation - // from the server - var flow = this.props.flow; - var client_conn = flow.client_conn; - var server_conn = flow.server_conn; - - var preStyle = {maxHeight: 100}; - return ( - React.createElement("div", null, - client_conn.cert ? React.createElement("h4", null, "Client Certificate") : null, - client_conn.cert ? React.createElement("pre", {style: preStyle}, client_conn.cert) : null, - - server_conn.cert ? React.createElement("h4", null, "Server Certificate") : null, - server_conn.cert ? React.createElement("pre", {style: preStyle}, server_conn.cert) : null - ) - ); - } -}); - -var Timing = React.createClass({displayName: "Timing", - render: function () { - var flow = this.props.flow; - var sc = flow.server_conn; - var cc = flow.client_conn; - var req = flow.request; - var resp = flow.response; - - var timestamps = [ - { - title: "Server conn. initiated", - t: sc.timestamp_start, - deltaTo: req.timestamp_start - }, { - title: "Server conn. TCP handshake", - t: sc.timestamp_tcp_setup, - deltaTo: req.timestamp_start - }, { - title: "Server conn. SSL handshake", - t: sc.timestamp_ssl_setup, - deltaTo: req.timestamp_start - }, { - title: "Client conn. established", - t: cc.timestamp_start, - deltaTo: req.timestamp_start - }, { - title: "Client conn. SSL handshake", - t: cc.timestamp_ssl_setup, - deltaTo: req.timestamp_start - }, { - title: "First request byte", - t: req.timestamp_start, - }, { - title: "Request complete", - t: req.timestamp_end, - deltaTo: req.timestamp_start - } - ]; - - if (flow.response) { - timestamps.push( - { - title: "First response byte", - t: resp.timestamp_start, - deltaTo: req.timestamp_start - }, { - title: "Response complete", - t: resp.timestamp_end, - deltaTo: req.timestamp_start - } - ); - } - - //Add unique key for each row. - timestamps.forEach(function (e) { - e.key = e.title; - }); - - timestamps = _.sortBy(timestamps, 't'); - - var rows = timestamps.map(function (e) { - return React.createElement(TimeStamp, React.__spread({}, e)); - }); - - return ( - React.createElement("div", null, - React.createElement("h4", null, "Timing"), - React.createElement("table", {className: "timing-table"}, - React.createElement("tbody", null, - rows - ) - ) - ) - ); - } -}); - -var FlowDetailConnectionInfo = React.createClass({displayName: "FlowDetailConnectionInfo", - render: function () { - var flow = this.props.flow; - var client_conn = flow.client_conn; - var server_conn = flow.server_conn; - return ( - React.createElement("section", null, - - React.createElement("h4", null, "Client Connection"), - React.createElement(ConnectionInfo, {conn: client_conn}), - - React.createElement("h4", null, "Server Connection"), - React.createElement(ConnectionInfo, {conn: server_conn}), - - React.createElement(CertificateInfo, {flow: flow}), - - React.createElement(Timing, {flow: flow}) - - ) - ); - } -}); - -var allTabs = { - request: FlowDetailRequest, - response: FlowDetailResponse, - error: FlowDetailError, - details: FlowDetailConnectionInfo -}; - -var FlowDetail = React.createClass({displayName: "FlowDetail", - mixins: [common.StickyHeadMixin, common.Navigation, common.State], - getTabs: function (flow) { - var tabs = []; - ["request", "response", "error"].forEach(function (e) { - if (flow[e]) { - tabs.push(e); - } - }); - tabs.push("details"); - return tabs; - }, - nextTab: function (i) { - var tabs = this.getTabs(this.props.flow); - var currentIndex = tabs.indexOf(this.getParams().detailTab); - // JS modulo operator doesn't correct negative numbers, make sure that we are positive. - var nextIndex = (currentIndex + i + tabs.length) % tabs.length; - this.selectTab(tabs[nextIndex]); - }, - selectTab: function (panel) { - this.replaceWith( - "flow", - { - flowId: this.getParams().flowId, - detailTab: panel - } - ); - }, - render: function () { - var flow = this.props.flow; - var tabs = this.getTabs(flow); - var active = this.getParams().detailTab; - - if (!_.contains(tabs, active)) { - if (active === "response" && flow.error) { - active = "error"; - } else if (active === "error" && flow.response) { - active = "response"; - } else { - active = tabs[0]; - } - this.selectTab(active); - } - - var Tab = allTabs[active]; - return ( - React.createElement("div", {className: "flow-detail", onScroll: this.adjustHead}, - React.createElement(FlowDetailNav, {ref: "head", - flow: flow, - tabs: tabs, - active: active, - selectTab: this.selectTab}), - React.createElement(Tab, {flow: flow}) - ) - ); - } -}); - -module.exports = { - FlowDetail: FlowDetail -}; - -},{"../actions.js":2,"../flow/utils.js":17,"../utils.js":20,"./common.js":4,"lodash":"lodash","react":"react"}],7:[function(require,module,exports){ +},{"../actions.js":2,"../store/view.js":25,"./common.js":4,"./virtualscroll.js":19,"lodash":"lodash","react":"react"}],7:[function(require,module,exports){ var React = require("react"); var RequestUtils = require("../flow/utils.js").RequestUtils; var ResponseUtils = require("../flow/utils.js").ResponseUtils; @@ -1195,7 +1049,7 @@ var TLSColumn = React.createClass({displayName: "TLSColumn", }, render: function () { var flow = this.props.flow; - var ssl = (flow.request.scheme == "https"); + var ssl = (flow.request.scheme === "https"); var classes; if (ssl) { classes = "col-tls col-tls-https"; @@ -1223,7 +1077,7 @@ var IconColumn = React.createClass({displayName: "IconColumn", var contentType = ResponseUtils.getContentType(flow.response); //TODO: We should assign a type to the flow somewhere else. - if (flow.response.code == 304) { + if (flow.response.code === 304) { icon = "resource-icon-not-modified"; } else if (300 <= flow.response.code && flow.response.code < 400) { icon = "resource-icon-redirect"; @@ -1379,7 +1233,7 @@ var all_columns = [ module.exports = all_columns; -},{"../flow/utils.js":17,"../utils.js":20,"react":"react"}],8:[function(require,module,exports){ +},{"../flow/utils.js":23,"../utils.js":26,"react":"react"}],8:[function(require,module,exports){ var React = require("react"); var common = require("./common.js"); var utils = require("../utils.js"); @@ -1490,33 +1344,25 @@ var ROW_HEIGHT = 32; var FlowTable = React.createClass({displayName: "FlowTable", mixins: [common.StickyHeadMixin, common.AutoScrollMixin, VirtualScrollMixin], + contextTypes: { + view: React.PropTypes.object.isRequired + }, getInitialState: function () { return { columns: flowtable_columns }; }, - _listen: function(view){ - if(!view){ - return; - } - view.addListener("add", this.onChange); - view.addListener("update", this.onChange); - view.addListener("remove", this.onChange); - view.addListener("recalculate", this.onChange); - }, componentWillMount: function () { - this._listen(this.props.view); + this.context.view.addListener("add", this.onChange); + this.context.view.addListener("update", this.onChange); + this.context.view.addListener("remove", this.onChange); + this.context.view.addListener("recalculate", this.onChange); }, - componentWillReceiveProps: function (nextProps) { - if (nextProps.view !== this.props.view) { - if (this.props.view) { - this.props.view.removeListener("add"); - this.props.view.removeListener("update"); - this.props.view.removeListener("remove"); - this.props.view.removeListener("recalculate"); - } - this._listen(nextProps.view); - } + componentWillUnmount: function(){ + this.context.view.removeListener("add", this.onChange); + this.context.view.removeListener("update", this.onChange); + this.context.view.removeListener("remove", this.onChange); + this.context.view.removeListener("recalculate", this.onChange); }, getDefaultProps: function () { return { @@ -1532,7 +1378,7 @@ var FlowTable = React.createClass({displayName: "FlowTable", }, scrollIntoView: function (flow) { this.scrollRowIntoView( - this.props.view.index(flow), + this.context.view.index(flow), this.refs.body.getDOMNode().offsetTop ); }, @@ -1540,8 +1386,8 @@ var FlowTable = React.createClass({displayName: "FlowTable", var selected = (flow === this.props.selected); var highlighted = ( - this.props.view._highlight && - this.props.view._highlight[flow.id] + this.context.view._highlight && + this.context.view._highlight[flow.id] ); return React.createElement(FlowRow, {key: flow.id, @@ -1554,9 +1400,7 @@ var FlowTable = React.createClass({displayName: "FlowTable", ); }, render: function () { - //console.log("render flowtable", this.state.start, this.state.stop, this.props.selected); - var flows = this.props.view ? this.props.view.list : []; - + var flows = this.context.view.list; var rows = this.renderRows(flows); return ( @@ -1579,16 +1423,960 @@ var FlowTable = React.createClass({displayName: "FlowTable", module.exports = FlowTable; -},{"../utils.js":20,"./common.js":4,"./flowtable-columns.js":7,"./virtualscroll.js":13,"lodash":"lodash","react":"react"}],9:[function(require,module,exports){ +},{"../utils.js":26,"./common.js":4,"./flowtable-columns.js":7,"./virtualscroll.js":19,"lodash":"lodash","react":"react"}],9:[function(require,module,exports){ +var React = require("react"); +var _ = require("lodash"); + +var MessageUtils = require("../../flow/utils.js").MessageUtils; +var utils = require("../../utils.js"); + +var image_regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i; +var ViewImage = React.createClass({displayName: "ViewImage", + statics: { + matches: function (message) { + return image_regex.test(MessageUtils.getContentType(message)); + } + }, + render: function () { + var url = MessageUtils.getContentURL(this.props.flow, this.props.message); + return React.createElement("div", {className: "flowview-image"}, + React.createElement("img", {src: url, alt: "preview", className: "img-thumbnail"}) + ); + } +}); + +var RawMixin = { + getInitialState: function () { + return { + content: undefined, + request: undefined + } + }, + requestContent: function (nextProps) { + if (this.state.request) { + this.state.request.abort(); + } + var request = MessageUtils.getContent(nextProps.flow, nextProps.message); + this.setState({ + content: undefined, + request: request + }); + request.done(function (data) { + this.setState({content: data}); + }.bind(this)).fail(function (jqXHR, textStatus, errorThrown) { + if (textStatus === "abort") { + return; + } + this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown}); + }.bind(this)).always(function () { + this.setState({request: undefined}); + }.bind(this)); + + }, + componentWillMount: function () { + this.requestContent(this.props); + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.message !== this.props.message) { + this.requestContent(nextProps); + } + }, + componentWillUnmount: function () { + if (this.state.request) { + this.state.request.abort(); + } + }, + render: function () { + if (!this.state.content) { + return React.createElement("div", {className: "text-center"}, + React.createElement("i", {className: "fa fa-spinner fa-spin"}) + ); + } + return this.renderContent(); + } +}; + +var ViewRaw = React.createClass({displayName: "ViewRaw", + mixins: [RawMixin], + statics: { + matches: function (message) { + return true; + } + }, + renderContent: function () { + return React.createElement("pre", null, this.state.content); + } +}); + +var json_regex = /^application\/json$/i; +var ViewJSON = React.createClass({displayName: "ViewJSON", + mixins: [RawMixin], + statics: { + matches: function (message) { + return json_regex.test(MessageUtils.getContentType(message)); + } + }, + renderContent: function () { + var json = this.state.content; + try { + json = JSON.stringify(JSON.parse(json), null, 2); + } catch (e) { + } + return React.createElement("pre", null, json); + } +}); + +var ViewAuto = React.createClass({displayName: "ViewAuto", + statics: { + matches: function () { + return false; // don't match itself + }, + findView: function (message) { + for (var i = 0; i < all.length; i++) { + if (all[i].matches(message)) { + return all[i]; + } + } + return all[all.length - 1]; + } + }, + render: function () { + var View = ViewAuto.findView(this.props.message); + return React.createElement(View, React.__spread({}, this.props)); + } +}); + +var all = [ViewAuto, ViewImage, ViewJSON, ViewRaw]; + + +var ContentEmpty = React.createClass({displayName: "ContentEmpty", + render: function () { + var message_name = this.props.flow.request === this.props.message ? "request" : "response"; + return React.createElement("div", {className: "alert alert-info"}, "No ", message_name, " content."); + } +}); + +var ContentMissing = React.createClass({displayName: "ContentMissing", + render: function () { + var message_name = this.props.flow.request === this.props.message ? "Request" : "Response"; + return React.createElement("div", {className: "alert alert-info"}, message_name, " content missing."); + } +}); + +var TooLarge = React.createClass({displayName: "TooLarge", + statics: { + isTooLarge: function (message) { + var max_mb = ViewImage.matches(message) ? 10 : 0.2; + return message.contentLength > 1024 * 1024 * max_mb; + } + }, + render: function () { + var size = utils.formatSize(this.props.message.contentLength); + return React.createElement("div", {className: "alert alert-warning"}, + React.createElement("button", {onClick: this.props.onClick, className: "btn btn-xs btn-warning pull-right"}, "Display anyway"), + size, " content size." + ); + } +}); + +var ViewSelector = React.createClass({displayName: "ViewSelector", + render: function () { + var views = []; + for (var i = 0; i < all.length; i++) { + var view = all[i]; + var className = "btn btn-default"; + if (view === this.props.active) { + className += " active"; + } + var text; + if (view === ViewAuto) { + text = "auto: " + ViewAuto.findView(this.props.message).displayName.toLowerCase().replace("view", ""); + } else { + text = view.displayName.toLowerCase().replace("view", ""); + } + views.push( + React.createElement("button", { + key: view.displayName, + onClick: this.props.selectView.bind(null, view), + className: className}, + text + ) + ); + } + + return React.createElement("div", {className: "view-selector btn-group btn-group-xs"}, views); + } +}); + +var ContentView = React.createClass({displayName: "ContentView", + getInitialState: function () { + return { + displayLarge: false, + View: ViewAuto + }; + }, + propTypes: { + // It may seem a bit weird at the first glance: + // Every view takes the flow and the message as props, e.g. + // <Auto flow={flow} message={flow.request}/> + flow: React.PropTypes.object.isRequired, + message: React.PropTypes.object.isRequired, + }, + selectView: function (view) { + this.setState({ + View: view + }); + }, + displayLarge: function () { + this.setState({displayLarge: true}); + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.message !== this.props.message) { + this.setState(this.getInitialState()); + } + }, + render: function () { + var message = this.props.message; + if (message.contentLength === 0) { + return React.createElement(ContentEmpty, React.__spread({}, this.props)); + } else if (message.contentLength === null) { + return React.createElement(ContentMissing, React.__spread({}, this.props)); + } else if (!this.state.displayLarge && TooLarge.isTooLarge(message)) { + return React.createElement(TooLarge, React.__spread({}, this.props, {onClick: this.displayLarge})); + } + + var downloadUrl = MessageUtils.getContentURL(this.props.flow, message); + + return React.createElement("div", null, + React.createElement(this.state.View, React.__spread({}, this.props)), + React.createElement("div", {className: "view-options text-center"}, + React.createElement(ViewSelector, {selectView: this.selectView, active: this.state.View, message: message}), + "Â ", + React.createElement("a", {className: "btn btn-default btn-xs", href: downloadUrl}, + React.createElement("i", {className: "fa fa-download"}) + ) + ) + ); + } +}); + +module.exports = ContentView; + +},{"../../flow/utils.js":23,"../../utils.js":26,"lodash":"lodash","react":"react"}],10:[function(require,module,exports){ +var React = require("react"); +var _ = require("lodash"); + +var utils = require("../../utils.js"); + +var TimeStamp = React.createClass({displayName: "TimeStamp", + render: function () { + + if (!this.props.t) { + //should be return null, but that triggers a React bug. + return React.createElement("tr", null); + } + + var ts = utils.formatTimeStamp(this.props.t); + + var delta; + if (this.props.deltaTo) { + delta = utils.formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); + delta = React.createElement("span", {className: "text-muted"}, "(" + delta + ")"); + } else { + delta = null; + } + + return React.createElement("tr", null, + React.createElement("td", null, this.props.title + ":"), + React.createElement("td", null, ts, " ", delta) + ); + } +}); + +var ConnectionInfo = React.createClass({displayName: "ConnectionInfo", + + render: function () { + var conn = this.props.conn; + var address = conn.address.address.join(":"); + + var sni = React.createElement("tr", {key: "sni"}); //should be null, but that triggers a React bug. + if (conn.sni) { + sni = React.createElement("tr", {key: "sni"}, + React.createElement("td", null, + React.createElement("abbr", {title: "TLS Server Name Indication"}, "TLS SNI:") + ), + React.createElement("td", null, conn.sni) + ); + } + return ( + React.createElement("table", {className: "connection-table"}, + React.createElement("tbody", null, + React.createElement("tr", {key: "address"}, + React.createElement("td", null, "Address:"), + React.createElement("td", null, address) + ), + sni + ) + ) + ); + } +}); + +var CertificateInfo = React.createClass({displayName: "CertificateInfo", + render: function () { + //TODO: We should fetch human-readable certificate representation + // from the server + var flow = this.props.flow; + var client_conn = flow.client_conn; + var server_conn = flow.server_conn; + + var preStyle = {maxHeight: 100}; + return ( + React.createElement("div", null, + client_conn.cert ? React.createElement("h4", null, "Client Certificate") : null, + client_conn.cert ? React.createElement("pre", {style: preStyle}, client_conn.cert) : null, + + server_conn.cert ? React.createElement("h4", null, "Server Certificate") : null, + server_conn.cert ? React.createElement("pre", {style: preStyle}, server_conn.cert) : null + ) + ); + } +}); + +var Timing = React.createClass({displayName: "Timing", + render: function () { + var flow = this.props.flow; + var sc = flow.server_conn; + var cc = flow.client_conn; + var req = flow.request; + var resp = flow.response; + + var timestamps = [ + { + title: "Server conn. initiated", + t: sc.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Server conn. TCP handshake", + t: sc.timestamp_tcp_setup, + deltaTo: req.timestamp_start + }, { + title: "Server conn. SSL handshake", + t: sc.timestamp_ssl_setup, + deltaTo: req.timestamp_start + }, { + title: "Client conn. established", + t: cc.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Client conn. SSL handshake", + t: cc.timestamp_ssl_setup, + deltaTo: req.timestamp_start + }, { + title: "First request byte", + t: req.timestamp_start, + }, { + title: "Request complete", + t: req.timestamp_end, + deltaTo: req.timestamp_start + } + ]; + + if (flow.response) { + timestamps.push( + { + title: "First response byte", + t: resp.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Response complete", + t: resp.timestamp_end, + deltaTo: req.timestamp_start + } + ); + } + + //Add unique key for each row. + timestamps.forEach(function (e) { + e.key = e.title; + }); + + timestamps = _.sortBy(timestamps, 't'); + + var rows = timestamps.map(function (e) { + return React.createElement(TimeStamp, React.__spread({}, e)); + }); + + return ( + React.createElement("div", null, + React.createElement("h4", null, "Timing"), + React.createElement("table", {className: "timing-table"}, + React.createElement("tbody", null, + rows + ) + ) + ) + ); + } +}); + +var Details = React.createClass({displayName: "Details", + render: function () { + var flow = this.props.flow; + var client_conn = flow.client_conn; + var server_conn = flow.server_conn; + return ( + React.createElement("section", null, + + React.createElement("h4", null, "Client Connection"), + React.createElement(ConnectionInfo, {conn: client_conn}), + + React.createElement("h4", null, "Server Connection"), + React.createElement(ConnectionInfo, {conn: server_conn}), + + React.createElement(CertificateInfo, {flow: flow}), + + React.createElement(Timing, {flow: flow}) + + ) + ); + } +}); + +module.exports = Details; + +},{"../../utils.js":26,"lodash":"lodash","react":"react"}],11:[function(require,module,exports){ var React = require("react"); +var _ = require("lodash"); + +var common = require("../common.js"); +var Nav = require("./nav.js"); +var Messages = require("./messages.js"); +var Details = require("./details.js"); +var Prompt = require("../prompt.js"); + + +var allTabs = { + request: Messages.Request, + response: Messages.Response, + error: Messages.Error, + details: Details +}; + +var FlowView = React.createClass({displayName: "FlowView", + mixins: [common.StickyHeadMixin, common.Navigation, common.RouterState], + getInitialState: function () { + return { + prompt: false + }; + }, + getTabs: function (flow) { + var tabs = []; + ["request", "response", "error"].forEach(function (e) { + if (flow[e]) { + tabs.push(e); + } + }); + tabs.push("details"); + return tabs; + }, + nextTab: function (i) { + var tabs = this.getTabs(this.props.flow); + var currentIndex = tabs.indexOf(this.getActive()); + // JS modulo operator doesn't correct negative numbers, make sure that we are positive. + var nextIndex = (currentIndex + i + tabs.length) % tabs.length; + this.selectTab(tabs[nextIndex]); + }, + selectTab: function (panel) { + this.replaceWith( + "flow", + { + flowId: this.getParams().flowId, + detailTab: panel + } + ); + }, + getActive: function(){ + return this.getParams().detailTab; + }, + promptEdit: function () { + var options; + switch(this.getActive()){ + case "request": + options = [ + "method", + "url", + {text:"http version", key:"v"}, + "header" + /*, "content"*/]; + break; + case "response": + options = [ + {text:"http version", key:"v"}, + "code", + "message", + "header" + /*, "content"*/]; + break; + case "details": + return; + default: + throw "Unknown tab for edit: " + this.getActive(); + } + + this.setState({ + prompt: { + done: function (k) { + this.setState({prompt: false}); + if(k){ + this.refs.tab.edit(k); + } + }.bind(this), + options: options + } + }); + }, + render: function () { + var flow = this.props.flow; + var tabs = this.getTabs(flow); + var active = this.getActive(); + + if (!_.contains(tabs, active)) { + if (active === "response" && flow.error) { + active = "error"; + } else if (active === "error" && flow.response) { + active = "response"; + } else { + active = tabs[0]; + } + this.selectTab(active); + } + + var prompt = null; + if (this.state.prompt) { + prompt = React.createElement(Prompt, React.__spread({}, this.state.prompt)); + } + + var Tab = allTabs[active]; + return ( + React.createElement("div", {className: "flow-detail", onScroll: this.adjustHead}, + React.createElement(Nav, {ref: "head", + flow: flow, + tabs: tabs, + active: active, + selectTab: this.selectTab}), + React.createElement(Tab, {ref: "tab", flow: flow}), + prompt + ) + ); + } +}); + +module.exports = FlowView; + +},{"../common.js":4,"../prompt.js":17,"./details.js":10,"./messages.js":12,"./nav.js":13,"lodash":"lodash","react":"react"}],12:[function(require,module,exports){ +var React = require("react"); +var _ = require("lodash"); + +var common = require("../common.js"); +var actions = require("../../actions.js"); +var flowutils = require("../../flow/utils.js"); +var utils = require("../../utils.js"); +var ContentView = require("./contentview.js"); +var ValueEditor = require("../editor.js").ValueEditor; + +var Headers = React.createClass({displayName: "Headers", + propTypes: { + onChange: React.PropTypes.func.isRequired, + message: React.PropTypes.object.isRequired + }, + onChange: function (row, col, val) { + var nextHeaders = _.cloneDeep(this.props.message.headers); + nextHeaders[row][col] = val; + if (!nextHeaders[row][0] && !nextHeaders[row][1]) { + // do not delete last row + if (nextHeaders.length === 1) { + nextHeaders[0][0] = "Name"; + nextHeaders[0][1] = "Value"; + } else { + nextHeaders.splice(row, 1); + // manually move selection target if this has been the last row. + if (row === nextHeaders.length) { + this._nextSel = (row - 1) + "-value"; + } + } + } + this.props.onChange(nextHeaders); + }, + edit: function () { + this.refs["0-key"].focus(); + }, + onTab: function (row, col, e) { + var headers = this.props.message.headers; + if (row === headers.length - 1 && col === 1) { + e.preventDefault(); + + var nextHeaders = _.cloneDeep(this.props.message.headers); + nextHeaders.push(["Name", "Value"]); + this.props.onChange(nextHeaders); + this._nextSel = (row + 1) + "-key"; + } + }, + componentDidUpdate: function () { + if (this._nextSel && this.refs[this._nextSel]) { + this.refs[this._nextSel].focus(); + this._nextSel = undefined; + } + }, + onRemove: function (row, col, e) { + if (col === 1) { + e.preventDefault(); + this.refs[row + "-key"].focus(); + } else if (row > 0) { + e.preventDefault(); + this.refs[(row - 1) + "-value"].focus(); + } + }, + render: function () { + + var rows = this.props.message.headers.map(function (header, i) { + + var kEdit = React.createElement(HeaderEditor, { + ref: i + "-key", + content: header[0], + onDone: this.onChange.bind(null, i, 0), + onRemove: this.onRemove.bind(null, i, 0), + onTab: this.onTab.bind(null, i, 0)}); + var vEdit = React.createElement(HeaderEditor, { + ref: i + "-value", + content: header[1], + onDone: this.onChange.bind(null, i, 1), + onRemove: this.onRemove.bind(null, i, 1), + onTab: this.onTab.bind(null, i, 1)}); + return ( + React.createElement("tr", {key: i}, + React.createElement("td", {className: "header-name"}, kEdit, ":"), + React.createElement("td", {className: "header-value"}, vEdit) + ) + ); + }.bind(this)); + return ( + React.createElement("table", {className: "header-table"}, + React.createElement("tbody", null, + rows + ) + ) + ); + } +}); + +var HeaderEditor = React.createClass({displayName: "HeaderEditor", + render: function () { + return React.createElement(ValueEditor, React.__spread({ref: "input"}, this.props, {onKeyDown: this.onKeyDown, inline: true})); + }, + focus: function () { + this.getDOMNode().focus(); + }, + onKeyDown: function (e) { + switch (e.keyCode) { + case utils.Key.BACKSPACE: + var s = window.getSelection().getRangeAt(0); + if (s.startOffset === 0 && s.endOffset === 0) { + this.props.onRemove(e); + } + break; + case utils.Key.TAB: + if (!e.shiftKey) { + this.props.onTab(e); + } + break; + } + } +}); + +var RequestLine = React.createClass({displayName: "RequestLine", + render: function () { + var flow = this.props.flow; + var url = flowutils.RequestUtils.pretty_url(flow.request); + var httpver = "HTTP/" + flow.request.httpversion.join("."); + + return React.createElement("div", {className: "first-line request-line"}, + React.createElement(ValueEditor, { + ref: "method", + content: flow.request.method, + onDone: this.onMethodChange, + inline: true}), + "Â ", + React.createElement(ValueEditor, { + ref: "url", + content: url, + onDone: this.onUrlChange, + isValid: this.isValidUrl, + inline: true}), + "Â ", + React.createElement(ValueEditor, { + ref: "httpVersion", + content: httpver, + onDone: this.onHttpVersionChange, + isValid: flowutils.isValidHttpVersion, + inline: true}) + ) + }, + isValidUrl: function (url) { + var u = flowutils.parseUrl(url); + return !!u.host; + }, + onMethodChange: function (nextMethod) { + actions.FlowActions.update( + this.props.flow, + {request: {method: nextMethod}} + ); + }, + onUrlChange: function (nextUrl) { + var props = flowutils.parseUrl(nextUrl); + props.path = props.path || ""; + actions.FlowActions.update( + this.props.flow, + {request: props} + ); + }, + onHttpVersionChange: function (nextVer) { + var ver = flowutils.parseHttpVersion(nextVer); + actions.FlowActions.update( + this.props.flow, + {request: {httpversion: ver}} + ); + } +}); + +var ResponseLine = React.createClass({displayName: "ResponseLine", + render: function () { + var flow = this.props.flow; + var httpver = "HTTP/" + flow.response.httpversion.join("."); + return React.createElement("div", {className: "first-line response-line"}, + React.createElement(ValueEditor, { + ref: "httpVersion", + content: httpver, + onDone: this.onHttpVersionChange, + isValid: flowutils.isValidHttpVersion, + inline: true}), + "Â ", + React.createElement(ValueEditor, { + ref: "code", + content: flow.response.code + "", + onDone: this.onCodeChange, + isValid: this.isValidCode, + inline: true}), + "Â ", + React.createElement(ValueEditor, { + ref: "msg", + content: flow.response.msg, + onDone: this.onMsgChange, + inline: true}) + ); + }, + isValidCode: function (code) { + return /^\d+$/.test(code); + }, + onHttpVersionChange: function (nextVer) { + var ver = flowutils.parseHttpVersion(nextVer); + actions.FlowActions.update( + this.props.flow, + {response: {httpversion: ver}} + ); + }, + onMsgChange: function (nextMsg) { + actions.FlowActions.update( + this.props.flow, + {response: {msg: nextMsg}} + ); + }, + onCodeChange: function (nextCode) { + nextCode = parseInt(nextCode); + actions.FlowActions.update( + this.props.flow, + {response: {code: nextCode}} + ); + } +}); + +var Request = React.createClass({displayName: "Request", + render: function () { + var flow = this.props.flow; + return ( + React.createElement("section", {className: "request"}, + React.createElement(RequestLine, {ref: "requestLine", flow: flow}), + /*<ResponseLine flow={flow}/>*/ + React.createElement(Headers, {ref: "headers", message: flow.request, onChange: this.onHeaderChange}), + React.createElement("hr", null), + React.createElement(ContentView, {flow: flow, message: flow.request}) + ) + ); + }, + edit: function (k) { + switch (k) { + case "m": + this.refs.requestLine.refs.method.focus(); + break; + case "u": + this.refs.requestLine.refs.url.focus(); + break; + case "v": + this.refs.requestLine.refs.httpVersion.focus(); + break; + case "h": + this.refs.headers.edit(); + break; + default: + throw "Unimplemented: " + k; + } + }, + onHeaderChange: function (nextHeaders) { + actions.FlowActions.update(this.props.flow, { + request: { + headers: nextHeaders + } + }); + } +}); + +var Response = React.createClass({displayName: "Response", + render: function () { + var flow = this.props.flow; + return ( + React.createElement("section", {className: "response"}, + /*<RequestLine flow={flow}/>*/ + React.createElement(ResponseLine, {ref: "responseLine", flow: flow}), + React.createElement(Headers, {ref: "headers", message: flow.response, onChange: this.onHeaderChange}), + React.createElement("hr", null), + React.createElement(ContentView, {flow: flow, message: flow.response}) + ) + ); + }, + edit: function (k) { + switch (k) { + case "c": + this.refs.responseLine.refs.code.focus(); + break; + case "m": + this.refs.responseLine.refs.msg.focus(); + break; + case "v": + this.refs.responseLine.refs.httpVersion.focus(); + break; + case "h": + this.refs.headers.edit(); + break; + default: + throw "Unimplemented: " + k; + } + }, + onHeaderChange: function (nextHeaders) { + actions.FlowActions.update(this.props.flow, { + response: { + headers: nextHeaders + } + }); + } +}); + +var Error = React.createClass({displayName: "Error", + render: function () { + var flow = this.props.flow; + return ( + React.createElement("section", null, + React.createElement("div", {className: "alert alert-warning"}, + flow.error.msg, + React.createElement("div", null, + React.createElement("small", null, utils.formatTimeStamp(flow.error.timestamp) ) + ) + ) + ) + ); + } +}); + +module.exports = { + Request: Request, + Response: Response, + Error: Error +}; + +},{"../../actions.js":2,"../../flow/utils.js":23,"../../utils.js":26,"../common.js":4,"../editor.js":5,"./contentview.js":9,"lodash":"lodash","react":"react"}],13:[function(require,module,exports){ +var React = require("react"); + +var actions = require("../../actions.js"); + +var NavAction = React.createClass({displayName: "NavAction", + onClick: function (e) { + e.preventDefault(); + this.props.onClick(); + }, + render: function () { + return ( + React.createElement("a", {title: this.props.title, + href: "#", + className: "nav-action", + onClick: this.onClick}, + React.createElement("i", {className: "fa fa-fw " + this.props.icon}) + ) + ); + } +}); + +var Nav = React.createClass({displayName: "Nav", + render: function () { + var flow = this.props.flow; + + var tabs = this.props.tabs.map(function (e) { + var str = e.charAt(0).toUpperCase() + e.slice(1); + var className = this.props.active === e ? "active" : ""; + var onClick = function (event) { + this.props.selectTab(e); + event.preventDefault(); + }.bind(this); + return React.createElement("a", {key: e, + href: "#", + className: className, + onClick: onClick}, str); + }.bind(this)); + + var acceptButton = null; + if(flow.intercepted){ + acceptButton = React.createElement(NavAction, {title: "[a]ccept intercepted flow", icon: "fa-play", onClick: actions.FlowActions.accept.bind(null, flow)}); + } + var revertButton = null; + if(flow.modified){ + revertButton = React.createElement(NavAction, {title: "revert changes to flow [V]", icon: "fa-history", onClick: actions.FlowActions.revert.bind(null, flow)}); + } + + return ( + React.createElement("nav", {ref: "head", className: "nav-tabs nav-tabs-sm"}, + tabs, + React.createElement(NavAction, {title: "[d]elete flow", icon: "fa-trash", onClick: actions.FlowActions.delete.bind(null, flow)}), + React.createElement(NavAction, {title: "[D]uplicate flow", icon: "fa-copy", onClick: actions.FlowActions.duplicate.bind(null, flow)}), + React.createElement(NavAction, {disabled: true, title: "[r]eplay flow", icon: "fa-repeat", onClick: actions.FlowActions.replay.bind(null, flow)}), + acceptButton, + revertButton + ) + ); + } +}); + +module.exports = Nav; + +},{"../../actions.js":2,"react":"react"}],14:[function(require,module,exports){ +var React = require("react"); +var common = require("./common.js"); var Footer = React.createClass({displayName: "Footer", + mixins: [common.SettingsState], render: function () { - var mode = this.props.settings.mode; - var intercept = this.props.settings.intercept; + var mode = this.state.settings.mode; + var intercept = this.state.settings.intercept; return ( React.createElement("footer", null, - mode != "regular" ? React.createElement("span", {className: "label label-success"}, mode, " mode") : null, + mode && mode != "regular" ? React.createElement("span", {className: "label label-success"}, mode, " mode") : null, "Â ", intercept ? React.createElement("span", {className: "label label-success"}, "Intercept: ", intercept) : null ) @@ -1598,7 +2386,7 @@ var Footer = React.createClass({displayName: "Footer", module.exports = Footer; -},{"react":"react"}],10:[function(require,module,exports){ +},{"./common.js":4,"react":"react"}],15:[function(require,module,exports){ var React = require("react"); var $ = require("jquery"); @@ -1651,6 +2439,7 @@ var FilterDocs = React.createClass({displayName: "FilterDocs", } }); var FilterInput = React.createClass({displayName: "FilterInput", + mixins: [common.ChildFocus], getInitialState: function () { // Consider both focus and mouseover for showing/hiding the tooltip, // because onBlur of the input is triggered before the click on the tooltip @@ -1715,11 +2504,13 @@ var FilterInput = React.createClass({displayName: "FilterInput", // If closed using ESC/ENTER, hide the tooltip. this.setState({mousefocus: false}); } + e.stopPropagation(); }, blur: function () { this.refs.input.getDOMNode().blur(); + this.returnFocus(); }, - focus: function () { + select: function () { this.refs.input.getDOMNode().select(); }, render: function () { @@ -1758,14 +2549,14 @@ var FilterInput = React.createClass({displayName: "FilterInput", }); var MainMenu = React.createClass({displayName: "MainMenu", - mixins: [common.Navigation, common.State], + mixins: [common.Navigation, common.RouterState, common.SettingsState], statics: { title: "Start", route: "flows" }, - onFilterChange: function (val) { + onSearchChange: function (val) { var d = {}; - d[Query.FILTER] = val; + d[Query.SEARCH] = val; this.setQuery(d); }, onHighlightChange: function (val) { @@ -1774,29 +2565,32 @@ var MainMenu = React.createClass({displayName: "MainMenu", this.setQuery(d); }, onInterceptChange: function (val) { - SettingsActions.update({intercept: val}); + actions.SettingsActions.update({intercept: val}); }, render: function () { - var filter = this.getQuery()[Query.FILTER] || ""; + var search = this.getQuery()[Query.SEARCH] || ""; var highlight = this.getQuery()[Query.HIGHLIGHT] || ""; - var intercept = this.props.settings.intercept || ""; + var intercept = this.state.settings.intercept || ""; return ( React.createElement("div", null, React.createElement("div", {className: "menu-row"}, React.createElement(FilterInput, { - placeholder: "Filter", - type: "filter", + ref: "search", + placeholder: "Search", + type: "search", color: "black", - value: filter, - onChange: this.onFilterChange}), + value: search, + onChange: this.onSearchChange}), React.createElement(FilterInput, { + ref: "highlight", placeholder: "Highlight", type: "tag", color: "hsl(48, 100%, 50%)", value: highlight, onChange: this.onHighlightChange}), React.createElement(FilterInput, { + ref: "intercept", placeholder: "Intercept", type: "pause", color: "hsl(208, 56%, 53%)", @@ -1815,7 +2609,7 @@ var ViewMenu = React.createClass({displayName: "ViewMenu", title: "View", route: "flows" }, - mixins: [common.Navigation, common.State], + mixins: [common.Navigation, common.RouterState], toggleEventLog: function () { var d = {}; @@ -1957,15 +2751,17 @@ var Header = React.createClass({displayName: "Header", }, render: function () { var header = header_entries.map(function (entry, i) { - var classes = React.addons.classSet({ - active: entry == this.state.active - }); + var className; + if (entry === this.state.active) { + className = "active"; + } else { + className = ""; + } return ( React.createElement("a", {key: i, href: "#", - className: classes, - onClick: this.handleClick.bind(this, entry) - }, + className: className, + onClick: this.handleClick.bind(this, entry)}, entry.title ) ); @@ -1978,7 +2774,7 @@ var Header = React.createClass({displayName: "Header", header ), React.createElement("div", {className: "menu"}, - React.createElement(this.state.active, {settings: this.props.settings}) + React.createElement(this.state.active, {ref: "active"}) ) ) ); @@ -1987,32 +2783,56 @@ var Header = React.createClass({displayName: "Header", module.exports = { - Header: Header -} + Header: Header, + MainMenu: MainMenu +}; -},{"../actions.js":2,"../filt/filt.js":16,"../utils.js":20,"./common.js":4,"jquery":"jquery","react":"react"}],11:[function(require,module,exports){ +},{"../actions.js":2,"../filt/filt.js":22,"../utils.js":26,"./common.js":4,"jquery":"jquery","react":"react"}],16:[function(require,module,exports){ var React = require("react"); -var common = require("./common.js"); var actions = require("../actions.js"); var Query = require("../actions.js").Query; -var toputils = require("../utils.js"); +var utils = require("../utils.js"); var views = require("../store/view.js"); var Filt = require("../filt/filt.js"); -FlowTable = require("./flowtable.js"); -var flowdetail = require("./flowdetail.js"); + +var common = require("./common.js"); +var FlowTable = require("./flowtable.js"); +var FlowView = require("./flowview/index.js"); var MainView = React.createClass({displayName: "MainView", - mixins: [common.Navigation, common.State], + mixins: [common.Navigation, common.RouterState], + contextTypes: { + flowStore: React.PropTypes.object.isRequired, + }, + childContextTypes: { + view: React.PropTypes.object.isRequired, + }, + getChildContext: function () { + return { + view: this.state.view + }; + }, getInitialState: function () { + var sortKeyFun = false; + var view = new views.StoreView(this.context.flowStore, this.getViewFilt(), sortKeyFun); + view.addListener("recalculate", this.onRecalculate); + view.addListener("add", this.onUpdate); + view.addListener("update", this.onUpdate); + view.addListener("remove", this.onUpdate); + view.addListener("remove", this.onRemove); + return { - flows: [], - sortKeyFun: false + view: view, + sortKeyFun: sortKeyFun }; }, + componentWillUnmount: function () { + this.state.view.close(); + }, getViewFilt: function () { try { - var filt = Filt.parse(this.getQuery()[Query.FILTER] || ""); + var filt = Filt.parse(this.getQuery()[Query.SEARCH] || ""); var highlightStr = this.getQuery()[Query.HIGHLIGHT]; var highlight = highlightStr ? Filt.parse(highlightStr) : false; } catch (e) { @@ -2028,29 +2848,12 @@ var MainView = React.createClass({displayName: "MainView", }; }, componentWillReceiveProps: function (nextProps) { - if (nextProps.flowStore !== this.props.flowStore) { - this.closeView(); - this.openView(nextProps.flowStore); - } - - var filterChanged = (this.props.query[Query.FILTER] !== nextProps.query[Query.FILTER]); + var filterChanged = (this.props.query[Query.SEARCH] !== nextProps.query[Query.SEARCH]); var highlightChanged = (this.props.query[Query.HIGHLIGHT] !== nextProps.query[Query.HIGHLIGHT]); if (filterChanged || highlightChanged) { this.state.view.recalculate(this.getViewFilt(), this.state.sortKeyFun); } }, - openView: function (store) { - var view = new views.StoreView(store, this.getViewFilt(), this.state.sortKeyFun); - this.setState({ - view: view - }); - - view.addListener("recalculate", this.onRecalculate); - view.addListener("add", this.onUpdate); - view.addListener("update", this.onUpdate); - view.addListener("remove", this.onUpdate); - view.addListener("remove", this.onRemove); - }, onRecalculate: function () { this.forceUpdate(); var selected = this.getSelected(); @@ -2069,16 +2872,7 @@ var MainView = React.createClass({displayName: "MainView", this.selectFlow(flow_to_select); } }, - closeView: function () { - this.state.view.close(); - }, - componentWillMount: function () { - this.openView(this.props.flowStore); - }, - componentWillUnmount: function () { - this.closeView(); - }, - setSortKeyFun: function(sortKeyFun){ + setSortKeyFun: function (sortKeyFun) { this.setState({ sortKeyFun: sortKeyFun }); @@ -2102,7 +2896,7 @@ var MainView = React.createClass({displayName: "MainView", var flows = this.state.view.list; var index; if (!this.getParams().flowId) { - if (shift > 0) { + if (shift < 0) { index = flows.length - 1; } else { index = 0; @@ -2122,55 +2916,55 @@ var MainView = React.createClass({displayName: "MainView", } this.selectFlow(flows[index]); }, - onKeyDown: function (e) { + onMainKeyDown: function (e) { var flow = this.getSelected(); if (e.ctrlKey) { return; } switch (e.keyCode) { - case toputils.Key.K: - case toputils.Key.UP: + case utils.Key.K: + case utils.Key.UP: this.selectFlowRelative(-1); break; - case toputils.Key.J: - case toputils.Key.DOWN: + case utils.Key.J: + case utils.Key.DOWN: this.selectFlowRelative(+1); break; - case toputils.Key.SPACE: - case toputils.Key.PAGE_DOWN: + case utils.Key.SPACE: + case utils.Key.PAGE_DOWN: this.selectFlowRelative(+10); break; - case toputils.Key.PAGE_UP: + case utils.Key.PAGE_UP: this.selectFlowRelative(-10); break; - case toputils.Key.END: + case utils.Key.END: this.selectFlowRelative(+1e10); break; - case toputils.Key.HOME: + case utils.Key.HOME: this.selectFlowRelative(-1e10); break; - case toputils.Key.ESC: + case utils.Key.ESC: this.selectFlow(null); break; - case toputils.Key.H: - case toputils.Key.LEFT: + case utils.Key.H: + case utils.Key.LEFT: if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(-1); } break; - case toputils.Key.L: - case toputils.Key.TAB: - case toputils.Key.RIGHT: + case utils.Key.L: + case utils.Key.TAB: + case utils.Key.RIGHT: if (this.refs.flowDetails) { this.refs.flowDetails.nextTab(+1); } break; - case toputils.Key.C: + case utils.Key.C: if (e.shiftKey) { actions.FlowActions.clear(); } break; - case toputils.Key.D: + case utils.Key.D: if (flow) { if (e.shiftKey) { actions.FlowActions.duplicate(flow); @@ -2179,23 +2973,30 @@ var MainView = React.createClass({displayName: "MainView", } } break; - case toputils.Key.A: + case utils.Key.A: if (e.shiftKey) { actions.FlowActions.accept_all(); } else if (flow && flow.intercepted) { actions.FlowActions.accept(flow); } break; - case toputils.Key.R: + case utils.Key.R: if (!e.shiftKey && flow) { actions.FlowActions.replay(flow); } break; - case toputils.Key.V: + case utils.Key.V: if (e.shiftKey && flow && flow.modified) { actions.FlowActions.revert(flow); } break; + case utils.Key.E: + if (this.refs.flowDetails) { + this.refs.flowDetails.promptEdit(); + } + break; + case utils.Key.SHIFT: + break; default: console.debug("keydown", e.keyCode); return; @@ -2203,7 +3004,7 @@ var MainView = React.createClass({displayName: "MainView", e.preventDefault(); }, getSelected: function () { - return this.props.flowStore.get(this.getParams().flowId); + return this.context.flowStore.get(this.getParams().flowId); }, render: function () { var selected = this.getSelected(); @@ -2212,16 +3013,15 @@ var MainView = React.createClass({displayName: "MainView", if (selected) { details = [ React.createElement(common.Splitter, {key: "splitter"}), - React.createElement(flowdetail.FlowDetail, {key: "flowDetails", ref: "flowDetails", flow: selected}) + React.createElement(FlowView, {key: "flowDetails", ref: "flowDetails", flow: selected}) ]; } else { details = null; } return ( - React.createElement("div", {className: "main-view", onKeyDown: this.onKeyDown, tabIndex: "0"}, + React.createElement("div", {className: "main-view"}, React.createElement(FlowTable, {ref: "flowTable", - view: this.state.view, selectFlow: this.selectFlow, setSortKeyFun: this.setSortKeyFun, selected: selected}), @@ -2234,7 +3034,109 @@ var MainView = React.createClass({displayName: "MainView", module.exports = MainView; -},{"../actions.js":2,"../filt/filt.js":16,"../store/view.js":19,"../utils.js":20,"./common.js":4,"./flowdetail.js":6,"./flowtable.js":8,"react":"react"}],12:[function(require,module,exports){ +},{"../actions.js":2,"../filt/filt.js":22,"../store/view.js":25,"../utils.js":26,"./common.js":4,"./flowtable.js":8,"./flowview/index.js":11,"react":"react"}],17:[function(require,module,exports){ +var React = require("react"); +var _ = require("lodash"); + +var utils = require("../utils.js"); +var common = require("./common.js"); + +var Prompt = React.createClass({displayName: "Prompt", + mixins: [common.ChildFocus], + propTypes: { + options: React.PropTypes.array.isRequired, + done: React.PropTypes.func.isRequired, + prompt: React.PropTypes.string + }, + componentDidMount: function () { + React.findDOMNode(this).focus(); + }, + onKeyDown: function (e) { + e.stopPropagation(); + e.preventDefault(); + var opts = this.getOptions(); + for (var i = 0; i < opts.length; i++) { + var k = opts[i].key; + if (utils.Key[k.toUpperCase()] === e.keyCode) { + this.done(k); + return; + } + } + if (e.keyCode === utils.Key.ESC || e.keyCode === utils.Key.ENTER) { + this.done(false); + } + }, + onClick: function (e) { + this.done(false); + }, + done: function (ret) { + this.props.done(ret); + this.returnFocus(); + }, + getOptions: function () { + var opts = []; + + var keyTaken = function (k) { + return _.includes(_.pluck(opts, "key"), k); + }; + + for (var i = 0; i < this.props.options.length; i++) { + var opt = this.props.options[i]; + if (_.isString(opt)) { + var str = opt; + while (str.length > 0 && keyTaken(str[0])) { + str = str.substr(1); + } + opt = { + text: opt, + key: str[0] + }; + } + if (!opt.text || !opt.key || keyTaken(opt.key)) { + throw "invalid options"; + } else { + opts.push(opt); + } + } + return opts; + }, + render: function () { + var opts = this.getOptions(); + opts = _.map(opts, function (o) { + var prefix, suffix; + var idx = o.text.indexOf(o.key); + if (idx !== -1) { + prefix = o.text.substring(0, idx); + suffix = o.text.substring(idx + 1); + + } else { + prefix = o.text + " ("; + suffix = ")"; + } + var onClick = function (e) { + this.done(o.key); + e.stopPropagation(); + }.bind(this); + return React.createElement("span", { + key: o.key, + className: "option", + onClick: onClick}, + prefix, + React.createElement("strong", {className: "text-primary"}, o.key), suffix + ); + }.bind(this)); + return React.createElement("div", {tabIndex: "0", onKeyDown: this.onKeyDown, onClick: this.onClick, className: "prompt-dialog"}, + React.createElement("div", {className: "prompt-content"}, + this.props.prompt || React.createElement("strong", null, "Select: "), + opts + ) + ); + } +}); + +module.exports = Prompt; + +},{"../utils.js":26,"./common.js":4,"lodash":"lodash","react":"react"}],18:[function(require,module,exports){ var React = require("react"); var ReactRouter = require("react-router"); var _ = require("lodash"); @@ -2246,6 +3148,7 @@ var header = require("./header.js"); var EventLog = require("./eventlog.js"); var store = require("../store/store.js"); var Query = require("../actions.js").Query; +var Key = require("../utils.js").Key; //TODO: Move out of here, just a stub. @@ -2257,52 +3160,87 @@ var Reports = React.createClass({displayName: "Reports", var ProxyAppMain = React.createClass({displayName: "ProxyAppMain", - mixins: [common.State], + mixins: [common.RouterState], + childContextTypes: { + settingsStore: React.PropTypes.object.isRequired, + flowStore: React.PropTypes.object.isRequired, + eventStore: React.PropTypes.object.isRequired, + returnFocus: React.PropTypes.func.isRequired, + }, + componentDidMount: function () { + this.focus(); + }, + getChildContext: function () { + return { + settingsStore: this.state.settingsStore, + flowStore: this.state.flowStore, + eventStore: this.state.eventStore, + returnFocus: this.focus, + }; + }, getInitialState: function () { var eventStore = new store.EventLogStore(); var flowStore = new store.FlowStore(); - var settings = new store.SettingsStore(); + var settingsStore = new store.SettingsStore(); // Default Settings before fetch - _.extend(settings.dict,{ - }); + _.extend(settingsStore.dict, {}); return { - settings: settings, + settingsStore: settingsStore, flowStore: flowStore, eventStore: eventStore }; }, - componentDidMount: function () { - this.state.settings.addListener("recalculate", this.onSettingsChange); - window.app = this; + focus: function () { + React.findDOMNode(this).focus(); }, - componentWillUnmount: function () { - this.state.settings.removeListener("recalculate", this.onSettingsChange); + getMainComponent: function () { + return this.refs.view.refs.__routeHandler__; }, - onSettingsChange: function(){ - this.setState({ - settings: this.state.settings - }); + onKeydown: function (e) { + + var selectFilterInput = function (name) { + var headerComponent = this.refs.header; + headerComponent.setState({active: header.MainMenu}, function () { + headerComponent.refs.active.refs[name].select(); + }); + }.bind(this); + + switch (e.keyCode) { + case Key.I: + selectFilterInput("intercept"); + break; + case Key.L: + selectFilterInput("search"); + break; + case Key.H: + selectFilterInput("highlight"); + break; + default: + var main = this.getMainComponent(); + if (main.onMainKeyDown) { + main.onMainKeyDown(e); + } + return; // don't prevent default then + } + e.preventDefault(); }, render: function () { var eventlog; if (this.getQuery()[Query.SHOW_EVENTLOG]) { eventlog = [ React.createElement(common.Splitter, {key: "splitter", axis: "y"}), - React.createElement(EventLog, {key: "eventlog", eventStore: this.state.eventStore}) + React.createElement(EventLog, {key: "eventlog"}) ]; } else { eventlog = null; } return ( - React.createElement("div", {id: "container"}, - React.createElement(header.Header, {settings: this.state.settings.dict}), - React.createElement(RouteHandler, { - settings: this.state.settings.dict, - flowStore: this.state.flowStore, - query: this.getQuery()}), + React.createElement("div", {id: "container", tabIndex: "0", onKeyDown: this.onKeydown}, + React.createElement(header.Header, {ref: "header"}), + React.createElement(RouteHandler, {ref: "view", query: this.getQuery()}), eventlog, - React.createElement(Footer, {settings: this.state.settings.dict}) + React.createElement(Footer, null) ) ); } @@ -2329,7 +3267,7 @@ module.exports = { routes: routes }; -},{"../actions.js":2,"../store/store.js":18,"./common.js":4,"./eventlog.js":5,"./footer.js":9,"./header.js":10,"./mainview.js":11,"lodash":"lodash","react":"react","react-router":"react-router"}],13:[function(require,module,exports){ +},{"../actions.js":2,"../store/store.js":24,"../utils.js":26,"./common.js":4,"./eventlog.js":6,"./footer.js":14,"./header.js":15,"./mainview.js":16,"lodash":"lodash","react":"react","react-router":"react-router"}],19:[function(require,module,exports){ var React = require("react"); var VirtualScrollMixin = { @@ -2416,9 +3354,10 @@ var VirtualScrollMixin = { module.exports = VirtualScrollMixin; -},{"react":"react"}],14:[function(require,module,exports){ +},{"react":"react"}],20:[function(require,module,exports){ var actions = require("./actions.js"); +var AppDispatcher = require("./dispatcher.js").AppDispatcher; function Connection(url) { if (url[0] === "/") { @@ -2435,18 +3374,18 @@ function Connection(url) { }; ws.onerror = function () { actions.ConnectionActions.error(); - EventLogActions.add_event("WebSocket connection error."); + actions.EventLogActions.add_event("WebSocket connection error."); }; ws.onclose = function () { actions.ConnectionActions.close(); - EventLogActions.add_event("WebSocket connection closed."); + actions.EventLogActions.add_event("WebSocket connection closed."); }; return ws; } module.exports = Connection; -},{"./actions.js":2}],15:[function(require,module,exports){ +},{"./actions.js":2,"./dispatcher.js":21}],21:[function(require,module,exports){ var flux = require("flux"); @@ -2456,7 +3395,7 @@ const PayloadSources = { }; -AppDispatcher = new flux.Dispatcher(); +var AppDispatcher = new flux.Dispatcher(); AppDispatcher.dispatchViewAction = function (action) { action.source = PayloadSources.VIEW; this.dispatch(action); @@ -2470,7 +3409,7 @@ module.exports = { AppDispatcher: AppDispatcher }; -},{"flux":"flux"}],16:[function(require,module,exports){ +},{"flux":"flux"}],22:[function(require,module,exports){ module.exports = (function() { /* * Generated by PEG.js 0.8.0. @@ -4246,12 +5185,21 @@ module.exports = (function() { }; })(); -},{"../flow/utils.js":17}],17:[function(require,module,exports){ +},{"../flow/utils.js":23}],23:[function(require,module,exports){ var _ = require("lodash"); +var $ = require("jquery"); + +var defaultPorts = { + "http": 80, + "https": 443 +}; -var _MessageUtils = { +var MessageUtils = { getContentType: function (message) { - return this.get_first_header(message, /^Content-Type$/i); + var ct = this.get_first_header(message, /^Content-Type$/i); + if(ct){ + return ct.split(";")[0].trim(); + } }, get_first_header: function (message, regex) { //FIXME: Cache Invalidation. @@ -4283,15 +5231,22 @@ var _MessageUtils = { } } return false; + }, + getContentURL: function (flow, message) { + if (message === flow.request) { + message = "request"; + } else if (message === flow.response) { + message = "response"; + } + return "/flows/" + flow.id + "/" + message + "/content"; + }, + getContent: function (flow, message) { + var url = MessageUtils.getContentURL(flow, message); + return $.get(url); } }; -var defaultPorts = { - "http": 80, - "https": 443 -}; - -var RequestUtils = _.extend(_MessageUtils, { +var RequestUtils = _.extend(MessageUtils, { pretty_host: function (request) { //FIXME: Add hostheader return request.host; @@ -4305,16 +5260,64 @@ var RequestUtils = _.extend(_MessageUtils, { } }); -var ResponseUtils = _.extend(_MessageUtils, {}); +var ResponseUtils = _.extend(MessageUtils, {}); + +var parseUrl_regex = /^(?:(https?):\/\/)?([^\/:]+)?(?::(\d+))?(\/.*)?$/i; +var parseUrl = function (url) { + //there are many correct ways to parse a URL, + //however, a mitmproxy user may also wish to generate a not-so-correct URL. ;-) + var parts = parseUrl_regex.exec(url); + if(!parts){ + return false; + } + + var scheme = parts[1], + host = parts[2], + port = parseInt(parts[3]), + path = parts[4]; + if (scheme) { + port = port || defaultPorts[scheme]; + } + var ret = {}; + if (scheme) { + ret.scheme = scheme; + } + if (host) { + ret.host = host; + } + if (port) { + ret.port = port; + } + if (path) { + ret.path = path; + } + return ret; +}; + + +var isValidHttpVersion_regex = /^HTTP\/\d+(\.\d+)*$/i; +var isValidHttpVersion = function (httpVersion) { + return isValidHttpVersion_regex.test(httpVersion); +}; + +var parseHttpVersion = function (httpVersion) { + httpVersion = httpVersion.replace("HTTP/", "").split("."); + return _.map(httpVersion, function (x) { + return parseInt(x); + }); +}; module.exports = { ResponseUtils: ResponseUtils, - RequestUtils: RequestUtils - -} + RequestUtils: RequestUtils, + MessageUtils: MessageUtils, + parseUrl: parseUrl, + parseHttpVersion: parseHttpVersion, + isValidHttpVersion: isValidHttpVersion +}; -},{"lodash":"lodash"}],18:[function(require,module,exports){ +},{"jquery":"jquery","lodash":"lodash"}],24:[function(require,module,exports){ var _ = require("lodash"); var $ = require("jquery"); @@ -4497,7 +5500,7 @@ module.exports = { FlowStore: FlowStore }; -},{"../actions.js":2,"../dispatcher.js":15,"../utils.js":20,"events":1,"jquery":"jquery","lodash":"lodash"}],19:[function(require,module,exports){ +},{"../actions.js":2,"../dispatcher.js":21,"../utils.js":26,"events":1,"jquery":"jquery","lodash":"lodash"}],25:[function(require,module,exports){ var EventEmitter = require('events').EventEmitter; var _ = require("lodash"); @@ -4514,8 +5517,6 @@ var default_filt = function (elem) { function StoreView(store, filt, sortfun) { EventEmitter.call(this); - filt = filt || default_filt; - sortfun = sortfun || default_sort; this.store = store; @@ -4537,12 +5538,13 @@ _.extend(StoreView.prototype, EventEmitter.prototype, { this.store.removeListener("update", this.update); this.store.removeListener("remove", this.remove); this.store.removeListener("recalculate", this.recalculate); + this.removeAllListeners(); }, recalculate: function (filt, sortfun) { - filt = filt || default_filt; - sortfun = sortfun || default_sort; + filt = filt || this.filt || default_filt; + sortfun = sortfun || this.sortfun || default_sort; filt = filt.bind(this); - sortfun = sortfun.bind(this) + sortfun = sortfun.bind(this); this.filt = filt; this.sortfun = sortfun; @@ -4615,9 +5617,14 @@ module.exports = { StoreView: StoreView }; -},{"../utils.js":20,"events":1,"lodash":"lodash"}],20:[function(require,module,exports){ +},{"../utils.js":26,"events":1,"lodash":"lodash"}],26:[function(require,module,exports){ var $ = require("jquery"); var _ = require("lodash"); +var actions = require("./actions.js"); + +window.$ = $; +window._ = _; +window.React = require("react"); var Key = { UP: 38, @@ -4633,6 +5640,7 @@ var Key = { TAB: 9, SPACE: 32, BACKSPACE: 8, + SHIFT: 16 }; // Add A-Z for (var i = 65; i <= 90; i++) { @@ -4644,17 +5652,17 @@ var formatSize = function (bytes) { if (bytes === 0) return "0"; var prefix = ["b", "kb", "mb", "gb", "tb"]; - for (var i = 0; i < prefix.length; i++){ - if (Math.pow(1024, i + 1) > bytes){ + for (var i = 0; i < prefix.length; i++) { + if (Math.pow(1024, i + 1) > bytes) { break; } } var precision; - if (bytes%Math.pow(1024, i) === 0) + if (bytes % Math.pow(1024, i) === 0) precision = 0; else precision = 1; - return (bytes/Math.pow(1024, i)).toFixed(precision) + prefix[i]; + return (bytes / Math.pow(1024, i)).toFixed(precision) + prefix[i]; }; @@ -4676,21 +5684,20 @@ var formatTimeStamp = function (seconds) { return ts.replace("T", " ").replace("Z", ""); }; - // At some places, we need to sort strings alphabetically descending, // but we can only provide a key function. // This beauty "reverses" a JS string. var end = String.fromCharCode(0xffff); -function reverseString(s){ +function reverseString(s) { return String.fromCharCode.apply(String, - _.map(s.split(""), function (c) { - return 0xffff - c.charCodeAt(); - }) - ) + end; + _.map(s.split(""), function (c) { + return 0xffff - c.charCodeAt(0); + }) + ) + end; } function getCookie(name) { - var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); + var r = document.cookie.match(new RegExp("\\b" + name + "=([^;]*)\\b")); return r ? r[1] : undefined; } var xsrf = $.param({_xsrf: getCookie("_xsrf")}); @@ -4698,19 +5705,22 @@ var xsrf = $.param({_xsrf: getCookie("_xsrf")}); //Tornado XSRF Protection. $.ajaxPrefilter(function (options) { if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") { - if (options.data) { - options.data += ("&" + xsrf); + if(options.url.indexOf("?") === -1){ + options.url += "?" + xsrf; } else { - options.data = xsrf; + options.url += "&" + xsrf; } } }); // Log AJAX Errors $(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) { + if (thrownError === "abort") { + return; + } var message = jqXHR.responseText; - console.error(message, arguments); - EventLogActions.add_event(thrownError + ": " + message); - window.alert(message); + console.error(thrownError, message, arguments); + actions.EventLogActions.add_event(thrownError + ": " + message); + alert(message); }); module.exports = { @@ -4718,10 +5728,10 @@ module.exports = { formatTimeDelta: formatTimeDelta, formatTimeStamp: formatTimeStamp, reverseString: reverseString, - Key: Key + Key: Key, }; -},{"jquery":"jquery","lodash":"lodash"}]},{},[3]) +},{"./actions.js":2,"jquery":"jquery","lodash":"lodash","react":"react"}]},{},[3]) //# sourceMappingURL=app.js.map
\ No newline at end of file diff --git a/libmproxy/web/static/vendor.css b/libmproxy/web/static/vendor.css index 149372c8..a170c49a 100644 --- a/libmproxy/web/static/vendor.css +++ b/libmproxy/web/static/vendor.css @@ -945,12 +945,24 @@ th { .glyphicon-bitcoin:before { content: "\e227"; } +.glyphicon-btc:before { + content: "\e227"; +} +.glyphicon-xbt:before { + content: "\e227"; +} .glyphicon-yen:before { content: "\00a5"; } +.glyphicon-jpy:before { + content: "\00a5"; +} .glyphicon-ruble:before { content: "\20bd"; } +.glyphicon-rub:before { + content: "\20bd"; +} .glyphicon-scale:before { content: "\e230"; } @@ -1147,6 +1159,9 @@ hr { overflow: visible; clip: auto; } +[role="button"] { + cursor: pointer; +} h1, h2, h3, @@ -2548,10 +2563,13 @@ output { .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { - cursor: not-allowed; background-color: #eeeeee; opacity: 1; } +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} textarea.form-control { height: auto; } @@ -2618,6 +2636,7 @@ input[type="search"] { } .radio-inline, .checkbox-inline { + position: relative; display: inline-block; padding-left: 20px; margin-bottom: 0; @@ -2654,6 +2673,7 @@ fieldset[disabled] .checkbox label { padding-top: 7px; padding-bottom: 7px; margin-bottom: 0; + min-height: 34px; } .form-control-static.input-lg, .form-control-static.input-sm { @@ -2695,6 +2715,7 @@ select[multiple].form-group-sm .form-control { padding: 5px 10px; font-size: 12px; line-height: 1.5; + min-height: 32px; } .input-lg { height: 46px; @@ -2731,6 +2752,7 @@ select[multiple].form-group-lg .form-control { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; + min-height: 38px; } .has-feedback { position: relative; @@ -3348,11 +3370,9 @@ input[type="button"].btn-block { } .collapse { display: none; - visibility: hidden; } .collapse.in { display: block; - visibility: visible; } tr.collapse.in { display: table-row; @@ -3377,7 +3397,7 @@ tbody.collapse.in { height: 0; margin-left: 2px; vertical-align: middle; - border-top: 4px solid; + border-top: 4px dashed; border-right: 4px solid transparent; border-left: 4px solid transparent; } @@ -4016,11 +4036,9 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .tab-content > .tab-pane { display: none; - visibility: hidden; } .tab-content > .active { display: block; - visibility: visible; } .nav-tabs .dropdown-menu { margin-top: -1px; @@ -4062,7 +4080,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .navbar-collapse.collapse { display: block !important; - visibility: visible !important; height: auto !important; padding-bottom: 0; overflow: visible !important; @@ -4791,7 +4808,8 @@ a.label:focus { position: relative; top: -1px; } -.btn-xs .badge { +.btn-xs .badge, +.btn-group-xs > .btn .badge { top: 0; padding: 1px 5px; } @@ -5614,10 +5632,10 @@ a.list-group-item-danger.active:focus { width: 100%; border: 0; } -.embed-responsive.embed-responsive-16by9 { +.embed-responsive-16by9 { padding-bottom: 56.25%; } -.embed-responsive.embed-responsive-4by3 { +.embed-responsive-4by3 { padding-bottom: 75%; } .well { @@ -5678,7 +5696,7 @@ button.close { right: 0; bottom: 0; left: 0; - z-index: 1040; + z-index: 1050; -webkit-overflow-scrolling: touch; outline: 0; } @@ -5719,10 +5737,12 @@ button.close { outline: 0; } .modal-backdrop { - position: absolute; + position: fixed; top: 0; right: 0; + bottom: 0; left: 0; + z-index: 1040; background-color: #000000; } .modal-backdrop.fade { @@ -5793,7 +5813,6 @@ button.close { position: absolute; z-index: 1070; display: block; - visibility: visible; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 12px; font-weight: normal; @@ -6316,7 +6335,6 @@ button.close { } .hidden { display: none !important; - visibility: hidden !important; } .affix { position: fixed; diff --git a/libmproxy/web/static/vendor.js b/libmproxy/web/static/vendor.js index d98e50d9..1e83ac50 100644 --- a/libmproxy/web/static/vendor.js +++ b/libmproxy/web/static/vendor.js @@ -394,49 +394,432 @@ var invariant = function(condition, format, a, b, c, d, e, f) { module.exports = invariant; },{}],4:[function(require,module,exports){ -"use strict"; +module.exports = require('./lib/'); -/** - * Represents a cancellation caused by navigating away - * before the previous transition has fully resolved. - */ -function Cancellation() {} +},{"./lib/":5}],5:[function(require,module,exports){ +// Load modules -module.exports = Cancellation; -},{}],5:[function(require,module,exports){ -"use strict"; +var Stringify = require('./stringify'); +var Parse = require('./parse'); -var warning = require("react/lib/warning"); -var invariant = require("react/lib/invariant"); -function checkPropTypes(componentName, propTypes, props) { - for (var propName in propTypes) { - if (propTypes.hasOwnProperty(propName)) { - var error = propTypes[propName](props, propName, componentName); +// Declare internals - if (error instanceof Error) warning(false, error.message); +var internals = {}; + + +module.exports = { + stringify: Stringify, + parse: Parse +}; + +},{"./parse":6,"./stringify":7}],6:[function(require,module,exports){ +// Load modules + +var Utils = require('./utils'); + + +// Declare internals + +var internals = { + delimiter: '&', + depth: 5, + arrayLimit: 20, + parameterLimit: 1000 +}; + + +internals.parseValues = function (str, options) { + + var obj = {}; + var parts = str.split(options.delimiter, options.parameterLimit === Infinity ? undefined : options.parameterLimit); + + for (var i = 0, il = parts.length; i < il; ++i) { + var part = parts[i]; + var pos = part.indexOf(']=') === -1 ? part.indexOf('=') : part.indexOf(']=') + 1; + + if (pos === -1) { + obj[Utils.decode(part)] = ''; + } + else { + var key = Utils.decode(part.slice(0, pos)); + var val = Utils.decode(part.slice(pos + 1)); + + if (Object.prototype.hasOwnProperty(key)) { + continue; + } + + if (!obj.hasOwnProperty(key)) { + obj[key] = val; + } + else { + obj[key] = [].concat(obj[key]).concat(val); + } + } } - } -} -var Configuration = { + return obj; +}; + - statics: { +internals.parseObject = function (chain, val, options) { - validateProps: function validateProps(props) { - checkPropTypes(this.displayName, this.propTypes, props); + if (!chain.length) { + return val; } - }, + var root = chain.shift(); - render: function render() { - invariant(false, "%s elements are for router configuration only and should not be rendered", this.constructor.displayName); - } + var obj = {}; + if (root === '[]') { + obj = []; + obj = obj.concat(internals.parseObject(chain, val, options)); + } + else { + var cleanRoot = root[0] === '[' && root[root.length - 1] === ']' ? root.slice(1, root.length - 1) : root; + var index = parseInt(cleanRoot, 10); + var indexString = '' + index; + if (!isNaN(index) && + root !== cleanRoot && + indexString === cleanRoot && + index >= 0 && + index <= options.arrayLimit) { + + obj = []; + obj[index] = internals.parseObject(chain, val, options); + } + else { + obj[cleanRoot] = internals.parseObject(chain, val, options); + } + } + + return obj; +}; + + +internals.parseKeys = function (key, val, options) { + + if (!key) { + return; + } + + // The regex chunks + + var parent = /^([^\[\]]*)/; + var child = /(\[[^\[\]]*\])/g; + + // Get the parent + + var segment = parent.exec(key); + + // Don't allow them to overwrite object prototype properties + + if (Object.prototype.hasOwnProperty(segment[1])) { + return; + } + + // Stash the parent if it exists + + var keys = []; + if (segment[1]) { + keys.push(segment[1]); + } + + // Loop through children appending to the array until we hit depth + var i = 0; + while ((segment = child.exec(key)) !== null && i < options.depth) { + + ++i; + if (!Object.prototype.hasOwnProperty(segment[1].replace(/\[|\]/g, ''))) { + keys.push(segment[1]); + } + } + + // If there's a remainder, just add whatever is left + + if (segment) { + keys.push('[' + key.slice(segment.index) + ']'); + } + + return internals.parseObject(keys, val, options); }; -module.exports = Configuration; -},{"react/lib/invariant":182,"react/lib/warning":202}],6:[function(require,module,exports){ + +module.exports = function (str, options) { + + if (str === '' || + str === null || + typeof str === 'undefined') { + + return {}; + } + + options = options || {}; + options.delimiter = typeof options.delimiter === 'string' || Utils.isRegExp(options.delimiter) ? options.delimiter : internals.delimiter; + options.depth = typeof options.depth === 'number' ? options.depth : internals.depth; + options.arrayLimit = typeof options.arrayLimit === 'number' ? options.arrayLimit : internals.arrayLimit; + options.parameterLimit = typeof options.parameterLimit === 'number' ? options.parameterLimit : internals.parameterLimit; + + var tempObj = typeof str === 'string' ? internals.parseValues(str, options) : str; + var obj = {}; + + // Iterate over the keys and setup the new object + + var keys = Object.keys(tempObj); + for (var i = 0, il = keys.length; i < il; ++i) { + var key = keys[i]; + var newObj = internals.parseKeys(key, tempObj[key], options); + obj = Utils.merge(obj, newObj); + } + + return Utils.compact(obj); +}; + +},{"./utils":8}],7:[function(require,module,exports){ +// Load modules + +var Utils = require('./utils'); + + +// Declare internals + +var internals = { + delimiter: '&', + arrayPrefixGenerators: { + brackets: function (prefix, key) { + return prefix + '[]'; + }, + indices: function (prefix, key) { + return prefix + '[' + key + ']'; + }, + repeat: function (prefix, key) { + return prefix; + } + } +}; + + +internals.stringify = function (obj, prefix, generateArrayPrefix) { + + if (Utils.isBuffer(obj)) { + obj = obj.toString(); + } + else if (obj instanceof Date) { + obj = obj.toISOString(); + } + else if (obj === null) { + obj = ''; + } + + if (typeof obj === 'string' || + typeof obj === 'number' || + typeof obj === 'boolean') { + + return [encodeURIComponent(prefix) + '=' + encodeURIComponent(obj)]; + } + + var values = []; + + if (typeof obj === 'undefined') { + return values; + } + + var objKeys = Object.keys(obj); + for (var i = 0, il = objKeys.length; i < il; ++i) { + var key = objKeys[i]; + if (Array.isArray(obj)) { + values = values.concat(internals.stringify(obj[key], generateArrayPrefix(prefix, key), generateArrayPrefix)); + } + else { + values = values.concat(internals.stringify(obj[key], prefix + '[' + key + ']', generateArrayPrefix)); + } + } + + return values; +}; + + +module.exports = function (obj, options) { + + options = options || {}; + var delimiter = typeof options.delimiter === 'undefined' ? internals.delimiter : options.delimiter; + + var keys = []; + + if (typeof obj !== 'object' || + obj === null) { + + return ''; + } + + var arrayFormat; + if (options.arrayFormat in internals.arrayPrefixGenerators) { + arrayFormat = options.arrayFormat; + } + else if ('indices' in options) { + arrayFormat = options.indices ? 'indices' : 'repeat'; + } + else { + arrayFormat = 'indices'; + } + + var generateArrayPrefix = internals.arrayPrefixGenerators[arrayFormat]; + + var objKeys = Object.keys(obj); + for (var i = 0, il = objKeys.length; i < il; ++i) { + var key = objKeys[i]; + keys = keys.concat(internals.stringify(obj[key], key, generateArrayPrefix)); + } + + return keys.join(delimiter); +}; + +},{"./utils":8}],8:[function(require,module,exports){ +// Load modules + + +// Declare internals + +var internals = {}; + + +exports.arrayToObject = function (source) { + + var obj = {}; + for (var i = 0, il = source.length; i < il; ++i) { + if (typeof source[i] !== 'undefined') { + + obj[i] = source[i]; + } + } + + return obj; +}; + + +exports.merge = function (target, source) { + + if (!source) { + return target; + } + + if (typeof source !== 'object') { + if (Array.isArray(target)) { + target.push(source); + } + else { + target[source] = true; + } + + return target; + } + + if (typeof target !== 'object') { + target = [target].concat(source); + return target; + } + + if (Array.isArray(target) && + !Array.isArray(source)) { + + target = exports.arrayToObject(target); + } + + var keys = Object.keys(source); + for (var k = 0, kl = keys.length; k < kl; ++k) { + var key = keys[k]; + var value = source[key]; + + if (!target[key]) { + target[key] = value; + } + else { + target[key] = exports.merge(target[key], value); + } + } + + return target; +}; + + +exports.decode = function (str) { + + try { + return decodeURIComponent(str.replace(/\+/g, ' ')); + } catch (e) { + return str; + } +}; + + +exports.compact = function (obj, refs) { + + if (typeof obj !== 'object' || + obj === null) { + + return obj; + } + + refs = refs || []; + var lookup = refs.indexOf(obj); + if (lookup !== -1) { + return refs[lookup]; + } + + refs.push(obj); + + if (Array.isArray(obj)) { + var compacted = []; + + for (var i = 0, il = obj.length; i < il; ++i) { + if (typeof obj[i] !== 'undefined') { + compacted.push(obj[i]); + } + } + + return compacted; + } + + var keys = Object.keys(obj); + for (i = 0, il = keys.length; i < il; ++i) { + var key = keys[i]; + obj[key] = exports.compact(obj[key], refs); + } + + return obj; +}; + + +exports.isRegExp = function (obj) { + return Object.prototype.toString.call(obj) === '[object RegExp]'; +}; + + +exports.isBuffer = function (obj) { + + if (obj === null || + typeof obj === 'undefined') { + + return false; + } + + return !!(obj.constructor && + obj.constructor.isBuffer && + obj.constructor.isBuffer(obj)); +}; + +},{}],9:[function(require,module,exports){ +"use strict"; + +/** + * Represents a cancellation caused by navigating away + * before the previous transition has fully resolved. + */ +function Cancellation() {} + +module.exports = Cancellation; +},{}],10:[function(require,module,exports){ "use strict"; var invariant = require("react/lib/invariant"); @@ -467,10 +850,10 @@ var History = { }; module.exports = History; -},{"react/lib/ExecutionEnvironment":64,"react/lib/invariant":182}],7:[function(require,module,exports){ +},{"react/lib/ExecutionEnvironment":62,"react/lib/invariant":191}],11:[function(require,module,exports){ "use strict"; -var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; @@ -520,7 +903,7 @@ var Match = (function () { this.routes = routes; } - _prototypeProperties(Match, { + _createClass(Match, null, { findMatch: { /** @@ -537,9 +920,7 @@ var Match = (function () { for (var i = 0, len = routes.length; match == null && i < len; ++i) match = deepSearch(routes[i], pathname, query); return match; - }, - writable: true, - configurable: true + } } }); @@ -547,11 +928,20 @@ var Match = (function () { })(); module.exports = Match; -},{"./PathUtils":10}],8:[function(require,module,exports){ +},{"./PathUtils":13}],12:[function(require,module,exports){ "use strict"; +var warning = require("react/lib/warning"); var PropTypes = require("./PropTypes"); +function deprecatedMethod(routerMethodName, fn) { + return function () { + warning(false, "Router.Navigation is deprecated. Please use this.context.router." + routerMethodName + "() instead"); + + return fn.apply(this, arguments); + }; +} + /** * A mixin for components that modify the URL. * @@ -559,11 +949,11 @@ var PropTypes = require("./PropTypes"); * * var MyLink = React.createClass({ * mixins: [ Router.Navigation ], - * handleClick: function (event) { + * handleClick(event) { * event.preventDefault(); * this.transitionTo('aRoute', { the: 'params' }, { the: 'query' }); * }, - * render: function () { + * render() { * return ( * <a onClick={this.handleClick}>Click me!</a> * ); @@ -573,97 +963,62 @@ var PropTypes = require("./PropTypes"); var Navigation = { contextTypes: { - makePath: PropTypes.func.isRequired, - makeHref: PropTypes.func.isRequired, - transitionTo: PropTypes.func.isRequired, - replaceWith: PropTypes.func.isRequired, - goBack: PropTypes.func.isRequired + router: PropTypes.router.isRequired }, /** * Returns an absolute URL path created from the given route * name, URL parameters, and query values. */ - makePath: function makePath(to, params, query) { - return this.context.makePath(to, params, query); - }, + makePath: deprecatedMethod("makePath", function (to, params, query) { + return this.context.router.makePath(to, params, query); + }), /** * Returns a string that may safely be used as the href of a * link to the route with the given name. */ - makeHref: function makeHref(to, params, query) { - return this.context.makeHref(to, params, query); - }, + makeHref: deprecatedMethod("makeHref", function (to, params, query) { + return this.context.router.makeHref(to, params, query); + }), /** * Transitions to the URL specified in the arguments by pushing * a new URL onto the history stack. */ - transitionTo: function transitionTo(to, params, query) { - this.context.transitionTo(to, params, query); - }, + transitionTo: deprecatedMethod("transitionTo", function (to, params, query) { + this.context.router.transitionTo(to, params, query); + }), /** * Transitions to the URL specified in the arguments by replacing * the current URL in the history stack. */ - replaceWith: function replaceWith(to, params, query) { - this.context.replaceWith(to, params, query); - }, + replaceWith: deprecatedMethod("replaceWith", function (to, params, query) { + this.context.router.replaceWith(to, params, query); + }), /** * Transitions to the previous URL. */ - goBack: function goBack() { - return this.context.goBack(); - } + goBack: deprecatedMethod("goBack", function () { + return this.context.router.goBack(); + }) }; module.exports = Navigation; -},{"./PropTypes":11}],9:[function(require,module,exports){ -"use strict"; - -var PropTypes = require("./PropTypes"); - -/** - * Provides the router with context for Router.Navigation. - */ -var NavigationContext = { - - childContextTypes: { - makePath: PropTypes.func.isRequired, - makeHref: PropTypes.func.isRequired, - transitionTo: PropTypes.func.isRequired, - replaceWith: PropTypes.func.isRequired, - goBack: PropTypes.func.isRequired - }, - - getChildContext: function getChildContext() { - return { - makePath: this.constructor.makePath.bind(this.constructor), - makeHref: this.constructor.makeHref.bind(this.constructor), - transitionTo: this.constructor.transitionTo.bind(this.constructor), - replaceWith: this.constructor.replaceWith.bind(this.constructor), - goBack: this.constructor.goBack.bind(this.constructor) - }; - } - -}; - -module.exports = NavigationContext; -},{"./PropTypes":11}],10:[function(require,module,exports){ +},{"./PropTypes":14,"react/lib/warning":212}],13:[function(require,module,exports){ "use strict"; var invariant = require("react/lib/invariant"); -var merge = require("qs/lib/utils").merge; +var objectAssign = require("object-assign"); var qs = require("qs"); var paramCompileMatcher = /:([a-zA-Z_$][a-zA-Z0-9_$]*)|[*.()\[\]\\+|{}^$]/g; var paramInjectMatcher = /:([a-zA-Z_$][a-zA-Z0-9_$?]*[?]?)|[*]/g; var paramInjectTrailingSlashMatcher = /\/\/\?|\/\?\/|\/\?/g; -var queryMatcher = /\?(.+)/; +var queryMatcher = /\?(.*)$/; var _compiledPatterns = {}; @@ -795,39 +1150,51 @@ var PathUtils = { withQuery: function withQuery(path, query) { var existingQuery = PathUtils.extractQuery(path); - if (existingQuery) query = query ? merge(existingQuery, query) : existingQuery; + if (existingQuery) query = query ? objectAssign(existingQuery, query) : existingQuery; - var queryString = qs.stringify(query, { indices: false }); + var queryString = qs.stringify(query, { arrayFormat: "brackets" }); if (queryString) { return PathUtils.withoutQuery(path) + "?" + queryString; - }return path; + }return PathUtils.withoutQuery(path); } }; module.exports = PathUtils; -},{"qs":38,"qs/lib/utils":42,"react/lib/invariant":182}],11:[function(require,module,exports){ +},{"object-assign":41,"qs":4,"react/lib/invariant":191}],14:[function(require,module,exports){ "use strict"; var assign = require("react/lib/Object.assign"); var ReactPropTypes = require("react").PropTypes; +var Route = require("./Route"); -var PropTypes = assign({ +var PropTypes = assign({}, ReactPropTypes, { /** - * Requires that the value of a prop be falsy. + * Indicates that a prop should be falsy. */ falsy: function falsy(props, propName, componentName) { if (props[propName]) { return new Error("<" + componentName + "> may not have a \"" + propName + "\" prop"); } - } + }, -}, ReactPropTypes); + /** + * Indicates that a prop should be a Route object. + */ + route: ReactPropTypes.instanceOf(Route), + + /** + * Indicates that a prop should be a Router object. + */ + //router: ReactPropTypes.instanceOf(Router) // TODO + router: ReactPropTypes.func + +}); module.exports = PropTypes; -},{"react":"react","react/lib/Object.assign":70}],12:[function(require,module,exports){ +},{"./Route":16,"react":"react","react/lib/Object.assign":69}],15:[function(require,module,exports){ "use strict"; /** @@ -840,10 +1207,10 @@ function Redirect(to, params, query) { } module.exports = Redirect; -},{}],13:[function(require,module,exports){ +},{}],16:[function(require,module,exports){ "use strict"; -var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; @@ -869,7 +1236,33 @@ var Route = (function () { this.handler = handler; } - _prototypeProperties(Route, { + _createClass(Route, { + appendChild: { + + /** + * Appends the given route to this route's child routes. + */ + + value: function appendChild(route) { + invariant(route instanceof Route, "route.appendChild must use a valid Route"); + + if (!this.childRoutes) this.childRoutes = []; + + this.childRoutes.push(route); + } + }, + toString: { + value: function toString() { + var string = "<Route"; + + if (this.name) string += " name=\"" + this.name + "\""; + + string += " path=\"" + this.path + "\">"; + + return string; + } + } + }, { createRoute: { /** @@ -928,7 +1321,7 @@ var Route = (function () { if (path && !(options.isDefault || options.isNotFound)) { if (PathUtils.isAbsolute(path)) { if (parentRoute) { - invariant(parentRoute.paramNames.length === 0, "You cannot nest path \"%s\" inside \"%s\"; the parent requires URL parameters", path, parentRoute.path); + invariant(path === parentRoute.path || parentRoute.paramNames.length === 0, "You cannot nest path \"%s\" inside \"%s\"; the parent requires URL parameters", path, parentRoute.path); } } else if (parentRoute) { // Relative paths extend their parent. @@ -968,9 +1361,7 @@ var Route = (function () { } return route; - }, - writable: true, - configurable: true + } }, createDefaultRoute: { @@ -981,9 +1372,7 @@ var Route = (function () { value: function createDefaultRoute(options) { return Route.createRoute(assign({}, options, { isDefault: true })); - }, - writable: true, - configurable: true + } }, createNotFoundRoute: { @@ -994,9 +1383,7 @@ var Route = (function () { value: function createNotFoundRoute(options) { return Route.createRoute(assign({}, options, { isNotFound: true })); - }, - writable: true, - configurable: true + } }, createRedirect: { @@ -1020,39 +1407,7 @@ var Route = (function () { transition.redirect(options.to, options.params || params, options.query || query); } })); - }, - writable: true, - configurable: true - } - }, { - appendChild: { - - /** - * Appends the given route to this route's child routes. - */ - - value: function appendChild(route) { - invariant(route instanceof Route, "route.appendChild must use a valid Route"); - - if (!this.childRoutes) this.childRoutes = []; - - this.childRoutes.push(route); - }, - writable: true, - configurable: true - }, - toString: { - value: function toString() { - var string = "<Route"; - - if (this.name) string += " name=\"" + this.name + "\""; - - string += " path=\"" + this.path + "\">"; - - return string; - }, - writable: true, - configurable: true + } } }); @@ -1060,62 +1415,7 @@ var Route = (function () { })(); module.exports = Route; -},{"./PathUtils":10,"react/lib/Object.assign":70,"react/lib/invariant":182,"react/lib/warning":202}],14:[function(require,module,exports){ -"use strict"; - -var React = require("react"); -var assign = require("react/lib/Object.assign"); -var PropTypes = require("./PropTypes"); - -var REF_NAME = "__routeHandler__"; - -var RouteHandlerMixin = { - - contextTypes: { - getRouteAtDepth: PropTypes.func.isRequired, - setRouteComponentAtDepth: PropTypes.func.isRequired, - routeHandlers: PropTypes.array.isRequired - }, - - childContextTypes: { - routeHandlers: PropTypes.array.isRequired - }, - - getChildContext: function getChildContext() { - return { - routeHandlers: this.context.routeHandlers.concat([this]) - }; - }, - - componentDidMount: function componentDidMount() { - this._updateRouteComponent(this.refs[REF_NAME]); - }, - - componentDidUpdate: function componentDidUpdate() { - this._updateRouteComponent(this.refs[REF_NAME]); - }, - - componentWillUnmount: function componentWillUnmount() { - this._updateRouteComponent(null); - }, - - _updateRouteComponent: function _updateRouteComponent(component) { - this.context.setRouteComponentAtDepth(this.getRouteDepth(), component); - }, - - getRouteDepth: function getRouteDepth() { - return this.context.routeHandlers.length; - }, - - createChildRouteHandler: function createChildRouteHandler(props) { - var route = this.context.getRouteAtDepth(this.getRouteDepth()); - return route ? React.createElement(route.handler, assign({}, props || this.props, { ref: REF_NAME })) : null; - } - -}; - -module.exports = RouteHandlerMixin; -},{"./PropTypes":11,"react":"react","react/lib/Object.assign":70}],15:[function(require,module,exports){ +},{"./PathUtils":13,"react/lib/Object.assign":69,"react/lib/invariant":191,"react/lib/warning":212}],17:[function(require,module,exports){ "use strict"; var invariant = require("react/lib/invariant"); @@ -1191,11 +1491,20 @@ var ScrollHistory = { }; module.exports = ScrollHistory; -},{"./getWindowScrollPosition":30,"react/lib/ExecutionEnvironment":64,"react/lib/invariant":182}],16:[function(require,module,exports){ +},{"./getWindowScrollPosition":32,"react/lib/ExecutionEnvironment":62,"react/lib/invariant":191}],18:[function(require,module,exports){ "use strict"; +var warning = require("react/lib/warning"); var PropTypes = require("./PropTypes"); +function deprecatedMethod(routerMethodName, fn) { + return function () { + warning(false, "Router.State is deprecated. Please use this.context.router." + routerMethodName + "() instead"); + + return fn.apply(this, arguments); + }; +} + /** * A mixin for components that need to know the path, routes, URL * params and query that are currently active. @@ -1204,7 +1513,7 @@ var PropTypes = require("./PropTypes"); * * var AboutLink = React.createClass({ * mixins: [ Router.State ], - * render: function () { + * render() { * var className = this.props.className; * * if (this.isActive('about')) @@ -1217,158 +1526,56 @@ var PropTypes = require("./PropTypes"); var State = { contextTypes: { - getCurrentPath: PropTypes.func.isRequired, - getCurrentRoutes: PropTypes.func.isRequired, - getCurrentPathname: PropTypes.func.isRequired, - getCurrentParams: PropTypes.func.isRequired, - getCurrentQuery: PropTypes.func.isRequired, - isActive: PropTypes.func.isRequired + router: PropTypes.router.isRequired }, /** * Returns the current URL path. */ - getPath: function getPath() { - return this.context.getCurrentPath(); - }, - - /** - * Returns an array of the routes that are currently active. - */ - getRoutes: function getRoutes() { - return this.context.getCurrentRoutes(); - }, + getPath: deprecatedMethod("getCurrentPath", function () { + return this.context.router.getCurrentPath(); + }), /** * Returns the current URL path without the query string. */ - getPathname: function getPathname() { - return this.context.getCurrentPathname(); - }, + getPathname: deprecatedMethod("getCurrentPathname", function () { + return this.context.router.getCurrentPathname(); + }), /** * Returns an object of the URL params that are currently active. */ - getParams: function getParams() { - return this.context.getCurrentParams(); - }, + getParams: deprecatedMethod("getCurrentParams", function () { + return this.context.router.getCurrentParams(); + }), /** * Returns an object of the query params that are currently active. */ - getQuery: function getQuery() { - return this.context.getCurrentQuery(); - }, - - /** - * A helper method to determine if a given route, params, and query - * are active. - */ - isActive: function isActive(to, params, query) { - return this.context.isActive(to, params, query); - } - -}; - -module.exports = State; -},{"./PropTypes":11}],17:[function(require,module,exports){ -"use strict"; - -var assign = require("react/lib/Object.assign"); -var PropTypes = require("./PropTypes"); -var PathUtils = require("./PathUtils"); - -function routeIsActive(activeRoutes, routeName) { - return activeRoutes.some(function (route) { - return route.name === routeName; - }); -} - -function paramsAreActive(activeParams, params) { - for (var property in params) if (String(activeParams[property]) !== String(params[property])) { - return false; - }return true; -} - -function queryIsActive(activeQuery, query) { - for (var property in query) if (String(activeQuery[property]) !== String(query[property])) { - return false; - }return true; -} - -/** - * Provides the router with context for Router.State. - */ -var StateContext = { - - /** - * Returns the current URL path + query string. - */ - getCurrentPath: function getCurrentPath() { - return this.state.path; - }, - - /** - * Returns a read-only array of the currently active routes. - */ - getCurrentRoutes: function getCurrentRoutes() { - return this.state.routes.slice(0); - }, - - /** - * Returns the current URL path without the query string. - */ - getCurrentPathname: function getCurrentPathname() { - return this.state.pathname; - }, + getQuery: deprecatedMethod("getCurrentQuery", function () { + return this.context.router.getCurrentQuery(); + }), /** - * Returns a read-only object of the currently active URL parameters. - */ - getCurrentParams: function getCurrentParams() { - return assign({}, this.state.params); - }, - - /** - * Returns a read-only object of the currently active query parameters. + * Returns an array of the routes that are currently active. */ - getCurrentQuery: function getCurrentQuery() { - return assign({}, this.state.query); - }, + getRoutes: deprecatedMethod("getCurrentRoutes", function () { + return this.context.router.getCurrentRoutes(); + }), /** - * Returns true if the given route, params, and query are active. + * A helper method to determine if a given route, params, and query + * are active. */ - isActive: function isActive(to, params, query) { - if (PathUtils.isAbsolute(to)) { - return to === this.state.path; - }return routeIsActive(this.state.routes, to) && paramsAreActive(this.state.params, params) && (query == null || queryIsActive(this.state.query, query)); - }, - - childContextTypes: { - getCurrentPath: PropTypes.func.isRequired, - getCurrentRoutes: PropTypes.func.isRequired, - getCurrentPathname: PropTypes.func.isRequired, - getCurrentParams: PropTypes.func.isRequired, - getCurrentQuery: PropTypes.func.isRequired, - isActive: PropTypes.func.isRequired - }, - - getChildContext: function getChildContext() { - return { - getCurrentPath: this.getCurrentPath, - getCurrentRoutes: this.getCurrentRoutes, - getCurrentPathname: this.getCurrentPathname, - getCurrentParams: this.getCurrentParams, - getCurrentQuery: this.getCurrentQuery, - isActive: this.isActive - }; - } + isActive: deprecatedMethod("isActive", function (to, params, query) { + return this.context.router.isActive(to, params, query); + }) }; -module.exports = StateContext; -},{"./PathUtils":10,"./PropTypes":11,"react/lib/Object.assign":70}],18:[function(require,module,exports){ +module.exports = State; +},{"./PropTypes":14,"react/lib/warning":212}],19:[function(require,module,exports){ "use strict"; /* jshint -W058 */ @@ -1444,7 +1651,7 @@ Transition.to = function (transition, routes, params, query, callback) { }; module.exports = Transition; -},{"./Cancellation":4,"./Redirect":12}],19:[function(require,module,exports){ +},{"./Cancellation":9,"./Redirect":15}],20:[function(require,module,exports){ "use strict"; /** @@ -1470,7 +1677,7 @@ var LocationActions = { }; module.exports = LocationActions; -},{}],20:[function(require,module,exports){ +},{}],21:[function(require,module,exports){ "use strict"; var LocationActions = require("../actions/LocationActions"); @@ -1500,7 +1707,7 @@ var ImitateBrowserBehavior = { }; module.exports = ImitateBrowserBehavior; -},{"../actions/LocationActions":19}],21:[function(require,module,exports){ +},{"../actions/LocationActions":20}],22:[function(require,module,exports){ "use strict"; /** @@ -1516,12 +1723,56 @@ var ScrollToTopBehavior = { }; module.exports = ScrollToTopBehavior; -},{}],22:[function(require,module,exports){ +},{}],23:[function(require,module,exports){ "use strict"; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + +/** + * This component is necessary to get around a context warning + * present in React 0.13.0. It sovles this by providing a separation + * between the "owner" and "parent" contexts. + */ + var React = require("react"); -var Configuration = require("../Configuration"); + +var ContextWrapper = (function (_React$Component) { + function ContextWrapper() { + _classCallCheck(this, ContextWrapper); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(ContextWrapper, _React$Component); + + _createClass(ContextWrapper, { + render: { + value: function render() { + return this.props.children; + } + } + }); + + return ContextWrapper; +})(React.Component); + +module.exports = ContextWrapper; +},{"react":"react"}],24:[function(require,module,exports){ +"use strict"; + +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var PropTypes = require("../PropTypes"); +var RouteHandler = require("./RouteHandler"); +var Route = require("./Route"); /** * A <DefaultRoute> component is a special kind of <Route> that @@ -1529,32 +1780,49 @@ var PropTypes = require("../PropTypes"); * Only one such route may be used at any given level in the * route hierarchy. */ -var DefaultRoute = React.createClass({ - - displayName: "DefaultRoute", - mixins: [Configuration], +var DefaultRoute = (function (_Route) { + function DefaultRoute() { + _classCallCheck(this, DefaultRoute); - propTypes: { - name: PropTypes.string, - path: PropTypes.falsy, - children: PropTypes.falsy, - handler: PropTypes.func.isRequired + if (_Route != null) { + _Route.apply(this, arguments); + } } -}); + _inherits(DefaultRoute, _Route); + + return DefaultRoute; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +DefaultRoute.propTypes = { + name: PropTypes.string, + path: PropTypes.falsy, + children: PropTypes.falsy, + handler: PropTypes.func.isRequired +}; + +DefaultRoute.defaultProps = { + handler: RouteHandler +}; module.exports = DefaultRoute; -},{"../Configuration":5,"../PropTypes":11,"react":"react"}],23:[function(require,module,exports){ +},{"../PropTypes":14,"./Route":28,"./RouteHandler":29}],25:[function(require,module,exports){ "use strict"; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var React = require("react"); -var classSet = require("react/lib/cx"); var assign = require("react/lib/Object.assign"); -var Navigation = require("../Navigation"); -var State = require("../State"); var PropTypes = require("../PropTypes"); -var Route = require("../Route"); function isLeftClickEvent(event) { return event.button === 0; @@ -1582,88 +1850,116 @@ function isModifiedEvent(event) { * * <Link to="showPost" params={{ postID: "123" }} query={{ show:true }}/> */ -var Link = React.createClass({ - displayName: "Link", +var Link = (function (_React$Component) { + function Link() { + _classCallCheck(this, Link); - mixins: [Navigation, State], + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } - propTypes: { - activeClassName: PropTypes.string.isRequired, - to: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Route)]), - params: PropTypes.object, - query: PropTypes.object, - activeStyle: PropTypes.object, - onClick: PropTypes.func - }, + _inherits(Link, _React$Component); - getDefaultProps: function getDefaultProps() { - return { - activeClassName: "active" - }; - }, + _createClass(Link, { + handleClick: { + value: function handleClick(event) { + var allowTransition = true; + var clickResult; - handleClick: function handleClick(event) { - var allowTransition = true; - var clickResult; + if (this.props.onClick) clickResult = this.props.onClick(event); - if (this.props.onClick) clickResult = this.props.onClick(event); + if (isModifiedEvent(event) || !isLeftClickEvent(event)) { + return; + }if (clickResult === false || event.defaultPrevented === true) allowTransition = false; - if (isModifiedEvent(event) || !isLeftClickEvent(event)) { - return; - }if (clickResult === false || event.defaultPrevented === true) allowTransition = false; + event.preventDefault(); - event.preventDefault(); + if (allowTransition) this.context.router.transitionTo(this.props.to, this.props.params, this.props.query); + } + }, + getHref: { - if (allowTransition) this.transitionTo(this.props.to, this.props.params, this.props.query); - }, + /** + * Returns the value of the "href" attribute to use on the DOM element. + */ - /** - * Returns the value of the "href" attribute to use on the DOM element. - */ - getHref: function getHref() { - return this.makeHref(this.props.to, this.props.params, this.props.query); - }, + value: function getHref() { + return this.context.router.makeHref(this.props.to, this.props.params, this.props.query); + } + }, + getClassName: { - /** - * Returns the value of the "class" attribute to use on the DOM element, which contains - * the value of the activeClassName property when this <Link> is active. - */ - getClassName: function getClassName() { - var classNames = {}; + /** + * Returns the value of the "class" attribute to use on the DOM element, which contains + * the value of the activeClassName property when this <Link> is active. + */ - if (this.props.className) classNames[this.props.className] = true; + value: function getClassName() { + var className = this.props.className; - if (this.getActiveState()) classNames[this.props.activeClassName] = true; + if (this.getActiveState()) className += " " + this.props.activeClassName; - return classSet(classNames); - }, + return className; + } + }, + getActiveState: { + value: function getActiveState() { + return this.context.router.isActive(this.props.to, this.props.params, this.props.query); + } + }, + render: { + value: function render() { + var props = assign({}, this.props, { + href: this.getHref(), + className: this.getClassName(), + onClick: this.handleClick.bind(this) + }); - getActiveState: function getActiveState() { - return this.isActive(this.props.to, this.props.params, this.props.query); - }, + if (props.activeStyle && this.getActiveState()) props.style = props.activeStyle; - render: function render() { - var props = assign({}, this.props, { - href: this.getHref(), - className: this.getClassName(), - onClick: this.handleClick - }); + return React.DOM.a(props, this.props.children); + } + } + }); - if (props.activeStyle && this.getActiveState()) props.style = props.activeStyle; + return Link; +})(React.Component); - return React.DOM.a(props, this.props.children); - } +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 -}); +Link.contextTypes = { + router: PropTypes.router.isRequired +}; + +Link.propTypes = { + activeClassName: PropTypes.string.isRequired, + to: PropTypes.oneOfType([PropTypes.string, PropTypes.route]).isRequired, + params: PropTypes.object, + query: PropTypes.object, + activeStyle: PropTypes.object, + onClick: PropTypes.func +}; + +Link.defaultProps = { + activeClassName: "active", + className: "" +}; module.exports = Link; -},{"../Navigation":8,"../PropTypes":11,"../Route":13,"../State":16,"react":"react","react/lib/Object.assign":70,"react/lib/cx":160}],24:[function(require,module,exports){ +},{"../PropTypes":14,"react":"react","react/lib/Object.assign":69}],26:[function(require,module,exports){ "use strict"; -var React = require("react"); -var Configuration = require("../Configuration"); +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var PropTypes = require("../PropTypes"); +var RouteHandler = require("./RouteHandler"); +var Route = require("./Route"); /** * A <NotFoundRoute> is a special kind of <Route> that @@ -1672,56 +1968,95 @@ var PropTypes = require("../PropTypes"); * Only one such route may be used at any given level in the * route hierarchy. */ -var NotFoundRoute = React.createClass({ - - displayName: "NotFoundRoute", - mixins: [Configuration], +var NotFoundRoute = (function (_Route) { + function NotFoundRoute() { + _classCallCheck(this, NotFoundRoute); - propTypes: { - name: PropTypes.string, - path: PropTypes.falsy, - children: PropTypes.falsy, - handler: PropTypes.func.isRequired + if (_Route != null) { + _Route.apply(this, arguments); + } } -}); + _inherits(NotFoundRoute, _Route); + + return NotFoundRoute; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +NotFoundRoute.propTypes = { + name: PropTypes.string, + path: PropTypes.falsy, + children: PropTypes.falsy, + handler: PropTypes.func.isRequired +}; + +NotFoundRoute.defaultProps = { + handler: RouteHandler +}; module.exports = NotFoundRoute; -},{"../Configuration":5,"../PropTypes":11,"react":"react"}],25:[function(require,module,exports){ +},{"../PropTypes":14,"./Route":28,"./RouteHandler":29}],27:[function(require,module,exports){ "use strict"; -var React = require("react"); -var Configuration = require("../Configuration"); +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var PropTypes = require("../PropTypes"); +var Route = require("./Route"); /** * A <Redirect> component is a special kind of <Route> that always * redirects to another route when it matches. */ -var Redirect = React.createClass({ - displayName: "Redirect", +var Redirect = (function (_Route) { + function Redirect() { + _classCallCheck(this, Redirect); - mixins: [Configuration], - - propTypes: { - path: PropTypes.string, - from: PropTypes.string, // Alias for path. - to: PropTypes.string, - handler: PropTypes.falsy + if (_Route != null) { + _Route.apply(this, arguments); + } } -}); + _inherits(Redirect, _Route); + + return Redirect; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +Redirect.propTypes = { + path: PropTypes.string, + from: PropTypes.string, // Alias for path. + to: PropTypes.string, + handler: PropTypes.falsy +}; + +// Redirects should not have a default handler +Redirect.defaultProps = {}; module.exports = Redirect; -},{"../Configuration":5,"../PropTypes":11,"react":"react"}],26:[function(require,module,exports){ +},{"../PropTypes":14,"./Route":28}],28:[function(require,module,exports){ "use strict"; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var React = require("react"); -var Configuration = require("../Configuration"); +var invariant = require("react/lib/invariant"); var PropTypes = require("../PropTypes"); var RouteHandler = require("./RouteHandler"); + /** * <Route> components specify components that are rendered to the page when the * URL matches a given pattern. @@ -1762,52 +2097,147 @@ var RouteHandler = require("./RouteHandler"); * * If no handler is provided for the route, it will render a matched child route. */ -var Route = React.createClass({ - displayName: "Route", +var Route = (function (_React$Component) { + function Route() { + _classCallCheck(this, Route); - mixins: [Configuration], + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } - propTypes: { - name: PropTypes.string, - path: PropTypes.string, - handler: PropTypes.func, - ignoreScrollBehavior: PropTypes.bool - }, + _inherits(Route, _React$Component); - getDefaultProps: function getDefaultProps() { - return { - handler: RouteHandler - }; - } + _createClass(Route, { + render: { + value: function render() { + invariant(false, "%s elements are for router configuration only and should not be rendered", this.constructor.name); + } + } + }); -}); + return Route; +})(React.Component); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +Route.propTypes = { + name: PropTypes.string, + path: PropTypes.string, + handler: PropTypes.func, + ignoreScrollBehavior: PropTypes.bool +}; + +Route.defaultProps = { + handler: RouteHandler +}; module.exports = Route; -},{"../Configuration":5,"../PropTypes":11,"./RouteHandler":27,"react":"react"}],27:[function(require,module,exports){ +},{"../PropTypes":14,"./RouteHandler":29,"react":"react","react/lib/invariant":191}],29:[function(require,module,exports){ "use strict"; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var React = require("react"); -var RouteHandlerMixin = require("../RouteHandlerMixin"); +var ContextWrapper = require("./ContextWrapper"); +var assign = require("react/lib/Object.assign"); +var PropTypes = require("../PropTypes"); + +var REF_NAME = "__routeHandler__"; /** * A <RouteHandler> component renders the active child route handler * when routes are nested. */ -var RouteHandler = React.createClass({ - displayName: "RouteHandler", +var RouteHandler = (function (_React$Component) { + function RouteHandler() { + _classCallCheck(this, RouteHandler); - mixins: [RouteHandlerMixin], - - render: function render() { - return this.createChildRouteHandler(); + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } } -}); + _inherits(RouteHandler, _React$Component); + + _createClass(RouteHandler, { + getChildContext: { + value: function getChildContext() { + return { + routeDepth: this.context.routeDepth + 1 + }; + } + }, + componentDidMount: { + value: function componentDidMount() { + this._updateRouteComponent(this.refs[REF_NAME]); + } + }, + componentDidUpdate: { + value: function componentDidUpdate() { + this._updateRouteComponent(this.refs[REF_NAME]); + } + }, + componentWillUnmount: { + value: function componentWillUnmount() { + this._updateRouteComponent(null); + } + }, + _updateRouteComponent: { + value: function _updateRouteComponent(component) { + this.context.router.setRouteComponentAtDepth(this.getRouteDepth(), component); + } + }, + getRouteDepth: { + value: function getRouteDepth() { + return this.context.routeDepth; + } + }, + createChildRouteHandler: { + value: function createChildRouteHandler(props) { + var route = this.context.router.getRouteAtDepth(this.getRouteDepth()); + return route ? React.createElement(route.handler, assign({}, props || this.props, { ref: REF_NAME })) : null; + } + }, + render: { + value: function render() { + var handler = this.createChildRouteHandler(); + // <script/> for things like <CSSTransitionGroup/> that don't like null + return handler ? React.createElement( + ContextWrapper, + null, + handler + ) : React.createElement("script", null); + } + } + }); + + return RouteHandler; +})(React.Component); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +RouteHandler.contextTypes = { + routeDepth: PropTypes.number.isRequired, + router: PropTypes.router.isRequired +}; + +RouteHandler.childContextTypes = { + routeDepth: PropTypes.number.isRequired +}; module.exports = RouteHandler; -},{"../RouteHandlerMixin":14,"react":"react"}],28:[function(require,module,exports){ +},{"../PropTypes":14,"./ContextWrapper":23,"react":"react","react/lib/Object.assign":69}],30:[function(require,module,exports){ (function (process){ "use strict"; @@ -1822,9 +2252,7 @@ var HashLocation = require("./locations/HashLocation"); var HistoryLocation = require("./locations/HistoryLocation"); var RefreshLocation = require("./locations/RefreshLocation"); var StaticLocation = require("./locations/StaticLocation"); -var NavigationContext = require("./NavigationContext"); var ScrollHistory = require("./ScrollHistory"); -var StateContext = require("./StateContext"); var createRoutesFromReactChildren = require("./createRoutesFromReactChildren"); var isReactChildren = require("./isReactChildren"); var Transition = require("./Transition"); @@ -1887,6 +2315,24 @@ function addRoutesToNamedRoutes(routes, namedRoutes) { } } +function routeIsActive(activeRoutes, routeName) { + return activeRoutes.some(function (route) { + return route.name === routeName; + }); +} + +function paramsAreActive(activeParams, params) { + for (var property in params) if (String(activeParams[property]) !== String(params[property])) { + return false; + }return true; +} + +function queryIsActive(activeQuery, query) { + for (var property in query) if (String(activeQuery[property]) !== String(query[property])) { + return false; + }return true; +} + /** * Creates and returns a new router using the given options. A router * is a ReactComponent class that knows how to react to changes in the @@ -1946,9 +2392,9 @@ function createRouter(options) { }, clearAllRoutes: function clearAllRoutes() { - this.cancelPendingTransition(); - this.namedRoutes = {}; - this.routes = []; + Router.cancelPendingTransition(); + Router.namedRoutes = {}; + Router.routes = []; }, /** @@ -1957,18 +2403,18 @@ function createRouter(options) { addRoutes: function addRoutes(routes) { if (isReactChildren(routes)) routes = createRoutesFromReactChildren(routes); - addRoutesToNamedRoutes(routes, this.namedRoutes); + addRoutesToNamedRoutes(routes, Router.namedRoutes); - this.routes.push.apply(this.routes, routes); + Router.routes.push.apply(Router.routes, routes); }, /** * Replaces routes of this router from the given children object (see ReactChildren). */ replaceRoutes: function replaceRoutes(routes) { - this.clearAllRoutes(); - this.addRoutes(routes); - this.refresh(); + Router.clearAllRoutes(); + Router.addRoutes(routes); + Router.refresh(); }, /** @@ -1977,7 +2423,7 @@ function createRouter(options) { * match can be made. */ match: function match(path) { - return Match.findMatch(this.routes, path); + return Match.findMatch(Router.routes, path); }, /** @@ -1989,7 +2435,7 @@ function createRouter(options) { if (PathUtils.isAbsolute(to)) { path = to; } else { - var route = to instanceof Route ? to : this.namedRoutes[to]; + var route = to instanceof Route ? to : Router.namedRoutes[to]; invariant(route instanceof Route, "Cannot find a route named \"%s\"", to); @@ -2004,7 +2450,7 @@ function createRouter(options) { * to the route with the given name, URL parameters, and query. */ makeHref: function makeHref(to, params, query) { - var path = this.makePath(to, params, query); + var path = Router.makePath(to, params, query); return location === HashLocation ? "#" + path : path; }, @@ -2013,7 +2459,7 @@ function createRouter(options) { * a new URL onto the history stack. */ transitionTo: function transitionTo(to, params, query) { - var path = this.makePath(to, params, query); + var path = Router.makePath(to, params, query); if (pendingTransition) { // Replace so pending location does not stay in history. @@ -2028,7 +2474,7 @@ function createRouter(options) { * the current URL in the history stack. */ replaceWith: function replaceWith(to, params, query) { - location.replace(this.makePath(to, params, query)); + location.replace(Router.makePath(to, params, query)); }, /** @@ -2059,7 +2505,7 @@ function createRouter(options) { if (abortReason instanceof Cancellation) { return; } else if (abortReason instanceof Redirect) { - location.replace(this.makePath(abortReason.to, abortReason.params, abortReason.query)); + location.replace(Router.makePath(abortReason.to, abortReason.params, abortReason.query)); } else { location.pop(); } @@ -2071,7 +2517,7 @@ function createRouter(options) { }, handleLocationChange: function handleLocationChange(change) { - this.dispatch(change.path, change.type); + Router.dispatch(change.path, change.type); }, /** @@ -2091,7 +2537,7 @@ function createRouter(options) { * hooks wait, the transition is fully synchronous. */ dispatch: function dispatch(path, action) { - this.cancelPendingTransition(); + Router.cancelPendingTransition(); var prevPath = state.path; var isRefreshing = action == null; @@ -2102,9 +2548,9 @@ function createRouter(options) { // Record the scroll position as early as possible to // get it before browsers try update it automatically. - if (prevPath && action === LocationActions.PUSH) this.recordScrollPosition(prevPath); + if (prevPath && action === LocationActions.PUSH) Router.recordScrollPosition(prevPath); - var match = this.match(path); + var match = Router.match(path); warning(match != null, "No route matches path \"%s\". Make sure you have <Route path=\"%s\"> somewhere in your routes", path, path); @@ -2132,7 +2578,7 @@ function createRouter(options) { toRoutes = nextRoutes; } - var transition = new Transition(path, this.replaceWith.bind(this, path)); + var transition = new Transition(path, Router.replaceWith.bind(Router, path)); pendingTransition = transition; var fromComponents = mountedComponents.slice(prevRoutes.length - fromRoutes.length); @@ -2161,7 +2607,7 @@ function createRouter(options) { * Router.*Location objects (e.g. Router.HashLocation or Router.HistoryLocation). */ run: function run(callback) { - invariant(!this.isRunning, "Router is already running"); + invariant(!Router.isRunning, "Router is already running"); dispatchHandler = function (error, transition, newState) { if (error) Router.handleError(error); @@ -2173,18 +2619,18 @@ function createRouter(options) { if (transition.abortReason) { Router.handleAbort(transition.abortReason); } else { - callback.call(this, this, nextState = newState); + callback.call(Router, Router, nextState = newState); } }; if (!(location instanceof StaticLocation)) { if (location.addChangeListener) location.addChangeListener(Router.handleLocationChange); - this.isRunning = true; + Router.isRunning = true; } // Bootstrap using the current path. - this.refresh(); + Router.refresh(); }, refresh: function refresh() { @@ -2192,36 +2638,91 @@ function createRouter(options) { }, stop: function stop() { - this.cancelPendingTransition(); + Router.cancelPendingTransition(); if (location.removeChangeListener) location.removeChangeListener(Router.handleLocationChange); - this.isRunning = false; + Router.isRunning = false; + }, + + getLocation: function getLocation() { + return location; }, getScrollBehavior: function getScrollBehavior() { return scrollBehavior; + }, + + getRouteAtDepth: function getRouteAtDepth(routeDepth) { + var routes = state.routes; + return routes && routes[routeDepth]; + }, + + setRouteComponentAtDepth: function setRouteComponentAtDepth(routeDepth, component) { + mountedComponents[routeDepth] = component; + }, + + /** + * Returns the current URL path + query string. + */ + getCurrentPath: function getCurrentPath() { + return state.path; + }, + + /** + * Returns the current URL path without the query string. + */ + getCurrentPathname: function getCurrentPathname() { + return state.pathname; + }, + + /** + * Returns an object of the currently active URL parameters. + */ + getCurrentParams: function getCurrentParams() { + return state.params; + }, + + /** + * Returns an object of the currently active query parameters. + */ + getCurrentQuery: function getCurrentQuery() { + return state.query; + }, + + /** + * Returns an array of the currently active routes. + */ + getCurrentRoutes: function getCurrentRoutes() { + return state.routes; + }, + + /** + * Returns true if the given route, params, and query are active. + */ + isActive: function isActive(to, params, query) { + if (PathUtils.isAbsolute(to)) { + return to === state.path; + }return routeIsActive(state.routes, to) && paramsAreActive(state.params, params) && (query == null || queryIsActive(state.query, query)); } }, - mixins: [NavigationContext, StateContext, ScrollHistory], + mixins: [ScrollHistory], propTypes: { children: PropTypes.falsy }, childContextTypes: { - getRouteAtDepth: React.PropTypes.func.isRequired, - setRouteComponentAtDepth: React.PropTypes.func.isRequired, - routeHandlers: React.PropTypes.array.isRequired + routeDepth: PropTypes.number.isRequired, + router: PropTypes.router.isRequired }, getChildContext: function getChildContext() { return { - getRouteAtDepth: this.getRouteAtDepth, - setRouteComponentAtDepth: this.setRouteComponentAtDepth, - routeHandlers: [this] + routeDepth: 1, + router: Router }; }, @@ -2237,21 +2738,8 @@ function createRouter(options) { Router.stop(); }, - getLocation: function getLocation() { - return location; - }, - - getRouteAtDepth: function getRouteAtDepth(depth) { - var routes = this.state.routes; - return routes && routes[depth]; - }, - - setRouteComponentAtDepth: function setRouteComponentAtDepth(depth, component) { - mountedComponents[depth] = component; - }, - render: function render() { - var route = this.getRouteAtDepth(0); + var route = Router.getRouteAtDepth(0); return route ? React.createElement(route.handler, this.props) : null; } @@ -2266,17 +2754,16 @@ function createRouter(options) { module.exports = createRouter; }).call(this,require('_process')) -},{"./Cancellation":4,"./History":6,"./Match":7,"./NavigationContext":9,"./PathUtils":10,"./PropTypes":11,"./Redirect":12,"./Route":13,"./ScrollHistory":15,"./StateContext":17,"./Transition":18,"./actions/LocationActions":19,"./behaviors/ImitateBrowserBehavior":20,"./createRoutesFromReactChildren":29,"./isReactChildren":31,"./locations/HashLocation":32,"./locations/HistoryLocation":33,"./locations/RefreshLocation":34,"./locations/StaticLocation":35,"./supportsHistory":37,"_process":1,"react":"react","react/lib/ExecutionEnvironment":64,"react/lib/invariant":182,"react/lib/warning":202}],29:[function(require,module,exports){ +},{"./Cancellation":9,"./History":10,"./Match":11,"./PathUtils":13,"./PropTypes":14,"./Redirect":15,"./Route":16,"./ScrollHistory":17,"./Transition":19,"./actions/LocationActions":20,"./behaviors/ImitateBrowserBehavior":21,"./createRoutesFromReactChildren":31,"./isReactChildren":33,"./locations/HashLocation":34,"./locations/HistoryLocation":35,"./locations/RefreshLocation":36,"./locations/StaticLocation":37,"./supportsHistory":40,"_process":1,"react":"react","react/lib/ExecutionEnvironment":62,"react/lib/invariant":191,"react/lib/warning":212}],31:[function(require,module,exports){ "use strict"; /* jshint -W084 */ - var React = require("react"); var assign = require("react/lib/Object.assign"); var warning = require("react/lib/warning"); -var DefaultRouteType = require("./components/DefaultRoute").type; -var NotFoundRouteType = require("./components/NotFoundRoute").type; -var RedirectType = require("./components/Redirect").type; +var DefaultRoute = require("./components/DefaultRoute"); +var NotFoundRoute = require("./components/NotFoundRoute"); +var Redirect = require("./components/Redirect"); var Route = require("./Route"); function checkPropTypes(componentName, propTypes, props) { @@ -2307,15 +2794,15 @@ function createRouteFromReactElement(element) { if (!React.isValidElement(element)) { return; }var type = element.type; - var props = element.props; + var props = assign({}, type.defaultProps, element.props); if (type.propTypes) checkPropTypes(type.displayName, type.propTypes, props); - if (type === DefaultRouteType) { + if (type === DefaultRoute) { return Route.createDefaultRoute(createRouteOptions(props)); - }if (type === NotFoundRouteType) { + }if (type === NotFoundRoute) { return Route.createNotFoundRoute(createRouteOptions(props)); - }if (type === RedirectType) { + }if (type === Redirect) { return Route.createRedirect(createRouteOptions(props)); }return Route.createRoute(createRouteOptions(props), function () { if (props.children) createRoutesFromReactChildren(props.children); @@ -2349,7 +2836,7 @@ function createRoutesFromReactChildren(children) { } module.exports = createRoutesFromReactChildren; -},{"./Route":13,"./components/DefaultRoute":22,"./components/NotFoundRoute":24,"./components/Redirect":25,"react":"react","react/lib/Object.assign":70,"react/lib/warning":202}],30:[function(require,module,exports){ +},{"./Route":16,"./components/DefaultRoute":24,"./components/NotFoundRoute":26,"./components/Redirect":27,"react":"react","react/lib/Object.assign":69,"react/lib/warning":212}],32:[function(require,module,exports){ "use strict"; var invariant = require("react/lib/invariant"); @@ -2368,7 +2855,7 @@ function getWindowScrollPosition() { } module.exports = getWindowScrollPosition; -},{"react/lib/ExecutionEnvironment":64,"react/lib/invariant":182}],31:[function(require,module,exports){ +},{"react/lib/ExecutionEnvironment":62,"react/lib/invariant":191}],33:[function(require,module,exports){ "use strict"; var React = require("react"); @@ -2382,51 +2869,38 @@ function isReactChildren(object) { } module.exports = isReactChildren; -},{"react":"react"}],32:[function(require,module,exports){ +},{"react":"react"}],34:[function(require,module,exports){ "use strict"; var LocationActions = require("../actions/LocationActions"); var History = require("../History"); -/** - * Returns the current URL path from the `hash` portion of the URL, including - * query string. - */ -function getHashPath() { - return decodeURI( - // We can't use window.location.hash here because it's not - // consistent across browsers - Firefox will pre-decode it! - window.location.href.split("#")[1] || ""); -} - +var _listeners = []; +var _isListening = false; var _actionType; -function ensureSlash() { - var path = getHashPath(); - - if (path.charAt(0) === "/") { - return true; - }HashLocation.replace("/" + path); - - return false; -} - -var _changeListeners = []; - function notifyChange(type) { if (type === LocationActions.PUSH) History.length += 1; var change = { - path: getHashPath(), + path: HashLocation.getCurrentPath(), type: type }; - _changeListeners.forEach(function (listener) { - listener(change); + _listeners.forEach(function (listener) { + listener.call(HashLocation, change); }); } -var _isListening = false; +function ensureSlash() { + var path = HashLocation.getCurrentPath(); + + if (path.charAt(0) === "/") { + return true; + }HashLocation.replace("/" + path); + + return false; +} function onHashChange() { if (ensureSlash()) { @@ -2434,8 +2908,9 @@ function onHashChange() { // changed. It was probably caused by the user clicking the Back // button, but may have also been the Forward button or manual // manipulation. So just guess 'pop'. - notifyChange(_actionType || LocationActions.POP); + var curActionType = _actionType; _actionType = null; + notifyChange(curActionType || LocationActions.POP); } } @@ -2445,7 +2920,7 @@ function onHashChange() { var HashLocation = { addChangeListener: function addChangeListener(listener) { - _changeListeners.push(listener); + _listeners.push(listener); // Do this BEFORE listening for hashchange. ensureSlash(); @@ -2462,11 +2937,11 @@ var HashLocation = { }, removeChangeListener: function removeChangeListener(listener) { - _changeListeners = _changeListeners.filter(function (l) { + _listeners = _listeners.filter(function (l) { return l !== listener; }); - if (_changeListeners.length === 0) { + if (_listeners.length === 0) { if (window.removeEventListener) { window.removeEventListener("hashchange", onHashChange, false); } else { @@ -2492,7 +2967,12 @@ var HashLocation = { History.back(); }, - getCurrentPath: getHashPath, + getCurrentPath: function getCurrentPath() { + return decodeURI( + // We can't use window.location.hash here because it's not + // consistent across browsers - Firefox will pre-decode it! + window.location.href.split("#")[1] || ""); + }, toString: function toString() { return "<HashLocation>"; @@ -2501,34 +2981,26 @@ var HashLocation = { }; module.exports = HashLocation; -},{"../History":6,"../actions/LocationActions":19}],33:[function(require,module,exports){ +},{"../History":10,"../actions/LocationActions":20}],35:[function(require,module,exports){ "use strict"; var LocationActions = require("../actions/LocationActions"); var History = require("../History"); -/** - * Returns the current URL path from `window.location`, including query string. - */ -function getWindowPath() { - return decodeURI(window.location.pathname + window.location.search); -} - -var _changeListeners = []; +var _listeners = []; +var _isListening = false; function notifyChange(type) { var change = { - path: getWindowPath(), + path: HistoryLocation.getCurrentPath(), type: type }; - _changeListeners.forEach(function (listener) { - listener(change); + _listeners.forEach(function (listener) { + listener.call(HistoryLocation, change); }); } -var _isListening = false; - function onPopState(event) { if (event.state === undefined) { return; @@ -2543,7 +3015,7 @@ function onPopState(event) { var HistoryLocation = { addChangeListener: function addChangeListener(listener) { - _changeListeners.push(listener); + _listeners.push(listener); if (!_isListening) { if (window.addEventListener) { @@ -2557,11 +3029,11 @@ var HistoryLocation = { }, removeChangeListener: function removeChangeListener(listener) { - _changeListeners = _changeListeners.filter(function (l) { + _listeners = _listeners.filter(function (l) { return l !== listener; }); - if (_changeListeners.length === 0) { + if (_listeners.length === 0) { if (window.addEventListener) { window.removeEventListener("popstate", onPopState, false); } else { @@ -2585,7 +3057,9 @@ var HistoryLocation = { pop: History.back, - getCurrentPath: getWindowPath, + getCurrentPath: function getCurrentPath() { + return decodeURI(window.location.pathname + window.location.search); + }, toString: function toString() { return "<HistoryLocation>"; @@ -2594,7 +3068,7 @@ var HistoryLocation = { }; module.exports = HistoryLocation; -},{"../History":6,"../actions/LocationActions":19}],34:[function(require,module,exports){ +},{"../History":10,"../actions/LocationActions":20}],36:[function(require,module,exports){ "use strict"; var HistoryLocation = require("./HistoryLocation"); @@ -2626,10 +3100,10 @@ var RefreshLocation = { }; module.exports = RefreshLocation; -},{"../History":6,"./HistoryLocation":33}],35:[function(require,module,exports){ +},{"../History":10,"./HistoryLocation":35}],37:[function(require,module,exports){ "use strict"; -var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; @@ -2652,20 +3126,16 @@ var StaticLocation = (function () { this.path = path; } - _prototypeProperties(StaticLocation, null, { + _createClass(StaticLocation, { getCurrentPath: { value: function getCurrentPath() { return this.path; - }, - writable: true, - configurable: true + } }, toString: { value: function toString() { return "<StaticLocation path=\"" + this.path + "\">"; - }, - writable: true, - configurable: true + } } }); @@ -2674,12 +3144,110 @@ var StaticLocation = (function () { // TODO: Include these in the above class definition // once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + StaticLocation.prototype.push = throwCannotModify; StaticLocation.prototype.replace = throwCannotModify; StaticLocation.prototype.pop = throwCannotModify; module.exports = StaticLocation; -},{"react/lib/invariant":182}],36:[function(require,module,exports){ +},{"react/lib/invariant":191}],38:[function(require,module,exports){ +"use strict"; + +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + +var invariant = require("react/lib/invariant"); +var LocationActions = require("../actions/LocationActions"); +var History = require("../History"); + +/** + * A location that is convenient for testing and does not require a DOM. + */ + +var TestLocation = (function () { + function TestLocation(history) { + _classCallCheck(this, TestLocation); + + this.history = history || []; + this.listeners = []; + this._updateHistoryLength(); + } + + _createClass(TestLocation, { + needsDOM: { + get: function () { + return false; + } + }, + _updateHistoryLength: { + value: function _updateHistoryLength() { + History.length = this.history.length; + } + }, + _notifyChange: { + value: function _notifyChange(type) { + var change = { + path: this.getCurrentPath(), + type: type + }; + + for (var i = 0, len = this.listeners.length; i < len; ++i) this.listeners[i].call(this, change); + } + }, + addChangeListener: { + value: function addChangeListener(listener) { + this.listeners.push(listener); + } + }, + removeChangeListener: { + value: function removeChangeListener(listener) { + this.listeners = this.listeners.filter(function (l) { + return l !== listener; + }); + } + }, + push: { + value: function push(path) { + this.history.push(path); + this._updateHistoryLength(); + this._notifyChange(LocationActions.PUSH); + } + }, + replace: { + value: function replace(path) { + invariant(this.history.length, "You cannot replace the current path with no history"); + + this.history[this.history.length - 1] = path; + + this._notifyChange(LocationActions.REPLACE); + } + }, + pop: { + value: function pop() { + this.history.pop(); + this._updateHistoryLength(); + this._notifyChange(LocationActions.POP); + } + }, + getCurrentPath: { + value: function getCurrentPath() { + return this.history[this.history.length - 1]; + } + }, + toString: { + value: function toString() { + return "<TestLocation>"; + } + } + }); + + return TestLocation; +})(); + +module.exports = TestLocation; +},{"../History":10,"../actions/LocationActions":20,"react/lib/invariant":191}],39:[function(require,module,exports){ "use strict"; var createRouter = require("./createRouter"); @@ -2730,7 +3298,7 @@ function runRouter(routes, location, callback) { } module.exports = runRouter; -},{"./createRouter":28}],37:[function(require,module,exports){ +},{"./createRouter":30}],40:[function(require,module,exports){ "use strict"; function supportsHistory() { @@ -2747,401 +3315,37 @@ function supportsHistory() { } module.exports = supportsHistory; -},{}],38:[function(require,module,exports){ -module.exports = require('./lib/'); - -},{"./lib/":39}],39:[function(require,module,exports){ -// Load modules - -var Stringify = require('./stringify'); -var Parse = require('./parse'); - - -// Declare internals - -var internals = {}; - - -module.exports = { - stringify: Stringify, - parse: Parse -}; - -},{"./parse":40,"./stringify":41}],40:[function(require,module,exports){ -// Load modules - -var Utils = require('./utils'); - - -// Declare internals - -var internals = { - delimiter: '&', - depth: 5, - arrayLimit: 20, - parameterLimit: 1000 -}; - - -internals.parseValues = function (str, options) { - - var obj = {}; - var parts = str.split(options.delimiter, options.parameterLimit === Infinity ? undefined : options.parameterLimit); - - for (var i = 0, il = parts.length; i < il; ++i) { - var part = parts[i]; - var pos = part.indexOf(']=') === -1 ? part.indexOf('=') : part.indexOf(']=') + 1; - - if (pos === -1) { - obj[Utils.decode(part)] = ''; - } - else { - var key = Utils.decode(part.slice(0, pos)); - var val = Utils.decode(part.slice(pos + 1)); - - if (!obj.hasOwnProperty(key)) { - obj[key] = val; - } - else { - obj[key] = [].concat(obj[key]).concat(val); - } - } - } - - return obj; -}; - - -internals.parseObject = function (chain, val, options) { - - if (!chain.length) { - return val; - } - - var root = chain.shift(); - - var obj = {}; - if (root === '[]') { - obj = []; - obj = obj.concat(internals.parseObject(chain, val, options)); - } - else { - var cleanRoot = root[0] === '[' && root[root.length - 1] === ']' ? root.slice(1, root.length - 1) : root; - var index = parseInt(cleanRoot, 10); - var indexString = '' + index; - if (!isNaN(index) && - root !== cleanRoot && - indexString === cleanRoot && - index >= 0 && - index <= options.arrayLimit) { - - obj = []; - obj[index] = internals.parseObject(chain, val, options); - } - else { - obj[cleanRoot] = internals.parseObject(chain, val, options); - } - } - - return obj; -}; - - -internals.parseKeys = function (key, val, options) { - - if (!key) { - return; - } - - // The regex chunks - - var parent = /^([^\[\]]*)/; - var child = /(\[[^\[\]]*\])/g; - - // Get the parent - - var segment = parent.exec(key); - - // Don't allow them to overwrite object prototype properties - - if (Object.prototype.hasOwnProperty(segment[1])) { - return; - } - - // Stash the parent if it exists - - var keys = []; - if (segment[1]) { - keys.push(segment[1]); - } - - // Loop through children appending to the array until we hit depth - - var i = 0; - while ((segment = child.exec(key)) !== null && i < options.depth) { - - ++i; - if (!Object.prototype.hasOwnProperty(segment[1].replace(/\[|\]/g, ''))) { - keys.push(segment[1]); - } - } - - // If there's a remainder, just add whatever is left - - if (segment) { - keys.push('[' + key.slice(segment.index) + ']'); - } - - return internals.parseObject(keys, val, options); -}; - - -module.exports = function (str, options) { - - if (str === '' || - str === null || - typeof str === 'undefined') { - - return {}; - } - - options = options || {}; - options.delimiter = typeof options.delimiter === 'string' || Utils.isRegExp(options.delimiter) ? options.delimiter : internals.delimiter; - options.depth = typeof options.depth === 'number' ? options.depth : internals.depth; - options.arrayLimit = typeof options.arrayLimit === 'number' ? options.arrayLimit : internals.arrayLimit; - options.parameterLimit = typeof options.parameterLimit === 'number' ? options.parameterLimit : internals.parameterLimit; - - var tempObj = typeof str === 'string' ? internals.parseValues(str, options) : str; - var obj = {}; - - // Iterate over the keys and setup the new object - - var keys = Object.keys(tempObj); - for (var i = 0, il = keys.length; i < il; ++i) { - var key = keys[i]; - var newObj = internals.parseKeys(key, tempObj[key], options); - obj = Utils.merge(obj, newObj); - } - - return Utils.compact(obj); -}; - -},{"./utils":42}],41:[function(require,module,exports){ -// Load modules - -var Utils = require('./utils'); - - -// Declare internals - -var internals = { - delimiter: '&', - indices: true -}; - - -internals.stringify = function (obj, prefix, options) { - - if (Utils.isBuffer(obj)) { - obj = obj.toString(); - } - else if (obj instanceof Date) { - obj = obj.toISOString(); - } - else if (obj === null) { - obj = ''; - } - - if (typeof obj === 'string' || - typeof obj === 'number' || - typeof obj === 'boolean') { - - return [encodeURIComponent(prefix) + '=' + encodeURIComponent(obj)]; - } - - var values = []; - - if (typeof obj === 'undefined') { - return values; - } - - var objKeys = Object.keys(obj); - for (var i = 0, il = objKeys.length; i < il; ++i) { - var key = objKeys[i]; - if (!options.indices && - Array.isArray(obj)) { - - values = values.concat(internals.stringify(obj[key], prefix, options)); - } - else { - values = values.concat(internals.stringify(obj[key], prefix + '[' + key + ']', options)); - } - } - - return values; -}; - - -module.exports = function (obj, options) { - - options = options || {}; - var delimiter = typeof options.delimiter === 'undefined' ? internals.delimiter : options.delimiter; - options.indices = typeof options.indices === 'boolean' ? options.indices : internals.indices; - - var keys = []; - - if (typeof obj !== 'object' || - obj === null) { - - return ''; - } - - var objKeys = Object.keys(obj); - for (var i = 0, il = objKeys.length; i < il; ++i) { - var key = objKeys[i]; - keys = keys.concat(internals.stringify(obj[key], key, options)); - } - - return keys.join(delimiter); -}; - -},{"./utils":42}],42:[function(require,module,exports){ -// Load modules - - -// Declare internals - -var internals = {}; - - -exports.arrayToObject = function (source) { - - var obj = {}; - for (var i = 0, il = source.length; i < il; ++i) { - if (typeof source[i] !== 'undefined') { - - obj[i] = source[i]; - } - } - - return obj; -}; - - -exports.merge = function (target, source) { - - if (!source) { - return target; - } - - if (typeof source !== 'object') { - if (Array.isArray(target)) { - target.push(source); - } - else { - target[source] = true; - } - - return target; - } - - if (typeof target !== 'object') { - target = [target].concat(source); - return target; - } - - if (Array.isArray(target) && - !Array.isArray(source)) { - - target = exports.arrayToObject(target); - } - - var keys = Object.keys(source); - for (var k = 0, kl = keys.length; k < kl; ++k) { - var key = keys[k]; - var value = source[key]; - - if (!target[key]) { - target[key] = value; - } - else { - target[key] = exports.merge(target[key], value); - } - } - - return target; -}; - - -exports.decode = function (str) { - - try { - return decodeURIComponent(str.replace(/\+/g, ' ')); - } catch (e) { - return str; - } -}; - - -exports.compact = function (obj, refs) { - - if (typeof obj !== 'object' || - obj === null) { - - return obj; - } - - refs = refs || []; - var lookup = refs.indexOf(obj); - if (lookup !== -1) { - return refs[lookup]; - } - - refs.push(obj); - - if (Array.isArray(obj)) { - var compacted = []; - - for (var i = 0, il = obj.length; i < il; ++i) { - if (typeof obj[i] !== 'undefined') { - compacted.push(obj[i]); - } - } - - return compacted; - } - - var keys = Object.keys(obj); - for (i = 0, il = keys.length; i < il; ++i) { - var key = keys[i]; - obj[key] = exports.compact(obj[key], refs); - } - - return obj; -}; - +},{}],41:[function(require,module,exports){ +'use strict'; -exports.isRegExp = function (obj) { - return Object.prototype.toString.call(obj) === '[object RegExp]'; -}; +function ToObject(val) { + if (val == null) { + throw new TypeError('Object.assign cannot be called with null or undefined'); + } + return Object(val); +} -exports.isBuffer = function (obj) { +module.exports = Object.assign || function (target, source) { + var from; + var keys; + var to = ToObject(target); - if (obj === null || - typeof obj === 'undefined') { + for (var s = 1; s < arguments.length; s++) { + from = arguments[s]; + keys = Object.keys(Object(from)); - return false; - } + for (var i = 0; i < keys.length; i++) { + to[keys[i]] = from[keys[i]]; + } + } - return !!(obj.constructor && - obj.constructor.isBuffer && - obj.constructor.isBuffer(obj)); + return to; }; -},{}],43:[function(require,module,exports){ +},{}],42:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -3152,7 +3356,7 @@ exports.isBuffer = function (obj) { * @typechecks static-only */ -"use strict"; +'use strict'; var focusNode = require("./focusNode"); @@ -3166,9 +3370,9 @@ var AutoFocusMixin = { module.exports = AutoFocusMixin; -},{"./focusNode":167}],44:[function(require,module,exports){ +},{"./focusNode":175}],43:[function(require,module,exports){ /** - * Copyright 2013 Facebook, Inc. + * Copyright 2013-2015 Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -3179,19 +3383,48 @@ module.exports = AutoFocusMixin; * @typechecks static-only */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); var EventPropagators = require("./EventPropagators"); var ExecutionEnvironment = require("./ExecutionEnvironment"); +var FallbackCompositionState = require("./FallbackCompositionState"); +var SyntheticCompositionEvent = require("./SyntheticCompositionEvent"); var SyntheticInputEvent = require("./SyntheticInputEvent"); var keyOf = require("./keyOf"); +var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space +var START_KEYCODE = 229; + +var canUseCompositionEvent = ( + ExecutionEnvironment.canUseDOM && + 'CompositionEvent' in window +); + +var documentMode = null; +if (ExecutionEnvironment.canUseDOM && 'documentMode' in document) { + documentMode = document.documentMode; +} + +// Webkit offers a very useful `textInput` event that can be used to +// directly represent `beforeInput`. The IE `textinput` event is not as +// useful, so we don't use it. var canUseTextInputEvent = ( ExecutionEnvironment.canUseDOM && 'TextEvent' in window && - !('documentMode' in document || isPresto()) + !documentMode && + !isPresto() +); + +// In IE9+, we have access to composition events, but the data supplied +// by the native compositionend event may be incorrect. Japanese ideographic +// spaces, for instance (\u3000) are not recorded correctly. +var useFallbackCompositionData = ( + ExecutionEnvironment.canUseDOM && + ( + (!canUseCompositionEvent || documentMode && documentMode > 8 && documentMode <= 11) + ) ); /** @@ -3225,12 +3458,51 @@ var eventTypes = { topLevelTypes.topTextInput, topLevelTypes.topPaste ] + }, + compositionEnd: { + phasedRegistrationNames: { + bubbled: keyOf({onCompositionEnd: null}), + captured: keyOf({onCompositionEndCapture: null}) + }, + dependencies: [ + topLevelTypes.topBlur, + topLevelTypes.topCompositionEnd, + topLevelTypes.topKeyDown, + topLevelTypes.topKeyPress, + topLevelTypes.topKeyUp, + topLevelTypes.topMouseDown + ] + }, + compositionStart: { + phasedRegistrationNames: { + bubbled: keyOf({onCompositionStart: null}), + captured: keyOf({onCompositionStartCapture: null}) + }, + dependencies: [ + topLevelTypes.topBlur, + topLevelTypes.topCompositionStart, + topLevelTypes.topKeyDown, + topLevelTypes.topKeyPress, + topLevelTypes.topKeyUp, + topLevelTypes.topMouseDown + ] + }, + compositionUpdate: { + phasedRegistrationNames: { + bubbled: keyOf({onCompositionUpdate: null}), + captured: keyOf({onCompositionUpdateCapture: null}) + }, + dependencies: [ + topLevelTypes.topBlur, + topLevelTypes.topCompositionUpdate, + topLevelTypes.topKeyDown, + topLevelTypes.topKeyPress, + topLevelTypes.topKeyUp, + topLevelTypes.topMouseDown + ] } }; -// Track characters inserted via keypress and composition events. -var fallbackChars = null; - // Track whether we've ever handled a keypress on the space key. var hasSpaceKeypress = false; @@ -3247,6 +3519,297 @@ function isKeypressCommand(nativeEvent) { ); } + +/** + * Translate native top level events into event types. + * + * @param {string} topLevelType + * @return {object} + */ +function getCompositionEventType(topLevelType) { + switch (topLevelType) { + case topLevelTypes.topCompositionStart: + return eventTypes.compositionStart; + case topLevelTypes.topCompositionEnd: + return eventTypes.compositionEnd; + case topLevelTypes.topCompositionUpdate: + return eventTypes.compositionUpdate; + } +} + +/** + * Does our fallback best-guess model think this event signifies that + * composition has begun? + * + * @param {string} topLevelType + * @param {object} nativeEvent + * @return {boolean} + */ +function isFallbackCompositionStart(topLevelType, nativeEvent) { + return ( + topLevelType === topLevelTypes.topKeyDown && + nativeEvent.keyCode === START_KEYCODE + ); +} + +/** + * Does our fallback mode think that this event is the end of composition? + * + * @param {string} topLevelType + * @param {object} nativeEvent + * @return {boolean} + */ +function isFallbackCompositionEnd(topLevelType, nativeEvent) { + switch (topLevelType) { + case topLevelTypes.topKeyUp: + // Command keys insert or clear IME input. + return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1); + case topLevelTypes.topKeyDown: + // Expect IME keyCode on each keydown. If we get any other + // code we must have exited earlier. + return (nativeEvent.keyCode !== START_KEYCODE); + case topLevelTypes.topKeyPress: + case topLevelTypes.topMouseDown: + case topLevelTypes.topBlur: + // Events are not possible without cancelling IME. + return true; + default: + return false; + } +} + +/** + * Google Input Tools provides composition data via a CustomEvent, + * with the `data` property populated in the `detail` object. If this + * is available on the event object, use it. If not, this is a plain + * composition event and we have nothing special to extract. + * + * @param {object} nativeEvent + * @return {?string} + */ +function getDataFromCustomEvent(nativeEvent) { + var detail = nativeEvent.detail; + if (typeof detail === 'object' && 'data' in detail) { + return detail.data; + } + return null; +} + +// Track the current IME composition fallback object, if any. +var currentComposition = null; + +/** + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {?object} A SyntheticCompositionEvent. + */ +function extractCompositionEvent( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent +) { + var eventType; + var fallbackData; + + if (canUseCompositionEvent) { + eventType = getCompositionEventType(topLevelType); + } else if (!currentComposition) { + if (isFallbackCompositionStart(topLevelType, nativeEvent)) { + eventType = eventTypes.compositionStart; + } + } else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) { + eventType = eventTypes.compositionEnd; + } + + if (!eventType) { + return null; + } + + if (useFallbackCompositionData) { + // The current composition is stored statically and must not be + // overwritten while composition continues. + if (!currentComposition && eventType === eventTypes.compositionStart) { + currentComposition = FallbackCompositionState.getPooled(topLevelTarget); + } else if (eventType === eventTypes.compositionEnd) { + if (currentComposition) { + fallbackData = currentComposition.getData(); + } + } + } + + var event = SyntheticCompositionEvent.getPooled( + eventType, + topLevelTargetID, + nativeEvent + ); + + if (fallbackData) { + // Inject data generated from fallback path into the synthetic event. + // This matches the property of native CompositionEventInterface. + event.data = fallbackData; + } else { + var customData = getDataFromCustomEvent(nativeEvent); + if (customData !== null) { + event.data = customData; + } + } + + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; +} + +/** + * @param {string} topLevelType Record from `EventConstants`. + * @param {object} nativeEvent Native browser event. + * @return {?string} The string corresponding to this `beforeInput` event. + */ +function getNativeBeforeInputChars(topLevelType, nativeEvent) { + switch (topLevelType) { + case topLevelTypes.topCompositionEnd: + return getDataFromCustomEvent(nativeEvent); + case topLevelTypes.topKeyPress: + /** + * If native `textInput` events are available, our goal is to make + * use of them. However, there is a special case: the spacebar key. + * In Webkit, preventing default on a spacebar `textInput` event + * cancels character insertion, but it *also* causes the browser + * to fall back to its default spacebar behavior of scrolling the + * page. + * + * Tracking at: + * https://code.google.com/p/chromium/issues/detail?id=355103 + * + * To avoid this issue, use the keypress event as if no `textInput` + * event is available. + */ + var which = nativeEvent.which; + if (which !== SPACEBAR_CODE) { + return null; + } + + hasSpaceKeypress = true; + return SPACEBAR_CHAR; + + case topLevelTypes.topTextInput: + // Record the characters to be added to the DOM. + var chars = nativeEvent.data; + + // If it's a spacebar character, assume that we have already handled + // it at the keypress level and bail immediately. Android Chrome + // doesn't give us keycodes, so we need to blacklist it. + if (chars === SPACEBAR_CHAR && hasSpaceKeypress) { + return null; + } + + return chars; + + default: + // For other native event types, do nothing. + return null; + } +} + +/** + * For browsers that do not provide the `textInput` event, extract the + * appropriate string to use for SyntheticInputEvent. + * + * @param {string} topLevelType Record from `EventConstants`. + * @param {object} nativeEvent Native browser event. + * @return {?string} The fallback string for this `beforeInput` event. + */ +function getFallbackBeforeInputChars(topLevelType, nativeEvent) { + // If we are currently composing (IME) and using a fallback to do so, + // try to extract the composed characters from the fallback object. + if (currentComposition) { + if ( + topLevelType === topLevelTypes.topCompositionEnd || + isFallbackCompositionEnd(topLevelType, nativeEvent) + ) { + var chars = currentComposition.getData(); + FallbackCompositionState.release(currentComposition); + currentComposition = null; + return chars; + } + return null; + } + + switch (topLevelType) { + case topLevelTypes.topPaste: + // If a paste event occurs after a keypress, throw out the input + // chars. Paste events should not lead to BeforeInput events. + return null; + case topLevelTypes.topKeyPress: + /** + * As of v27, Firefox may fire keypress events even when no character + * will be inserted. A few possibilities: + * + * - `which` is `0`. Arrow keys, Esc key, etc. + * + * - `which` is the pressed key code, but no char is available. + * Ex: 'AltGr + d` in Polish. There is no modified character for + * this key combination and no character is inserted into the + * document, but FF fires the keypress for char code `100` anyway. + * No `input` event will occur. + * + * - `which` is the pressed key code, but a command combination is + * being used. Ex: `Cmd+C`. No character is inserted, and no + * `input` event will occur. + */ + if (nativeEvent.which && !isKeypressCommand(nativeEvent)) { + return String.fromCharCode(nativeEvent.which); + } + return null; + case topLevelTypes.topCompositionEnd: + return useFallbackCompositionData ? null : nativeEvent.data; + default: + return null; + } +} + +/** + * Extract a SyntheticInputEvent for `beforeInput`, based on either native + * `textInput` or fallback behavior. + * + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {?object} A SyntheticInputEvent. + */ +function extractBeforeInputEvent( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent +) { + var chars; + + if (canUseTextInputEvent) { + chars = getNativeBeforeInputChars(topLevelType, nativeEvent); + } else { + chars = getFallbackBeforeInputChars(topLevelType, nativeEvent); + } + + // If no characters are being inserted, no BeforeInput event should + // be fired. + if (!chars) { + return null; + } + + var event = SyntheticInputEvent.getPooled( + eventTypes.beforeInput, + topLevelTargetID, + nativeEvent + ); + + event.data = chars; + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; +} + /** * Create an `onBeforeInput` event to match * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents. @@ -3260,6 +3823,10 @@ function isKeypressCommand(nativeEvent) { * actually been added, contrary to the spec. Thus, `textInput` is the best * available event to identify the characters that have actually been inserted * into the target node. + * + * This plugin is also responsible for emitting `composition` events, thus + * allowing us to share composition fallback code for both `beforeInput` and + * `composition` event types. */ var BeforeInputEventPlugin = { @@ -3274,124 +3841,34 @@ var BeforeInputEventPlugin = { * @see {EventPluginHub.extractEvents} */ extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent) { - - var chars; - - if (canUseTextInputEvent) { - switch (topLevelType) { - case topLevelTypes.topKeyPress: - /** - * If native `textInput` events are available, our goal is to make - * use of them. However, there is a special case: the spacebar key. - * In Webkit, preventing default on a spacebar `textInput` event - * cancels character insertion, but it *also* causes the browser - * to fall back to its default spacebar behavior of scrolling the - * page. - * - * Tracking at: - * https://code.google.com/p/chromium/issues/detail?id=355103 - * - * To avoid this issue, use the keypress event as if no `textInput` - * event is available. - */ - var which = nativeEvent.which; - if (which !== SPACEBAR_CODE) { - return; - } - - hasSpaceKeypress = true; - chars = SPACEBAR_CHAR; - break; - - case topLevelTypes.topTextInput: - // Record the characters to be added to the DOM. - chars = nativeEvent.data; - - // If it's a spacebar character, assume that we have already handled - // it at the keypress level and bail immediately. Android Chrome - // doesn't give us keycodes, so we need to blacklist it. - if (chars === SPACEBAR_CHAR && hasSpaceKeypress) { - return; - } - - // Otherwise, carry on. - break; - - default: - // For other native event types, do nothing. - return; - } - } else { - switch (topLevelType) { - case topLevelTypes.topPaste: - // If a paste event occurs after a keypress, throw out the input - // chars. Paste events should not lead to BeforeInput events. - fallbackChars = null; - break; - case topLevelTypes.topKeyPress: - /** - * As of v27, Firefox may fire keypress events even when no character - * will be inserted. A few possibilities: - * - * - `which` is `0`. Arrow keys, Esc key, etc. - * - * - `which` is the pressed key code, but no char is available. - * Ex: 'AltGr + d` in Polish. There is no modified character for - * this key combination and no character is inserted into the - * document, but FF fires the keypress for char code `100` anyway. - * No `input` event will occur. - * - * - `which` is the pressed key code, but a command combination is - * being used. Ex: `Cmd+C`. No character is inserted, and no - * `input` event will occur. - */ - if (nativeEvent.which && !isKeypressCommand(nativeEvent)) { - fallbackChars = String.fromCharCode(nativeEvent.which); - } - break; - case topLevelTypes.topCompositionEnd: - fallbackChars = nativeEvent.data; - break; - } - - // If no changes have occurred to the fallback string, no relevant - // event has fired and we're done. - if (fallbackChars === null) { - return; - } - - chars = fallbackChars; - } - - // If no characters are being inserted, no BeforeInput event should - // be fired. - if (!chars) { - return; - } - - var event = SyntheticInputEvent.getPooled( - eventTypes.beforeInput, - topLevelTargetID, - nativeEvent - ); - - event.data = chars; - fallbackChars = null; - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent + ) { + return [ + extractCompositionEvent( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent + ), + extractBeforeInputEvent( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent + ) + ]; } }; module.exports = BeforeInputEventPlugin; -},{"./EventConstants":58,"./EventPropagators":63,"./ExecutionEnvironment":64,"./SyntheticInputEvent":143,"./keyOf":189}],45:[function(require,module,exports){ +},{"./EventConstants":56,"./EventPropagators":61,"./ExecutionEnvironment":62,"./FallbackCompositionState":63,"./SyntheticCompositionEvent":147,"./SyntheticInputEvent":151,"./keyOf":198}],44:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -3500,9 +3977,9 @@ var CSSCore = { module.exports = CSSCore; }).call(this,require('_process')) -},{"./invariant":182,"_process":1}],46:[function(require,module,exports){ +},{"./invariant":191,"_process":1}],45:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -3512,14 +3989,15 @@ module.exports = CSSCore; * @providesModule CSSProperty */ -"use strict"; +'use strict'; /** * CSS properties which accept numbers but are not in units of "px". */ var isUnitlessNumber = { + boxFlex: true, + boxFlexGroup: true, columnCount: true, - fillOpacity: true, flex: true, flexGrow: true, flexShrink: true, @@ -3531,7 +4009,11 @@ var isUnitlessNumber = { orphans: true, widows: true, zIndex: true, - zoom: true + zoom: true, + + // SVG-related properties + fillOpacity: true, + strokeOpacity: true }; /** @@ -3616,10 +4098,10 @@ var CSSProperty = { module.exports = CSSProperty; -},{}],47:[function(require,module,exports){ +},{}],46:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -3630,7 +4112,7 @@ module.exports = CSSProperty; * @typechecks static-only */ -"use strict"; +'use strict'; var CSSProperty = require("./CSSProperty"); var ExecutionEnvironment = require("./ExecutionEnvironment"); @@ -3654,7 +4136,14 @@ if (ExecutionEnvironment.canUseDOM) { } if ("production" !== process.env.NODE_ENV) { + // 'msTransform' is correct, but the other prefixes should be capitalized + var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/; + + // style values shouldn't contain a semicolon + var badStyleValueWithSemicolonPattern = /;\s*$/; + var warnedStyleNames = {}; + var warnedStyleValues = {}; var warnHyphenatedStyleName = function(name) { if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { @@ -3664,10 +4153,54 @@ if ("production" !== process.env.NODE_ENV) { warnedStyleNames[name] = true; ("production" !== process.env.NODE_ENV ? warning( false, - 'Unsupported style property ' + name + '. Did you mean ' + - camelizeStyleName(name) + '?' + 'Unsupported style property %s. Did you mean %s?', + name, + camelizeStyleName(name) ) : null); }; + + var warnBadVendoredStyleName = function(name) { + if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { + return; + } + + warnedStyleNames[name] = true; + ("production" !== process.env.NODE_ENV ? warning( + false, + 'Unsupported vendor-prefixed style property %s. Did you mean %s?', + name, + name.charAt(0).toUpperCase() + name.slice(1) + ) : null); + }; + + var warnStyleValueWithSemicolon = function(name, value) { + if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) { + return; + } + + warnedStyleValues[value] = true; + ("production" !== process.env.NODE_ENV ? warning( + false, + 'Style property values shouldn\'t contain a semicolon. ' + + 'Try "%s: %s" instead.', + name, + value.replace(badStyleValueWithSemicolonPattern, '') + ) : null); + }; + + /** + * @param {string} name + * @param {*} value + */ + var warnValidStyle = function(name, value) { + if (name.indexOf('-') > -1) { + warnHyphenatedStyleName(name); + } else if (badVendoredStyleNamePattern.test(name)) { + warnBadVendoredStyleName(name); + } else if (badStyleValueWithSemicolonPattern.test(value)) { + warnStyleValueWithSemicolon(name, value); + } + }; } /** @@ -3693,12 +4226,10 @@ var CSSPropertyOperations = { if (!styles.hasOwnProperty(styleName)) { continue; } + var styleValue = styles[styleName]; if ("production" !== process.env.NODE_ENV) { - if (styleName.indexOf('-') > -1) { - warnHyphenatedStyleName(styleName); - } + warnValidStyle(styleName, styleValue); } - var styleValue = styles[styleName]; if (styleValue != null) { serialized += processStyleName(styleName) + ':'; serialized += dangerousStyleValue(styleName, styleValue) + ';'; @@ -3721,9 +4252,7 @@ var CSSPropertyOperations = { continue; } if ("production" !== process.env.NODE_ENV) { - if (styleName.indexOf('-') > -1) { - warnHyphenatedStyleName(styleName); - } + warnValidStyle(styleName, styles[styleName]); } var styleValue = dangerousStyleValue(styleName, styles[styleName]); if (styleName === 'float') { @@ -3751,10 +4280,10 @@ var CSSPropertyOperations = { module.exports = CSSPropertyOperations; }).call(this,require('_process')) -},{"./CSSProperty":46,"./ExecutionEnvironment":64,"./camelizeStyleName":154,"./dangerousStyleValue":161,"./hyphenateStyleName":180,"./memoizeStringOnly":191,"./warning":202,"_process":1}],48:[function(require,module,exports){ +},{"./CSSProperty":45,"./ExecutionEnvironment":62,"./camelizeStyleName":162,"./dangerousStyleValue":169,"./hyphenateStyleName":189,"./memoizeStringOnly":200,"./warning":212,"_process":1}],47:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -3764,7 +4293,7 @@ module.exports = CSSPropertyOperations; * @providesModule CallbackQueue */ -"use strict"; +'use strict'; var PooledClass = require("./PooledClass"); @@ -3815,7 +4344,7 @@ assign(CallbackQueue.prototype, { if (callbacks) { ("production" !== process.env.NODE_ENV ? invariant( callbacks.length === contexts.length, - "Mismatched list of contexts in callback queue" + 'Mismatched list of contexts in callback queue' ) : invariant(callbacks.length === contexts.length)); this._callbacks = null; this._contexts = null; @@ -3851,9 +4380,9 @@ PooledClass.addPoolingTo(CallbackQueue); module.exports = CallbackQueue; }).call(this,require('_process')) -},{"./Object.assign":70,"./PooledClass":71,"./invariant":182,"_process":1}],49:[function(require,module,exports){ +},{"./Object.assign":69,"./PooledClass":70,"./invariant":191,"_process":1}],48:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -3863,7 +4392,7 @@ module.exports = CallbackQueue; * @providesModule ChangeEventPlugin */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); var EventPluginHub = require("./EventPluginHub"); @@ -3919,7 +4448,7 @@ var doesChangeEventBubble = false; if (ExecutionEnvironment.canUseDOM) { // See `handleChange` comment below doesChangeEventBubble = isEventSupported('change') && ( - !('documentMode' in document) || document.documentMode > 8 + (!('documentMode' in document) || document.documentMode > 8) ); } @@ -3996,7 +4525,7 @@ if (ExecutionEnvironment.canUseDOM) { // IE9 claims to support the input event but fails to trigger it when // deleting text, so we ignore its input events isInputEventSupported = isEventSupported('input') && ( - !('documentMode' in document) || document.documentMode > 9 + (!('documentMode' in document) || document.documentMode > 9) ); } @@ -4233,9 +4762,9 @@ var ChangeEventPlugin = { module.exports = ChangeEventPlugin; -},{"./EventConstants":58,"./EventPluginHub":60,"./EventPropagators":63,"./ExecutionEnvironment":64,"./ReactUpdates":132,"./SyntheticEvent":141,"./isEventSupported":183,"./isTextInputElement":185,"./keyOf":189}],50:[function(require,module,exports){ +},{"./EventConstants":56,"./EventPluginHub":58,"./EventPropagators":61,"./ExecutionEnvironment":62,"./ReactUpdates":140,"./SyntheticEvent":149,"./isEventSupported":192,"./isTextInputElement":194,"./keyOf":198}],49:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -4246,7 +4775,7 @@ module.exports = ChangeEventPlugin; * @typechecks */ -"use strict"; +'use strict'; var nextReactRootIndex = 0; @@ -4258,269 +4787,10 @@ var ClientReactRootIndex = { module.exports = ClientReactRootIndex; -},{}],51:[function(require,module,exports){ -/** - * Copyright 2013-2014, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule CompositionEventPlugin - * @typechecks static-only - */ - -"use strict"; - -var EventConstants = require("./EventConstants"); -var EventPropagators = require("./EventPropagators"); -var ExecutionEnvironment = require("./ExecutionEnvironment"); -var ReactInputSelection = require("./ReactInputSelection"); -var SyntheticCompositionEvent = require("./SyntheticCompositionEvent"); - -var getTextContentAccessor = require("./getTextContentAccessor"); -var keyOf = require("./keyOf"); - -var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space -var START_KEYCODE = 229; - -var useCompositionEvent = ( - ExecutionEnvironment.canUseDOM && - 'CompositionEvent' in window -); - -// In IE9+, we have access to composition events, but the data supplied -// by the native compositionend event may be incorrect. In Korean, for example, -// the compositionend event contains only one character regardless of -// how many characters have been composed since compositionstart. -// We therefore use the fallback data while still using the native -// events as triggers. -var useFallbackData = ( - !useCompositionEvent || - ( - 'documentMode' in document && - document.documentMode > 8 && - document.documentMode <= 11 - ) -); - -var topLevelTypes = EventConstants.topLevelTypes; -var currentComposition = null; - -// Events and their corresponding property names. -var eventTypes = { - compositionEnd: { - phasedRegistrationNames: { - bubbled: keyOf({onCompositionEnd: null}), - captured: keyOf({onCompositionEndCapture: null}) - }, - dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topCompositionEnd, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyPress, - topLevelTypes.topKeyUp, - topLevelTypes.topMouseDown - ] - }, - compositionStart: { - phasedRegistrationNames: { - bubbled: keyOf({onCompositionStart: null}), - captured: keyOf({onCompositionStartCapture: null}) - }, - dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topCompositionStart, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyPress, - topLevelTypes.topKeyUp, - topLevelTypes.topMouseDown - ] - }, - compositionUpdate: { - phasedRegistrationNames: { - bubbled: keyOf({onCompositionUpdate: null}), - captured: keyOf({onCompositionUpdateCapture: null}) - }, - dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topCompositionUpdate, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyPress, - topLevelTypes.topKeyUp, - topLevelTypes.topMouseDown - ] - } -}; - -/** - * Translate native top level events into event types. - * - * @param {string} topLevelType - * @return {object} - */ -function getCompositionEventType(topLevelType) { - switch (topLevelType) { - case topLevelTypes.topCompositionStart: - return eventTypes.compositionStart; - case topLevelTypes.topCompositionEnd: - return eventTypes.compositionEnd; - case topLevelTypes.topCompositionUpdate: - return eventTypes.compositionUpdate; - } -} - -/** - * Does our fallback best-guess model think this event signifies that - * composition has begun? - * - * @param {string} topLevelType - * @param {object} nativeEvent - * @return {boolean} - */ -function isFallbackStart(topLevelType, nativeEvent) { - return ( - topLevelType === topLevelTypes.topKeyDown && - nativeEvent.keyCode === START_KEYCODE - ); -} - -/** - * Does our fallback mode think that this event is the end of composition? - * - * @param {string} topLevelType - * @param {object} nativeEvent - * @return {boolean} - */ -function isFallbackEnd(topLevelType, nativeEvent) { - switch (topLevelType) { - case topLevelTypes.topKeyUp: - // Command keys insert or clear IME input. - return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1); - case topLevelTypes.topKeyDown: - // Expect IME keyCode on each keydown. If we get any other - // code we must have exited earlier. - return (nativeEvent.keyCode !== START_KEYCODE); - case topLevelTypes.topKeyPress: - case topLevelTypes.topMouseDown: - case topLevelTypes.topBlur: - // Events are not possible without cancelling IME. - return true; - default: - return false; - } -} - -/** - * Helper class stores information about selection and document state - * so we can figure out what changed at a later date. - * - * @param {DOMEventTarget} root - */ -function FallbackCompositionState(root) { - this.root = root; - this.startSelection = ReactInputSelection.getSelection(root); - this.startValue = this.getText(); -} - -/** - * Get current text of input. - * - * @return {string} - */ -FallbackCompositionState.prototype.getText = function() { - return this.root.value || this.root[getTextContentAccessor()]; -}; - -/** - * Text that has changed since the start of composition. - * - * @return {string} - */ -FallbackCompositionState.prototype.getData = function() { - var endValue = this.getText(); - var prefixLength = this.startSelection.start; - var suffixLength = this.startValue.length - this.startSelection.end; - - return endValue.substr( - prefixLength, - endValue.length - suffixLength - prefixLength - ); -}; - -/** - * This plugin creates `onCompositionStart`, `onCompositionUpdate` and - * `onCompositionEnd` events on inputs, textareas and contentEditable - * nodes. - */ -var CompositionEventPlugin = { - - eventTypes: eventTypes, - - /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. - * @see {EventPluginHub.extractEvents} - */ - extractEvents: function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent) { - - var eventType; - var data; - - if (useCompositionEvent) { - eventType = getCompositionEventType(topLevelType); - } else if (!currentComposition) { - if (isFallbackStart(topLevelType, nativeEvent)) { - eventType = eventTypes.compositionStart; - } - } else if (isFallbackEnd(topLevelType, nativeEvent)) { - eventType = eventTypes.compositionEnd; - } - - if (useFallbackData) { - // The current composition is stored statically and must not be - // overwritten while composition continues. - if (!currentComposition && eventType === eventTypes.compositionStart) { - currentComposition = new FallbackCompositionState(topLevelTarget); - } else if (eventType === eventTypes.compositionEnd) { - if (currentComposition) { - data = currentComposition.getData(); - currentComposition = null; - } - } - } - - if (eventType) { - var event = SyntheticCompositionEvent.getPooled( - eventType, - topLevelTargetID, - nativeEvent - ); - if (data) { - // Inject data generated from fallback path into the synthetic event. - // This matches the property of native CompositionEventInterface. - event.data = data; - } - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; - } - } -}; - -module.exports = CompositionEventPlugin; - -},{"./EventConstants":58,"./EventPropagators":63,"./ExecutionEnvironment":64,"./ReactInputSelection":106,"./SyntheticCompositionEvent":139,"./getTextContentAccessor":177,"./keyOf":189}],52:[function(require,module,exports){ +},{}],50:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -4531,23 +4801,15 @@ module.exports = CompositionEventPlugin; * @typechecks static-only */ -"use strict"; +'use strict'; var Danger = require("./Danger"); var ReactMultiChildUpdateTypes = require("./ReactMultiChildUpdateTypes"); -var getTextContentAccessor = require("./getTextContentAccessor"); +var setTextContent = require("./setTextContent"); var invariant = require("./invariant"); /** - * The DOM property to use when setting text content. - * - * @type {string} - * @private - */ -var textContentAccessor = getTextContentAccessor(); - -/** * Inserts `childNode` as a child of `parentNode` at the `index`. * * @param {DOMElement} parentNode Parent node in which to insert. @@ -4566,37 +4828,6 @@ function insertChildAt(parentNode, childNode, index) { ); } -var updateTextContent; -if (textContentAccessor === 'textContent') { - /** - * Sets the text content of `node` to `text`. - * - * @param {DOMElement} node Node to change - * @param {string} text New text content - */ - updateTextContent = function(node, text) { - node.textContent = text; - }; -} else { - /** - * Sets the text content of `node` to `text`. - * - * @param {DOMElement} node Node to change - * @param {string} text New text content - */ - updateTextContent = function(node, text) { - // In order to preserve newlines correctly, we can't use .innerText to set - // the contents (see #1080), so we empty the element then append a text node - while (node.firstChild) { - node.removeChild(node.firstChild); - } - if (text) { - var doc = node.ownerDocument || document; - node.appendChild(doc.createTextNode(text)); - } - }; -} - /** * Operations for updating with DOM children. */ @@ -4604,7 +4835,7 @@ var DOMChildrenOperations = { dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup, - updateTextContent: updateTextContent, + updateTextContent: setTextContent, /** * Updates a component's children by processing a series of updates. The @@ -4621,7 +4852,8 @@ var DOMChildrenOperations = { // List of children that will be moved or removed. var updatedChildren = null; - for (var i = 0; update = updates[i]; i++) { + for (var i = 0; i < updates.length; i++) { + update = updates[i]; if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING || update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) { var updatedIndex = update.fromIndex; @@ -4633,7 +4865,7 @@ var DOMChildrenOperations = { 'processUpdates(): Unable to find child %s of element. This ' + 'probably means the DOM was unexpectedly mutated (e.g., by the ' + 'browser), usually due to forgetting a <tbody> when using tables, ' + - 'nesting tags like <form>, <p>, or <a>, or using non-SVG elements '+ + 'nesting tags like <form>, <p>, or <a>, or using non-SVG elements ' + 'in an <svg> parent. Try inspecting the child nodes of the element ' + 'with React ID `%s`.', updatedIndex, @@ -4658,7 +4890,8 @@ var DOMChildrenOperations = { } } - for (var k = 0; update = updates[k]; k++) { + for (var k = 0; k < updates.length; k++) { + update = updates[k]; switch (update.type) { case ReactMultiChildUpdateTypes.INSERT_MARKUP: insertChildAt( @@ -4675,7 +4908,7 @@ var DOMChildrenOperations = { ); break; case ReactMultiChildUpdateTypes.TEXT_CONTENT: - updateTextContent( + setTextContent( update.parentNode, update.textContent ); @@ -4692,10 +4925,10 @@ var DOMChildrenOperations = { module.exports = DOMChildrenOperations; }).call(this,require('_process')) -},{"./Danger":55,"./ReactMultiChildUpdateTypes":113,"./getTextContentAccessor":177,"./invariant":182,"_process":1}],53:[function(require,module,exports){ +},{"./Danger":53,"./ReactMultiChildUpdateTypes":119,"./invariant":191,"./setTextContent":206,"_process":1}],51:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -4708,7 +4941,7 @@ module.exports = DOMChildrenOperations; /*jslint bitwise: true */ -"use strict"; +'use strict'; var invariant = require("./invariant"); @@ -4991,10 +5224,10 @@ var DOMProperty = { module.exports = DOMProperty; }).call(this,require('_process')) -},{"./invariant":182,"_process":1}],54:[function(require,module,exports){ +},{"./invariant":191,"_process":1}],52:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -5005,12 +5238,11 @@ module.exports = DOMProperty; * @typechecks static-only */ -"use strict"; +'use strict'; var DOMProperty = require("./DOMProperty"); -var escapeTextForBrowser = require("./escapeTextForBrowser"); -var memoizeStringOnly = require("./memoizeStringOnly"); +var quoteAttributeValueForBrowser = require("./quoteAttributeValueForBrowser"); var warning = require("./warning"); function shouldIgnoreValue(name, value) { @@ -5021,10 +5253,6 @@ function shouldIgnoreValue(name, value) { (DOMProperty.hasOverloadedBooleanValue[name] && value === false); } -var processAttributeNameAndPrefix = memoizeStringOnly(function(name) { - return escapeTextForBrowser(name) + '="'; -}); - if ("production" !== process.env.NODE_ENV) { var reactProps = { children: true, @@ -5056,7 +5284,9 @@ if ("production" !== process.env.NODE_ENV) { // logging too much when using transferPropsTo. ("production" !== process.env.NODE_ENV ? warning( standardName == null, - 'Unknown DOM property ' + name + '. Did you mean ' + standardName + '?' + 'Unknown DOM property %s. Did you mean %s?', + name, + standardName ) : null); }; @@ -5074,8 +5304,8 @@ var DOMPropertyOperations = { * @return {string} Markup string. */ createMarkupForID: function(id) { - return processAttributeNameAndPrefix(DOMProperty.ID_ATTRIBUTE_NAME) + - escapeTextForBrowser(id) + '"'; + return DOMProperty.ID_ATTRIBUTE_NAME + '=' + + quoteAttributeValueForBrowser(id); }, /** @@ -5094,16 +5324,14 @@ var DOMPropertyOperations = { var attributeName = DOMProperty.getAttributeName[name]; if (DOMProperty.hasBooleanValue[name] || (DOMProperty.hasOverloadedBooleanValue[name] && value === true)) { - return escapeTextForBrowser(attributeName); + return attributeName; } - return processAttributeNameAndPrefix(attributeName) + - escapeTextForBrowser(value) + '"'; + return attributeName + '=' + quoteAttributeValueForBrowser(value); } else if (DOMProperty.isCustomAttribute(name)) { if (value == null) { return ''; } - return processAttributeNameAndPrefix(name) + - escapeTextForBrowser(value) + '"'; + return name + '=' + quoteAttributeValueForBrowser(value); } else if ("production" !== process.env.NODE_ENV) { warnUnknownProperty(name); } @@ -5188,10 +5416,10 @@ var DOMPropertyOperations = { module.exports = DOMPropertyOperations; }).call(this,require('_process')) -},{"./DOMProperty":53,"./escapeTextForBrowser":165,"./memoizeStringOnly":191,"./warning":202,"_process":1}],55:[function(require,module,exports){ +},{"./DOMProperty":51,"./quoteAttributeValueForBrowser":204,"./warning":212,"_process":1}],53:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -5204,7 +5432,7 @@ module.exports = DOMPropertyOperations; /*jslint evil: true, sub: true */ -"use strict"; +'use strict'; var ExecutionEnvironment = require("./ExecutionEnvironment"); @@ -5274,7 +5502,8 @@ var Danger = { // This for-in loop skips the holes of the sparse array. The order of // iteration should follow the order of assignment, which happens to match // numerical index order, but we don't rely on that. - for (var resultIndex in markupListByNodeName) { + var resultIndex; + for (resultIndex in markupListByNodeName) { if (markupListByNodeName.hasOwnProperty(resultIndex)) { var markup = markupListByNodeName[resultIndex]; @@ -5295,8 +5524,8 @@ var Danger = { emptyFunction // Do nothing special with <script> tags. ); - for (i = 0; i < renderNodes.length; ++i) { - var renderNode = renderNodes[i]; + for (var j = 0; j < renderNodes.length; ++j) { + var renderNode = renderNodes[j]; if (renderNode.hasAttribute && renderNode.hasAttribute(RESULT_INDEX_ATTR)) { @@ -5316,7 +5545,7 @@ var Danger = { } else if ("production" !== process.env.NODE_ENV) { console.error( - "Danger: Discarding unexpected node:", + 'Danger: Discarding unexpected node:', renderNode ); } @@ -5362,7 +5591,7 @@ var Danger = { 'dangerouslyReplaceNodeWithMarkup(...): Cannot replace markup of the ' + '<html> node. This is because browser quirks make this unreliable ' + 'and/or slow. If you want to render to the root you must use ' + - 'server rendering. See renderComponentToString().' + 'server rendering. See React.renderToString().' ) : invariant(oldChild.tagName.toLowerCase() !== 'html')); var newChild = createNodesFromMarkup(markup, emptyFunction)[0]; @@ -5374,9 +5603,9 @@ var Danger = { module.exports = Danger; }).call(this,require('_process')) -},{"./ExecutionEnvironment":64,"./createNodesFromMarkup":159,"./emptyFunction":163,"./getMarkupWrap":174,"./invariant":182,"_process":1}],56:[function(require,module,exports){ +},{"./ExecutionEnvironment":62,"./createNodesFromMarkup":167,"./emptyFunction":170,"./getMarkupWrap":183,"./invariant":191,"_process":1}],54:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -5386,9 +5615,9 @@ module.exports = Danger; * @providesModule DefaultEventPluginOrder */ -"use strict"; +'use strict'; - var keyOf = require("./keyOf"); +var keyOf = require("./keyOf"); /** * Module that is injectable into `EventPluginHub`, that specifies a @@ -5406,7 +5635,6 @@ var DefaultEventPluginOrder = [ keyOf({EnterLeaveEventPlugin: null}), keyOf({ChangeEventPlugin: null}), keyOf({SelectEventPlugin: null}), - keyOf({CompositionEventPlugin: null}), keyOf({BeforeInputEventPlugin: null}), keyOf({AnalyticsEventPlugin: null}), keyOf({MobileSafariClickEventPlugin: null}) @@ -5414,9 +5642,9 @@ var DefaultEventPluginOrder = [ module.exports = DefaultEventPluginOrder; -},{"./keyOf":189}],57:[function(require,module,exports){ +},{"./keyOf":198}],55:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -5427,7 +5655,7 @@ module.exports = DefaultEventPluginOrder; * @typechecks static-only */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); var EventPropagators = require("./EventPropagators"); @@ -5554,9 +5782,9 @@ var EnterLeaveEventPlugin = { module.exports = EnterLeaveEventPlugin; -},{"./EventConstants":58,"./EventPropagators":63,"./ReactMount":111,"./SyntheticMouseEvent":145,"./keyOf":189}],58:[function(require,module,exports){ +},{"./EventConstants":56,"./EventPropagators":61,"./ReactMount":117,"./SyntheticMouseEvent":153,"./keyOf":198}],56:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -5566,7 +5794,7 @@ module.exports = EnterLeaveEventPlugin; * @providesModule EventConstants */ -"use strict"; +'use strict'; var keyMirror = require("./keyMirror"); @@ -5626,10 +5854,10 @@ var EventConstants = { module.exports = EventConstants; -},{"./keyMirror":188}],59:[function(require,module,exports){ +},{"./keyMirror":197}],57:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014 Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -5716,10 +5944,10 @@ var EventListener = { module.exports = EventListener; }).call(this,require('_process')) -},{"./emptyFunction":163,"_process":1}],60:[function(require,module,exports){ +},{"./emptyFunction":170,"_process":1}],58:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -5729,7 +5957,7 @@ module.exports = EventListener; * @providesModule EventPluginHub */ -"use strict"; +'use strict'; var EventPluginRegistry = require("./EventPluginRegistry"); var EventPluginUtils = require("./EventPluginUtils"); @@ -5778,12 +6006,14 @@ var executeDispatchesAndRelease = function(event) { var InstanceHandle = null; function validateInstanceHandle() { - var invalid = !InstanceHandle|| - !InstanceHandle.traverseTwoPhase || - !InstanceHandle.traverseEnterLeave; - if (invalid) { - throw new Error('InstanceHandle not injected before use!'); - } + var valid = + InstanceHandle && + InstanceHandle.traverseTwoPhase && + InstanceHandle.traverseEnterLeave; + ("production" !== process.env.NODE_ENV ? invariant( + valid, + 'InstanceHandle not injected before use!' + ) : invariant(valid)); } /** @@ -5992,10 +6222,10 @@ var EventPluginHub = { module.exports = EventPluginHub; }).call(this,require('_process')) -},{"./EventPluginRegistry":61,"./EventPluginUtils":62,"./accumulateInto":151,"./forEachAccumulated":168,"./invariant":182,"_process":1}],61:[function(require,module,exports){ +},{"./EventPluginRegistry":59,"./EventPluginUtils":60,"./accumulateInto":159,"./forEachAccumulated":176,"./invariant":191,"_process":1}],59:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -6006,7 +6236,7 @@ module.exports = EventPluginHub; * @typechecks static-only */ -"use strict"; +'use strict'; var invariant = require("./invariant"); @@ -6272,10 +6502,10 @@ var EventPluginRegistry = { module.exports = EventPluginRegistry; }).call(this,require('_process')) -},{"./invariant":182,"_process":1}],62:[function(require,module,exports){ +},{"./invariant":191,"_process":1}],60:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -6285,7 +6515,7 @@ module.exports = EventPluginRegistry; * @providesModule EventPluginUtils */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); @@ -6391,8 +6621,8 @@ function executeDispatch(event, listener, domID) { /** * Standard/simple iteration through an event's collected dispatches. */ -function executeDispatchesInOrder(event, executeDispatch) { - forEachEventDispatch(event, executeDispatch); +function executeDispatchesInOrder(event, cb) { + forEachEventDispatch(event, cb); event._dispatchListeners = null; event._dispatchIDs = null; } @@ -6493,10 +6723,10 @@ var EventPluginUtils = { module.exports = EventPluginUtils; }).call(this,require('_process')) -},{"./EventConstants":58,"./invariant":182,"_process":1}],63:[function(require,module,exports){ +},{"./EventConstants":56,"./invariant":191,"_process":1}],61:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -6506,7 +6736,7 @@ module.exports = EventPluginUtils; * @providesModule EventPropagators */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); var EventPluginHub = require("./EventPluginHub"); @@ -6635,9 +6865,9 @@ var EventPropagators = { module.exports = EventPropagators; }).call(this,require('_process')) -},{"./EventConstants":58,"./EventPluginHub":60,"./accumulateInto":151,"./forEachAccumulated":168,"_process":1}],64:[function(require,module,exports){ +},{"./EventConstants":56,"./EventPluginHub":58,"./accumulateInto":159,"./forEachAccumulated":176,"_process":1}],62:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -6652,9 +6882,8 @@ module.exports = EventPropagators; "use strict"; var canUseDOM = !!( - typeof window !== 'undefined' && - window.document && - window.document.createElement + (typeof window !== 'undefined' && + window.document && window.document.createElement) ); /** @@ -6680,9 +6909,100 @@ var ExecutionEnvironment = { module.exports = ExecutionEnvironment; -},{}],65:[function(require,module,exports){ +},{}],63:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule FallbackCompositionState + * @typechecks static-only + */ + +'use strict'; + +var PooledClass = require("./PooledClass"); + +var assign = require("./Object.assign"); +var getTextContentAccessor = require("./getTextContentAccessor"); + /** - * Copyright 2013-2014, Facebook, Inc. + * This helper class stores information about text content of a target node, + * allowing comparison of content before and after a given event. + * + * Identify the node where selection currently begins, then observe + * both its text content and its current position in the DOM. Since the + * browser may natively replace the target node during composition, we can + * use its position to find its replacement. + * + * @param {DOMEventTarget} root + */ +function FallbackCompositionState(root) { + this._root = root; + this._startText = this.getText(); + this._fallbackText = null; +} + +assign(FallbackCompositionState.prototype, { + /** + * Get current text of input. + * + * @return {string} + */ + getText: function() { + if ('value' in this._root) { + return this._root.value; + } + return this._root[getTextContentAccessor()]; + }, + + /** + * Determine the differing substring between the initially stored + * text content and the current content. + * + * @return {string} + */ + getData: function() { + if (this._fallbackText) { + return this._fallbackText; + } + + var start; + var startValue = this._startText; + var startLength = startValue.length; + var end; + var endValue = this.getText(); + var endLength = endValue.length; + + for (start = 0; start < startLength; start++) { + if (startValue[start] !== endValue[start]) { + break; + } + } + + var minEnd = startLength - start; + for (end = 1; end <= minEnd; end++) { + if (startValue[startLength - end] !== endValue[endLength - end]) { + break; + } + } + + var sliceTail = end > 1 ? 1 - end : undefined; + this._fallbackText = endValue.slice(start, sliceTail); + return this._fallbackText; + } +}); + +PooledClass.addPoolingTo(FallbackCompositionState); + +module.exports = FallbackCompositionState; + +},{"./Object.assign":69,"./PooledClass":70,"./getTextContentAccessor":186}],64:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -6694,7 +7014,7 @@ module.exports = ExecutionEnvironment; /*jslint bitwise: true*/ -"use strict"; +'use strict'; var DOMProperty = require("./DOMProperty"); var ExecutionEnvironment = require("./ExecutionEnvironment"); @@ -6771,8 +7091,13 @@ var HTMLDOMPropertyConfig = { draggable: null, encType: null, form: MUST_USE_ATTRIBUTE, + formAction: MUST_USE_ATTRIBUTE, + formEncType: MUST_USE_ATTRIBUTE, + formMethod: MUST_USE_ATTRIBUTE, formNoValidate: HAS_BOOLEAN_VALUE, + formTarget: MUST_USE_ATTRIBUTE, frameBorder: MUST_USE_ATTRIBUTE, + headers: null, height: MUST_USE_ATTRIBUTE, hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, href: null, @@ -6786,6 +7111,8 @@ var HTMLDOMPropertyConfig = { list: MUST_USE_ATTRIBUTE, loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, manifest: MUST_USE_ATTRIBUTE, + marginHeight: null, + marginWidth: null, max: null, maxLength: MUST_USE_ATTRIBUTE, media: MUST_USE_ATTRIBUTE, @@ -6796,7 +7123,7 @@ var HTMLDOMPropertyConfig = { muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, name: null, noValidate: HAS_BOOLEAN_VALUE, - open: null, + open: HAS_BOOLEAN_VALUE, pattern: null, placeholder: null, poster: null, @@ -6836,12 +7163,22 @@ var HTMLDOMPropertyConfig = { /** * Non-standard Properties */ - autoCapitalize: null, // Supported in Mobile Safari for keyboard hints - autoCorrect: null, // Supported in Mobile Safari for keyboard hints - itemProp: MUST_USE_ATTRIBUTE, // Microdata: http://schema.org/docs/gs.html - itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, // Microdata: http://schema.org/docs/gs.html - itemType: MUST_USE_ATTRIBUTE, // Microdata: http://schema.org/docs/gs.html - property: null // Supports OG in meta tags + // autoCapitalize and autoCorrect are supported in Mobile Safari for + // keyboard hints. + autoCapitalize: null, + autoCorrect: null, + // itemProp, itemScope, itemType are for + // Microdata support. See http://schema.org/docs/gs.html + itemProp: MUST_USE_ATTRIBUTE, + itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, + itemType: MUST_USE_ATTRIBUTE, + // itemID and itemRef are for Microdata support as well but + // only specified in the the WHATWG spec document. See + // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api + itemID: MUST_USE_ATTRIBUTE, + itemRef: MUST_USE_ATTRIBUTE, + // property is supported for OpenGraph in meta tags. + property: null }, DOMAttributeNames: { acceptCharset: 'accept-charset', @@ -6855,7 +7192,9 @@ var HTMLDOMPropertyConfig = { autoCorrect: 'autocorrect', autoFocus: 'autofocus', autoPlay: 'autoplay', - encType: 'enctype', + // `encoding` is equivalent to `enctype`, IE8 lacks an `enctype` setter. + // http://www.w3.org/TR/html5/forms.html#dom-fs-encoding + encType: 'encoding', hrefLang: 'hreflang', radioGroup: 'radiogroup', spellCheck: 'spellcheck', @@ -6866,9 +7205,9 @@ var HTMLDOMPropertyConfig = { module.exports = HTMLDOMPropertyConfig; -},{"./DOMProperty":53,"./ExecutionEnvironment":64}],66:[function(require,module,exports){ +},{"./DOMProperty":51,"./ExecutionEnvironment":62}],65:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -6879,7 +7218,7 @@ module.exports = HTMLDOMPropertyConfig; * @typechecks static-only */ -"use strict"; +'use strict'; var ReactLink = require("./ReactLink"); var ReactStateSetters = require("./ReactStateSetters"); @@ -6907,10 +7246,10 @@ var LinkedStateMixin = { module.exports = LinkedStateMixin; -},{"./ReactLink":109,"./ReactStateSetters":126}],67:[function(require,module,exports){ +},{"./ReactLink":115,"./ReactStateSetters":134}],66:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -6921,7 +7260,7 @@ module.exports = LinkedStateMixin; * @typechecks static-only */ -"use strict"; +'use strict'; var ReactPropTypes = require("./ReactPropTypes"); @@ -6992,7 +7331,7 @@ var LinkedValueUtils = { props.onChange || props.readOnly || props.disabled) { - return; + return null; } return new Error( 'You provided a `value` prop to a form field without an ' + @@ -7006,7 +7345,7 @@ var LinkedValueUtils = { props.onChange || props.readOnly || props.disabled) { - return; + return null; } return new Error( 'You provided a `checked` prop to a form field without an ' + @@ -7063,10 +7402,10 @@ var LinkedValueUtils = { module.exports = LinkedValueUtils; }).call(this,require('_process')) -},{"./ReactPropTypes":120,"./invariant":182,"_process":1}],68:[function(require,module,exports){ +},{"./ReactPropTypes":126,"./invariant":191,"_process":1}],67:[function(require,module,exports){ (function (process){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -7076,7 +7415,7 @@ module.exports = LinkedValueUtils; * @providesModule LocalEventTrapMixin */ -"use strict"; +'use strict'; var ReactBrowserEventEmitter = require("./ReactBrowserEventEmitter"); @@ -7091,10 +7430,17 @@ function remove(event) { var LocalEventTrapMixin = { trapBubbledEvent:function(topLevelType, handlerBaseName) { ("production" !== process.env.NODE_ENV ? invariant(this.isMounted(), 'Must be mounted to trap events') : invariant(this.isMounted())); + // If a component renders to null or if another component fatals and causes + // the state of the tree to be corrupted, `node` here can be null. + var node = this.getDOMNode(); + ("production" !== process.env.NODE_ENV ? invariant( + node, + 'LocalEventTrapMixin.trapBubbledEvent(...): Requires node to be rendered.' + ) : invariant(node)); var listener = ReactBrowserEventEmitter.trapBubbledEvent( topLevelType, handlerBaseName, - this.getDOMNode() + node ); this._localEventListeners = accumulateInto(this._localEventListeners, listener); @@ -7113,9 +7459,9 @@ var LocalEventTrapMixin = { module.exports = LocalEventTrapMixin; }).call(this,require('_process')) -},{"./ReactBrowserEventEmitter":74,"./accumulateInto":151,"./forEachAccumulated":168,"./invariant":182,"_process":1}],69:[function(require,module,exports){ +},{"./ReactBrowserEventEmitter":73,"./accumulateInto":159,"./forEachAccumulated":176,"./invariant":191,"_process":1}],68:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -7126,7 +7472,7 @@ module.exports = LocalEventTrapMixin; * @typechecks static-only */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); @@ -7171,9 +7517,9 @@ var MobileSafariClickEventPlugin = { module.exports = MobileSafariClickEventPlugin; -},{"./EventConstants":58,"./emptyFunction":163}],70:[function(require,module,exports){ +},{"./EventConstants":56,"./emptyFunction":170}],69:[function(require,module,exports){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -7185,6 +7531,8 @@ module.exports = MobileSafariClickEventPlugin; // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.assign +'use strict'; + function assign(target, sources) { if (target == null) { throw new TypeError('Object.assign target cannot be null or undefined'); @@ -7214,14 +7562,14 @@ function assign(target, sources) { } return to; -}; +} module.exports = assign; -},{}],71:[function(require,module,exports){ +},{}],70:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -7231,7 +7579,7 @@ module.exports = assign; * @providesModule PooledClass */ -"use strict"; +'use strict'; var invariant = require("./invariant"); @@ -7334,10 +7682,10 @@ var PooledClass = { module.exports = PooledClass; }).call(this,require('_process')) -},{"./invariant":182,"_process":1}],72:[function(require,module,exports){ +},{"./invariant":191,"_process":1}],71:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -7347,51 +7695,44 @@ module.exports = PooledClass; * @providesModule React */ -"use strict"; +/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__*/ + +'use strict'; -var DOMPropertyOperations = require("./DOMPropertyOperations"); var EventPluginUtils = require("./EventPluginUtils"); var ReactChildren = require("./ReactChildren"); var ReactComponent = require("./ReactComponent"); -var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactClass = require("./ReactClass"); var ReactContext = require("./ReactContext"); var ReactCurrentOwner = require("./ReactCurrentOwner"); var ReactElement = require("./ReactElement"); var ReactElementValidator = require("./ReactElementValidator"); var ReactDOM = require("./ReactDOM"); -var ReactDOMComponent = require("./ReactDOMComponent"); +var ReactDOMTextComponent = require("./ReactDOMTextComponent"); var ReactDefaultInjection = require("./ReactDefaultInjection"); var ReactInstanceHandles = require("./ReactInstanceHandles"); -var ReactLegacyElement = require("./ReactLegacyElement"); var ReactMount = require("./ReactMount"); -var ReactMultiChild = require("./ReactMultiChild"); var ReactPerf = require("./ReactPerf"); var ReactPropTypes = require("./ReactPropTypes"); +var ReactReconciler = require("./ReactReconciler"); var ReactServerRendering = require("./ReactServerRendering"); -var ReactTextComponent = require("./ReactTextComponent"); var assign = require("./Object.assign"); -var deprecated = require("./deprecated"); +var findDOMNode = require("./findDOMNode"); var onlyChild = require("./onlyChild"); ReactDefaultInjection.inject(); var createElement = ReactElement.createElement; var createFactory = ReactElement.createFactory; +var cloneElement = ReactElement.cloneElement; if ("production" !== process.env.NODE_ENV) { createElement = ReactElementValidator.createElement; createFactory = ReactElementValidator.createFactory; + cloneElement = ReactElementValidator.cloneElement; } -// TODO: Drop legacy elements once classes no longer export these factories -createElement = ReactLegacyElement.wrapCreateElement( - createElement -); -createFactory = ReactLegacyElement.wrapCreateFactory( - createFactory -); - var render = ReactPerf.measure('React', 'render', ReactMount.render); var React = { @@ -7401,56 +7742,32 @@ var React = { count: ReactChildren.count, only: onlyChild }, + Component: ReactComponent, DOM: ReactDOM, PropTypes: ReactPropTypes, initializeTouchEvents: function(shouldUseTouch) { EventPluginUtils.useTouchEvents = shouldUseTouch; }, - createClass: ReactCompositeComponent.createClass, + createClass: ReactClass.createClass, createElement: createElement, + cloneElement: cloneElement, createFactory: createFactory, + createMixin: function(mixin) { + // Currently a noop. Will be used to validate and trace mixins. + return mixin; + }, constructAndRenderComponent: ReactMount.constructAndRenderComponent, constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID, + findDOMNode: findDOMNode, render: render, renderToString: ReactServerRendering.renderToString, renderToStaticMarkup: ReactServerRendering.renderToStaticMarkup, unmountComponentAtNode: ReactMount.unmountComponentAtNode, - isValidClass: ReactLegacyElement.isValidClass, isValidElement: ReactElement.isValidElement, withContext: ReactContext.withContext, // Hook for JSX spread, don't use this for anything else. - __spread: assign, - - // Deprecations (remove for 0.13) - renderComponent: deprecated( - 'React', - 'renderComponent', - 'render', - this, - render - ), - renderComponentToString: deprecated( - 'React', - 'renderComponentToString', - 'renderToString', - this, - ReactServerRendering.renderToString - ), - renderComponentToStaticMarkup: deprecated( - 'React', - 'renderComponentToStaticMarkup', - 'renderToStaticMarkup', - this, - ReactServerRendering.renderToStaticMarkup - ), - isValidComponent: deprecated( - 'React', - 'isValidComponent', - 'isValidElement', - this, - ReactElement.isValidElement - ) + __spread: assign }; // Inject the runtime into a devtools global hook regardless of browser. @@ -7459,14 +7776,11 @@ if ( typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') { __REACT_DEVTOOLS_GLOBAL_HOOK__.inject({ - Component: ReactComponent, CurrentOwner: ReactCurrentOwner, - DOMComponent: ReactDOMComponent, - DOMPropertyOperations: DOMPropertyOperations, InstanceHandles: ReactInstanceHandles, Mount: ReactMount, - MultiChild: ReactMultiChild, - TextComponent: ReactTextComponent + Reconciler: ReactReconciler, + TextComponent: ReactDOMTextComponent }); } @@ -7515,17 +7829,14 @@ if ("production" !== process.env.NODE_ENV) { } } -// Version exists only in the open-source version of React, not in Facebook's -// internal version. -React.version = '0.12.0'; +React.version = '0.13.1'; module.exports = React; }).call(this,require('_process')) -},{"./DOMPropertyOperations":54,"./EventPluginUtils":62,"./ExecutionEnvironment":64,"./Object.assign":70,"./ReactChildren":77,"./ReactComponent":78,"./ReactCompositeComponent":81,"./ReactContext":82,"./ReactCurrentOwner":83,"./ReactDOM":84,"./ReactDOMComponent":86,"./ReactDefaultInjection":96,"./ReactElement":99,"./ReactElementValidator":100,"./ReactInstanceHandles":107,"./ReactLegacyElement":108,"./ReactMount":111,"./ReactMultiChild":112,"./ReactPerf":116,"./ReactPropTypes":120,"./ReactServerRendering":124,"./ReactTextComponent":128,"./deprecated":162,"./onlyChild":193,"_process":1}],73:[function(require,module,exports){ -(function (process){ +},{"./EventPluginUtils":60,"./ExecutionEnvironment":62,"./Object.assign":69,"./ReactChildren":77,"./ReactClass":78,"./ReactComponent":79,"./ReactContext":84,"./ReactCurrentOwner":85,"./ReactDOM":86,"./ReactDOMTextComponent":97,"./ReactDefaultInjection":100,"./ReactElement":103,"./ReactElementValidator":104,"./ReactInstanceHandles":112,"./ReactMount":117,"./ReactPerf":122,"./ReactPropTypes":126,"./ReactReconciler":129,"./ReactServerRendering":132,"./findDOMNode":173,"./onlyChild":201,"_process":1}],72:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -7535,12 +7846,9 @@ module.exports = React; * @providesModule ReactBrowserComponentMixin */ -"use strict"; - -var ReactEmptyComponent = require("./ReactEmptyComponent"); -var ReactMount = require("./ReactMount"); +'use strict'; -var invariant = require("./invariant"); +var findDOMNode = require("./findDOMNode"); var ReactBrowserComponentMixin = { /** @@ -7551,23 +7859,15 @@ var ReactBrowserComponentMixin = { * @protected */ getDOMNode: function() { - ("production" !== process.env.NODE_ENV ? invariant( - this.isMounted(), - 'getDOMNode(): A component must be mounted to have a DOM node.' - ) : invariant(this.isMounted())); - if (ReactEmptyComponent.isNullComponentID(this._rootNodeID)) { - return null; - } - return ReactMount.getNode(this._rootNodeID); + return findDOMNode(this); } }; module.exports = ReactBrowserComponentMixin; -}).call(this,require('_process')) -},{"./ReactEmptyComponent":101,"./ReactMount":111,"./invariant":182,"_process":1}],74:[function(require,module,exports){ +},{"./findDOMNode":173}],73:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -7578,7 +7878,7 @@ module.exports = ReactBrowserComponentMixin; * @typechecks static-only */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); var EventPluginHub = require("./EventPluginHub"); @@ -7694,7 +7994,7 @@ var topEventMapping = { /** * To ensure no conflicts with other potential React instances on the page */ -var topListenersIDKey = "_reactListenersID" + String(Math.random()).slice(2); +var topListenersIDKey = '_reactListenersID' + String(Math.random()).slice(2); function getListeningForDocument(mountAt) { // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty` @@ -7751,8 +8051,7 @@ var ReactBrowserEventEmitter = assign({}, ReactEventEmitterMixin, { */ isEnabled: function() { return !!( - ReactBrowserEventEmitter.ReactEventListener && - ReactBrowserEventEmitter.ReactEventListener.isEnabled() + (ReactBrowserEventEmitter.ReactEventListener && ReactBrowserEventEmitter.ReactEventListener.isEnabled()) ); }, @@ -7787,8 +8086,7 @@ var ReactBrowserEventEmitter = assign({}, ReactEventEmitterMixin, { for (var i = 0, l = dependencies.length; i < l; i++) { var dependency = dependencies[i]; if (!( - isListening.hasOwnProperty(dependency) && - isListening[dependency] + (isListening.hasOwnProperty(dependency) && isListening[dependency]) )) { if (dependency === topLevelTypes.topWheel) { if (isEventSupported('wheel')) { @@ -7896,7 +8194,7 @@ var ReactBrowserEventEmitter = assign({}, ReactEventEmitterMixin, { * * @see http://www.quirksmode.org/dom/events/scroll.html */ - ensureScrollValueMonitoring: function(){ + ensureScrollValueMonitoring: function() { if (!isMonitoringScrollValue) { var refresh = ViewportMetrics.refreshScrollValues; ReactBrowserEventEmitter.ReactEventListener.monitorScrollValue(refresh); @@ -7920,9 +8218,9 @@ var ReactBrowserEventEmitter = assign({}, ReactEventEmitterMixin, { module.exports = ReactBrowserEventEmitter; -},{"./EventConstants":58,"./EventPluginHub":60,"./EventPluginRegistry":61,"./Object.assign":70,"./ReactEventEmitterMixin":103,"./ViewportMetrics":150,"./isEventSupported":183}],75:[function(require,module,exports){ +},{"./EventConstants":56,"./EventPluginHub":58,"./EventPluginRegistry":59,"./Object.assign":69,"./ReactEventEmitterMixin":107,"./ViewportMetrics":158,"./isEventSupported":192}],74:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -7933,7 +8231,7 @@ module.exports = ReactBrowserEventEmitter; * @providesModule ReactCSSTransitionGroup */ -"use strict"; +'use strict'; var React = require("./React"); @@ -7951,12 +8249,14 @@ var ReactCSSTransitionGroup = React.createClass({ propTypes: { transitionName: React.PropTypes.string.isRequired, + transitionAppear: React.PropTypes.bool, transitionEnter: React.PropTypes.bool, transitionLeave: React.PropTypes.bool }, getDefaultProps: function() { return { + transitionAppear: false, transitionEnter: true, transitionLeave: true }; @@ -7969,6 +8269,7 @@ var ReactCSSTransitionGroup = React.createClass({ return ReactCSSTransitionGroupChild( { name: this.props.transitionName, + appear: this.props.transitionAppear, enter: this.props.transitionEnter, leave: this.props.transitionLeave }, @@ -7987,10 +8288,10 @@ var ReactCSSTransitionGroup = React.createClass({ module.exports = ReactCSSTransitionGroup; -},{"./Object.assign":70,"./React":72,"./ReactCSSTransitionGroupChild":76,"./ReactTransitionGroup":131}],76:[function(require,module,exports){ +},{"./Object.assign":69,"./React":71,"./ReactCSSTransitionGroupChild":75,"./ReactTransitionGroup":138}],75:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -8001,7 +8302,7 @@ module.exports = ReactCSSTransitionGroup; * @providesModule ReactCSSTransitionGroupChild */ -"use strict"; +'use strict'; var React = require("./React"); @@ -8009,6 +8310,7 @@ var CSSCore = require("./CSSCore"); var ReactTransitionEvents = require("./ReactTransitionEvents"); var onlyChild = require("./onlyChild"); +var warning = require("./warning"); // We don't remove the element from the DOM until we receive an animationend or // transitionend event. If the user screws up and forgets to add an animation @@ -8022,12 +8324,14 @@ var noEventListener = null; if ("production" !== process.env.NODE_ENV) { noEventListener = function() { - console.warn( + ("production" !== process.env.NODE_ENV ? warning( + false, 'transition(): tried to perform an animation without ' + 'an animationend or transitionend event after timeout (' + - NO_EVENT_TIMEOUT + 'ms). You should either disable this ' + - 'transition in JS or add a CSS animation/transition.' - ); + '%sms). You should either disable this ' + + 'transition in JS or add a CSS animation/transition.', + NO_EVENT_TIMEOUT + ) : null); }; } @@ -8055,7 +8359,9 @@ var ReactCSSTransitionGroupChild = React.createClass({ // Usually this optional callback is used for informing an owner of // a leave animation and telling it to remove the child. - finishCallback && finishCallback(); + if (finishCallback) { + finishCallback(); + } }; ReactTransitionEvents.addEndEventListener(node, endListener); @@ -8098,6 +8404,14 @@ var ReactCSSTransitionGroupChild = React.createClass({ } }, + componentWillAppear: function(done) { + if (this.props.appear) { + this.transition('appear', done); + } else { + done(); + } + }, + componentWillEnter: function(done) { if (this.props.enter) { this.transition('enter', done); @@ -8122,10 +8436,137 @@ var ReactCSSTransitionGroupChild = React.createClass({ module.exports = ReactCSSTransitionGroupChild; }).call(this,require('_process')) -},{"./CSSCore":45,"./React":72,"./ReactTransitionEvents":130,"./onlyChild":193,"_process":1}],77:[function(require,module,exports){ +},{"./CSSCore":44,"./React":71,"./ReactTransitionEvents":137,"./onlyChild":201,"./warning":212,"_process":1}],76:[function(require,module,exports){ +/** + * Copyright 2014-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactChildReconciler + * @typechecks static-only + */ + +'use strict'; + +var ReactReconciler = require("./ReactReconciler"); + +var flattenChildren = require("./flattenChildren"); +var instantiateReactComponent = require("./instantiateReactComponent"); +var shouldUpdateReactComponent = require("./shouldUpdateReactComponent"); + +/** + * ReactChildReconciler provides helpers for initializing or updating a set of + * children. Its output is suitable for passing it onto ReactMultiChild which + * does diffed reordering and insertion. + */ +var ReactChildReconciler = { + + /** + * Generates a "mount image" for each of the supplied children. In the case + * of `ReactDOMComponent`, a mount image is a string of markup. + * + * @param {?object} nestedChildNodes Nested child maps. + * @return {?object} A set of child instances. + * @internal + */ + instantiateChildren: function(nestedChildNodes, transaction, context) { + var children = flattenChildren(nestedChildNodes); + for (var name in children) { + if (children.hasOwnProperty(name)) { + var child = children[name]; + // The rendered children must be turned into instances as they're + // mounted. + var childInstance = instantiateReactComponent(child, null); + children[name] = childInstance; + } + } + return children; + }, + + /** + * Updates the rendered children and returns a new set of children. + * + * @param {?object} prevChildren Previously initialized set of children. + * @param {?object} nextNestedChildNodes Nested child maps. + * @param {ReactReconcileTransaction} transaction + * @param {object} context + * @return {?object} A new set of child instances. + * @internal + */ + updateChildren: function( + prevChildren, + nextNestedChildNodes, + transaction, + context) { + // We currently don't have a way to track moves here but if we use iterators + // instead of for..in we can zip the iterators and check if an item has + // moved. + // TODO: If nothing has changed, return the prevChildren object so that we + // can quickly bailout if nothing has changed. + var nextChildren = flattenChildren(nextNestedChildNodes); + if (!nextChildren && !prevChildren) { + return null; + } + var name; + for (name in nextChildren) { + if (!nextChildren.hasOwnProperty(name)) { + continue; + } + var prevChild = prevChildren && prevChildren[name]; + var prevElement = prevChild && prevChild._currentElement; + var nextElement = nextChildren[name]; + if (shouldUpdateReactComponent(prevElement, nextElement)) { + ReactReconciler.receiveComponent( + prevChild, nextElement, transaction, context + ); + nextChildren[name] = prevChild; + } else { + if (prevChild) { + ReactReconciler.unmountComponent(prevChild, name); + } + // The child must be instantiated before it's mounted. + var nextChildInstance = instantiateReactComponent( + nextElement, + null + ); + nextChildren[name] = nextChildInstance; + } + } + // Unmount children that are no longer present. + for (name in prevChildren) { + if (prevChildren.hasOwnProperty(name) && + !(nextChildren && nextChildren.hasOwnProperty(name))) { + ReactReconciler.unmountComponent(prevChildren[name]); + } + } + return nextChildren; + }, + + /** + * Unmounts all rendered children. This should be used to clean up children + * when this component is unmounted. + * + * @param {?object} renderedChildren Previously initialized set of children. + * @internal + */ + unmountChildren: function(renderedChildren) { + for (var name in renderedChildren) { + var renderedChild = renderedChildren[name]; + ReactReconciler.unmountComponent(renderedChild); + } + } + +}; + +module.exports = ReactChildReconciler; + +},{"./ReactReconciler":129,"./flattenChildren":174,"./instantiateReactComponent":190,"./shouldUpdateReactComponent":208}],77:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -8135,9 +8576,10 @@ module.exports = ReactCSSTransitionGroupChild; * @providesModule ReactChildren */ -"use strict"; +'use strict'; var PooledClass = require("./PooledClass"); +var ReactFragment = require("./ReactFragment"); var traverseAllChildren = require("./traverseAllChildren"); var warning = require("./warning"); @@ -8207,13 +8649,15 @@ function mapSingleChildIntoContext(traverseContext, child, name, i) { var mapResult = mapBookKeeping.mapResult; var keyUnique = !mapResult.hasOwnProperty(name); - ("production" !== process.env.NODE_ENV ? warning( - keyUnique, - 'ReactChildren.map(...): Encountered two children with the same key, ' + - '`%s`. Child keys must be unique; when two children share a key, only ' + - 'the first child will be used.', - name - ) : null); + if ("production" !== process.env.NODE_ENV) { + ("production" !== process.env.NODE_ENV ? warning( + keyUnique, + 'ReactChildren.map(...): Encountered two children with the same key, ' + + '`%s`. Child keys must be unique; when two children share a key, only ' + + 'the first child will be used.', + name + ) : null); + } if (keyUnique) { var mappedChild = @@ -8245,7 +8689,7 @@ function mapChildren(children, func, context) { var traverseContext = MapBookKeeping.getPooled(mapResult, func, context); traverseAllChildren(children, mapSingleChildIntoContext, traverseContext); MapBookKeeping.release(traverseContext); - return mapResult; + return ReactFragment.create(mapResult); } function forEachSingleChildDummy(traverseContext, child, name, i) { @@ -8272,664 +8716,41 @@ var ReactChildren = { module.exports = ReactChildren; }).call(this,require('_process')) -},{"./PooledClass":71,"./traverseAllChildren":200,"./warning":202,"_process":1}],78:[function(require,module,exports){ +},{"./PooledClass":70,"./ReactFragment":109,"./traverseAllChildren":210,"./warning":212,"_process":1}],78:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @providesModule ReactComponent - */ - -"use strict"; - -var ReactElement = require("./ReactElement"); -var ReactOwner = require("./ReactOwner"); -var ReactUpdates = require("./ReactUpdates"); - -var assign = require("./Object.assign"); -var invariant = require("./invariant"); -var keyMirror = require("./keyMirror"); - -/** - * Every React component is in one of these life cycles. - */ -var ComponentLifeCycle = keyMirror({ - /** - * Mounted components have a DOM node representation and are capable of - * receiving new props. - */ - MOUNTED: null, - /** - * Unmounted components are inactive and cannot receive new props. - */ - UNMOUNTED: null -}); - -var injected = false; - -/** - * Optionally injectable environment dependent cleanup hook. (server vs. - * browser etc). Example: A browser system caches DOM nodes based on component - * ID and must remove that cache entry when this instance is unmounted. - * - * @private + * @providesModule ReactClass */ -var unmountIDFromEnvironment = null; - -/** - * The "image" of a component tree, is the platform specific (typically - * serialized) data that represents a tree of lower level UI building blocks. - * On the web, this "image" is HTML markup which describes a construction of - * low level `div` and `span` nodes. Other platforms may have different - * encoding of this "image". This must be injected. - * - * @private - */ -var mountImageIntoNode = null; - -/** - * Components are the basic units of composition in React. - * - * Every component accepts a set of keyed input parameters known as "props" that - * are initialized by the constructor. Once a component is mounted, the props - * can be mutated using `setProps` or `replaceProps`. - * - * Every component is capable of the following operations: - * - * `mountComponent` - * Initializes the component, renders markup, and registers event listeners. - * - * `receiveComponent` - * Updates the rendered DOM nodes to match the given component. - * - * `unmountComponent` - * Releases any resources allocated by this component. - * - * Components can also be "owned" by other components. Being owned by another - * component means being constructed by that component. This is different from - * being the child of a component, which means having a DOM representation that - * is a child of the DOM representation of that component. - * - * @class ReactComponent - */ -var ReactComponent = { - - injection: { - injectEnvironment: function(ReactComponentEnvironment) { - ("production" !== process.env.NODE_ENV ? invariant( - !injected, - 'ReactComponent: injectEnvironment() can only be called once.' - ) : invariant(!injected)); - mountImageIntoNode = ReactComponentEnvironment.mountImageIntoNode; - unmountIDFromEnvironment = - ReactComponentEnvironment.unmountIDFromEnvironment; - ReactComponent.BackendIDOperations = - ReactComponentEnvironment.BackendIDOperations; - injected = true; - } - }, - - /** - * @internal - */ - LifeCycle: ComponentLifeCycle, - - /** - * Injected module that provides ability to mutate individual properties. - * Injected into the base class because many different subclasses need access - * to this. - * - * @internal - */ - BackendIDOperations: null, - - /** - * Base functionality for every ReactComponent constructor. Mixed into the - * `ReactComponent` prototype, but exposed statically for easy access. - * - * @lends {ReactComponent.prototype} - */ - Mixin: { - - /** - * Checks whether or not this component is mounted. - * - * @return {boolean} True if mounted, false otherwise. - * @final - * @protected - */ - isMounted: function() { - return this._lifeCycleState === ComponentLifeCycle.MOUNTED; - }, - - /** - * Sets a subset of the props. - * - * @param {object} partialProps Subset of the next props. - * @param {?function} callback Called after props are updated. - * @final - * @public - */ - setProps: function(partialProps, callback) { - // Merge with the pending element if it exists, otherwise with existing - // element props. - var element = this._pendingElement || this._currentElement; - this.replaceProps( - assign({}, element.props, partialProps), - callback - ); - }, - - /** - * Replaces all of the props. - * - * @param {object} props New props. - * @param {?function} callback Called after props are updated. - * @final - * @public - */ - replaceProps: function(props, callback) { - ("production" !== process.env.NODE_ENV ? invariant( - this.isMounted(), - 'replaceProps(...): Can only update a mounted component.' - ) : invariant(this.isMounted())); - ("production" !== process.env.NODE_ENV ? invariant( - this._mountDepth === 0, - 'replaceProps(...): You called `setProps` or `replaceProps` on a ' + - 'component with a parent. This is an anti-pattern since props will ' + - 'get reactively updated when rendered. Instead, change the owner\'s ' + - '`render` method to pass the correct value as props to the component ' + - 'where it is created.' - ) : invariant(this._mountDepth === 0)); - // This is a deoptimized path. We optimize for always having a element. - // This creates an extra internal element. - this._pendingElement = ReactElement.cloneAndReplaceProps( - this._pendingElement || this._currentElement, - props - ); - ReactUpdates.enqueueUpdate(this, callback); - }, - - /** - * Schedule a partial update to the props. Only used for internal testing. - * - * @param {object} partialProps Subset of the next props. - * @param {?function} callback Called after props are updated. - * @final - * @internal - */ - _setPropsInternal: function(partialProps, callback) { - // This is a deoptimized path. We optimize for always having a element. - // This creates an extra internal element. - var element = this._pendingElement || this._currentElement; - this._pendingElement = ReactElement.cloneAndReplaceProps( - element, - assign({}, element.props, partialProps) - ); - ReactUpdates.enqueueUpdate(this, callback); - }, - /** - * Base constructor for all React components. - * - * Subclasses that override this method should make sure to invoke - * `ReactComponent.Mixin.construct.call(this, ...)`. - * - * @param {ReactElement} element - * @internal - */ - construct: function(element) { - // This is the public exposed props object after it has been processed - // with default props. The element's props represents the true internal - // state of the props. - this.props = element.props; - // Record the component responsible for creating this component. - // This is accessible through the element but we maintain an extra - // field for compatibility with devtools and as a way to make an - // incremental update. TODO: Consider deprecating this field. - this._owner = element._owner; - - // All components start unmounted. - this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; - - // See ReactUpdates. - this._pendingCallbacks = null; - - // We keep the old element and a reference to the pending element - // to track updates. - this._currentElement = element; - this._pendingElement = null; - }, - - /** - * Initializes the component, renders markup, and registers event listeners. - * - * NOTE: This does not insert any nodes into the DOM. - * - * Subclasses that override this method should make sure to invoke - * `ReactComponent.Mixin.mountComponent.call(this, ...)`. - * - * @param {string} rootID DOM ID of the root node. - * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction - * @param {number} mountDepth number of components in the owner hierarchy. - * @return {?string} Rendered markup to be inserted into the DOM. - * @internal - */ - mountComponent: function(rootID, transaction, mountDepth) { - ("production" !== process.env.NODE_ENV ? invariant( - !this.isMounted(), - 'mountComponent(%s, ...): Can only mount an unmounted component. ' + - 'Make sure to avoid storing components between renders or reusing a ' + - 'single component instance in multiple places.', - rootID - ) : invariant(!this.isMounted())); - var ref = this._currentElement.ref; - if (ref != null) { - var owner = this._currentElement._owner; - ReactOwner.addComponentAsRefTo(this, ref, owner); - } - this._rootNodeID = rootID; - this._lifeCycleState = ComponentLifeCycle.MOUNTED; - this._mountDepth = mountDepth; - // Effectively: return ''; - }, - - /** - * Releases any resources allocated by `mountComponent`. - * - * NOTE: This does not remove any nodes from the DOM. - * - * Subclasses that override this method should make sure to invoke - * `ReactComponent.Mixin.unmountComponent.call(this)`. - * - * @internal - */ - unmountComponent: function() { - ("production" !== process.env.NODE_ENV ? invariant( - this.isMounted(), - 'unmountComponent(): Can only unmount a mounted component.' - ) : invariant(this.isMounted())); - var ref = this._currentElement.ref; - if (ref != null) { - ReactOwner.removeComponentAsRefFrom(this, ref, this._owner); - } - unmountIDFromEnvironment(this._rootNodeID); - this._rootNodeID = null; - this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; - }, - - /** - * Given a new instance of this component, updates the rendered DOM nodes - * as if that instance was rendered instead. - * - * Subclasses that override this method should make sure to invoke - * `ReactComponent.Mixin.receiveComponent.call(this, ...)`. - * - * @param {object} nextComponent Next set of properties. - * @param {ReactReconcileTransaction} transaction - * @internal - */ - receiveComponent: function(nextElement, transaction) { - ("production" !== process.env.NODE_ENV ? invariant( - this.isMounted(), - 'receiveComponent(...): Can only update a mounted component.' - ) : invariant(this.isMounted())); - this._pendingElement = nextElement; - this.performUpdateIfNecessary(transaction); - }, - - /** - * If `_pendingElement` is set, update the component. - * - * @param {ReactReconcileTransaction} transaction - * @internal - */ - performUpdateIfNecessary: function(transaction) { - if (this._pendingElement == null) { - return; - } - var prevElement = this._currentElement; - var nextElement = this._pendingElement; - this._currentElement = nextElement; - this.props = nextElement.props; - this._owner = nextElement._owner; - this._pendingElement = null; - this.updateComponent(transaction, prevElement); - }, - - /** - * Updates the component's currently mounted representation. - * - * @param {ReactReconcileTransaction} transaction - * @param {object} prevElement - * @internal - */ - updateComponent: function(transaction, prevElement) { - var nextElement = this._currentElement; - - // If either the owner or a `ref` has changed, make sure the newest owner - // has stored a reference to `this`, and the previous owner (if different) - // has forgotten the reference to `this`. We use the element instead - // of the public this.props because the post processing cannot determine - // a ref. The ref conceptually lives on the element. - - // TODO: Should this even be possible? The owner cannot change because - // it's forbidden by shouldUpdateReactComponent. The ref can change - // if you swap the keys of but not the refs. Reconsider where this check - // is made. It probably belongs where the key checking and - // instantiateReactComponent is done. - - if (nextElement._owner !== prevElement._owner || - nextElement.ref !== prevElement.ref) { - if (prevElement.ref != null) { - ReactOwner.removeComponentAsRefFrom( - this, prevElement.ref, prevElement._owner - ); - } - // Correct, even if the owner is the same, and only the ref has changed. - if (nextElement.ref != null) { - ReactOwner.addComponentAsRefTo( - this, - nextElement.ref, - nextElement._owner - ); - } - } - }, - - /** - * Mounts this component and inserts it into the DOM. - * - * @param {string} rootID DOM ID of the root node. - * @param {DOMElement} container DOM element to mount into. - * @param {boolean} shouldReuseMarkup If true, do not insert markup - * @final - * @internal - * @see {ReactMount.render} - */ - mountComponentIntoNode: function(rootID, container, shouldReuseMarkup) { - var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); - transaction.perform( - this._mountComponentIntoNode, - this, - rootID, - container, - transaction, - shouldReuseMarkup - ); - ReactUpdates.ReactReconcileTransaction.release(transaction); - }, - - /** - * @param {string} rootID DOM ID of the root node. - * @param {DOMElement} container DOM element to mount into. - * @param {ReactReconcileTransaction} transaction - * @param {boolean} shouldReuseMarkup If true, do not insert markup - * @final - * @private - */ - _mountComponentIntoNode: function( - rootID, - container, - transaction, - shouldReuseMarkup) { - var markup = this.mountComponent(rootID, transaction, 0); - mountImageIntoNode(markup, container, shouldReuseMarkup); - }, - - /** - * Checks if this component is owned by the supplied `owner` component. - * - * @param {ReactComponent} owner Component to check. - * @return {boolean} True if `owners` owns this component. - * @final - * @internal - */ - isOwnedBy: function(owner) { - return this._owner === owner; - }, - - /** - * Gets another component, that shares the same owner as this one, by ref. - * - * @param {string} ref of a sibling Component. - * @return {?ReactComponent} the actual sibling Component. - * @final - * @internal - */ - getSiblingByRef: function(ref) { - var owner = this._owner; - if (!owner || !owner.refs) { - return null; - } - return owner.refs[ref]; - } - } -}; - -module.exports = ReactComponent; - -}).call(this,require('_process')) -},{"./Object.assign":70,"./ReactElement":99,"./ReactOwner":115,"./ReactUpdates":132,"./invariant":182,"./keyMirror":188,"_process":1}],79:[function(require,module,exports){ -(function (process){ -/** - * Copyright 2013-2014, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactComponentBrowserEnvironment - */ - -/*jslint evil: true */ - -"use strict"; - -var ReactDOMIDOperations = require("./ReactDOMIDOperations"); -var ReactMarkupChecksum = require("./ReactMarkupChecksum"); -var ReactMount = require("./ReactMount"); -var ReactPerf = require("./ReactPerf"); -var ReactReconcileTransaction = require("./ReactReconcileTransaction"); - -var getReactRootElementInContainer = require("./getReactRootElementInContainer"); -var invariant = require("./invariant"); -var setInnerHTML = require("./setInnerHTML"); - - -var ELEMENT_NODE_TYPE = 1; -var DOC_NODE_TYPE = 9; - - -/** - * Abstracts away all functionality of `ReactComponent` requires knowledge of - * the browser context. - */ -var ReactComponentBrowserEnvironment = { - ReactReconcileTransaction: ReactReconcileTransaction, - - BackendIDOperations: ReactDOMIDOperations, - - /** - * If a particular environment requires that some resources be cleaned up, - * specify this in the injected Mixin. In the DOM, we would likely want to - * purge any cached node ID lookups. - * - * @private - */ - unmountIDFromEnvironment: function(rootNodeID) { - ReactMount.purgeID(rootNodeID); - }, - - /** - * @param {string} markup Markup string to place into the DOM Element. - * @param {DOMElement} container DOM Element to insert markup into. - * @param {boolean} shouldReuseMarkup Should reuse the existing markup in the - * container if possible. - */ - mountImageIntoNode: ReactPerf.measure( - 'ReactComponentBrowserEnvironment', - 'mountImageIntoNode', - function(markup, container, shouldReuseMarkup) { - ("production" !== process.env.NODE_ENV ? invariant( - container && ( - container.nodeType === ELEMENT_NODE_TYPE || - container.nodeType === DOC_NODE_TYPE - ), - 'mountComponentIntoNode(...): Target container is not valid.' - ) : invariant(container && ( - container.nodeType === ELEMENT_NODE_TYPE || - container.nodeType === DOC_NODE_TYPE - ))); - - if (shouldReuseMarkup) { - if (ReactMarkupChecksum.canReuseMarkup( - markup, - getReactRootElementInContainer(container))) { - return; - } else { - ("production" !== process.env.NODE_ENV ? invariant( - container.nodeType !== DOC_NODE_TYPE, - 'You\'re trying to render a component to the document using ' + - 'server rendering but the checksum was invalid. This usually ' + - 'means you rendered a different component type or props on ' + - 'the client from the one on the server, or your render() ' + - 'methods are impure. React cannot handle this case due to ' + - 'cross-browser quirks by rendering at the document root. You ' + - 'should look for environment dependent code in your components ' + - 'and ensure the props are the same client and server side.' - ) : invariant(container.nodeType !== DOC_NODE_TYPE)); - - if ("production" !== process.env.NODE_ENV) { - console.warn( - 'React attempted to use reuse markup in a container but the ' + - 'checksum was invalid. This generally means that you are ' + - 'using server rendering and the markup generated on the ' + - 'server was not what the client was expecting. React injected ' + - 'new markup to compensate which works but you have lost many ' + - 'of the benefits of server rendering. Instead, figure out ' + - 'why the markup being generated is different on the client ' + - 'or server.' - ); - } - } - } - - ("production" !== process.env.NODE_ENV ? invariant( - container.nodeType !== DOC_NODE_TYPE, - 'You\'re trying to render a component to the document but ' + - 'you didn\'t use server rendering. We can\'t do this ' + - 'without using server rendering due to cross-browser quirks. ' + - 'See renderComponentToString() for server rendering.' - ) : invariant(container.nodeType !== DOC_NODE_TYPE)); - - setInnerHTML(container, markup); - } - ) -}; - -module.exports = ReactComponentBrowserEnvironment; - -}).call(this,require('_process')) -},{"./ReactDOMIDOperations":88,"./ReactMarkupChecksum":110,"./ReactMount":111,"./ReactPerf":116,"./ReactReconcileTransaction":122,"./getReactRootElementInContainer":176,"./invariant":182,"./setInnerHTML":196,"_process":1}],80:[function(require,module,exports){ -/** - * Copyright 2013-2014, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * -* @providesModule ReactComponentWithPureRenderMixin -*/ - -"use strict"; - -var shallowEqual = require("./shallowEqual"); - -/** - * If your React component's render function is "pure", e.g. it will render the - * same result given the same props and state, provide this Mixin for a - * considerable performance boost. - * - * Most React components have pure render functions. - * - * Example: - * - * var ReactComponentWithPureRenderMixin = - * require('ReactComponentWithPureRenderMixin'); - * React.createClass({ - * mixins: [ReactComponentWithPureRenderMixin], - * - * render: function() { - * return <div className={this.props.className}>foo</div>; - * } - * }); - * - * Note: This only checks shallow equality for props and state. If these contain - * complex data structures this mixin may have false-negatives for deeper - * differences. Only mixin to components which have simple props and state, or - * use `forceUpdate()` when you know deep data structures have changed. - */ -var ReactComponentWithPureRenderMixin = { - shouldComponentUpdate: function(nextProps, nextState) { - return !shallowEqual(this.props, nextProps) || - !shallowEqual(this.state, nextState); - } -}; - -module.exports = ReactComponentWithPureRenderMixin; - -},{"./shallowEqual":197}],81:[function(require,module,exports){ -(function (process){ -/** - * Copyright 2013-2014, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactCompositeComponent - */ - -"use strict"; +'use strict'; var ReactComponent = require("./ReactComponent"); -var ReactContext = require("./ReactContext"); var ReactCurrentOwner = require("./ReactCurrentOwner"); var ReactElement = require("./ReactElement"); -var ReactElementValidator = require("./ReactElementValidator"); -var ReactEmptyComponent = require("./ReactEmptyComponent"); var ReactErrorUtils = require("./ReactErrorUtils"); -var ReactLegacyElement = require("./ReactLegacyElement"); -var ReactOwner = require("./ReactOwner"); -var ReactPerf = require("./ReactPerf"); -var ReactPropTransferer = require("./ReactPropTransferer"); +var ReactInstanceMap = require("./ReactInstanceMap"); +var ReactLifeCycle = require("./ReactLifeCycle"); var ReactPropTypeLocations = require("./ReactPropTypeLocations"); var ReactPropTypeLocationNames = require("./ReactPropTypeLocationNames"); -var ReactUpdates = require("./ReactUpdates"); +var ReactUpdateQueue = require("./ReactUpdateQueue"); var assign = require("./Object.assign"); -var instantiateReactComponent = require("./instantiateReactComponent"); var invariant = require("./invariant"); var keyMirror = require("./keyMirror"); var keyOf = require("./keyOf"); -var monitorCodeUse = require("./monitorCodeUse"); -var mapObject = require("./mapObject"); -var shouldUpdateReactComponent = require("./shouldUpdateReactComponent"); var warning = require("./warning"); var MIXINS_KEY = keyOf({mixins: null}); /** - * Policies that describe methods in `ReactCompositeComponentInterface`. + * Policies that describe methods in `ReactClassInterface`. */ var SpecPolicy = keyMirror({ /** @@ -8942,7 +8763,7 @@ var SpecPolicy = keyMirror({ */ DEFINE_MANY: null, /** - * These methods are overriding the base ReactCompositeComponent class. + * These methods are overriding the base class. */ OVERRIDE_BASE: null, /** @@ -8960,7 +8781,7 @@ var injectedMixins = []; * Composite components are higher-level components that compose other composite * or native components. * - * To create a new type of `ReactCompositeComponent`, pass a specification of + * To create a new type of `ReactClass`, pass a specification of * your new class to `React.createClass`. The only requirement of your class * specification is that you implement a `render` method. * @@ -8971,14 +8792,14 @@ var injectedMixins = []; * }); * * The class specification supports a specific protocol of methods that have - * special meaning (e.g. `render`). See `ReactCompositeComponentInterface` for + * special meaning (e.g. `render`). See `ReactClassInterface` for * more the comprehensive protocol. Any other properties and methods in the * class specification will available on the prototype. * - * @interface ReactCompositeComponentInterface + * @interface ReactClassInterface * @internal */ -var ReactCompositeComponentInterface = { +var ReactClassInterface = { /** * An array of Mixin objects to include when defining your component. @@ -9226,11 +9047,13 @@ var RESERVED_SPEC_KEYS = { } }, childContextTypes: function(Constructor, childContextTypes) { - validateTypeDef( - Constructor, - childContextTypes, - ReactPropTypeLocations.childContext - ); + if ("production" !== process.env.NODE_ENV) { + validateTypeDef( + Constructor, + childContextTypes, + ReactPropTypeLocations.childContext + ); + } Constructor.childContextTypes = assign( {}, Constructor.childContextTypes, @@ -9238,11 +9061,13 @@ var RESERVED_SPEC_KEYS = { ); }, contextTypes: function(Constructor, contextTypes) { - validateTypeDef( - Constructor, - contextTypes, - ReactPropTypeLocations.context - ); + if ("production" !== process.env.NODE_ENV) { + validateTypeDef( + Constructor, + contextTypes, + ReactPropTypeLocations.context + ); + } Constructor.contextTypes = assign( {}, Constructor.contextTypes, @@ -9264,11 +9089,13 @@ var RESERVED_SPEC_KEYS = { } }, propTypes: function(Constructor, propTypes) { - validateTypeDef( - Constructor, - propTypes, - ReactPropTypeLocations.prop - ); + if ("production" !== process.env.NODE_ENV) { + validateTypeDef( + Constructor, + propTypes, + ReactPropTypeLocations.prop + ); + } Constructor.propTypes = assign( {}, Constructor.propTypes, @@ -9280,40 +9107,33 @@ var RESERVED_SPEC_KEYS = { } }; -function getDeclarationErrorAddendum(component) { - var owner = component._owner || null; - if (owner && owner.constructor && owner.constructor.displayName) { - return ' Check the render method of `' + owner.constructor.displayName + - '`.'; - } - return ''; -} - function validateTypeDef(Constructor, typeDef, location) { for (var propName in typeDef) { if (typeDef.hasOwnProperty(propName)) { - ("production" !== process.env.NODE_ENV ? invariant( - typeof typeDef[propName] == 'function', + // use a warning instead of an invariant so components + // don't show up in prod but not in __DEV__ + ("production" !== process.env.NODE_ENV ? warning( + typeof typeDef[propName] === 'function', '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', - Constructor.displayName || 'ReactCompositeComponent', + Constructor.displayName || 'ReactClass', ReactPropTypeLocationNames[location], propName - ) : invariant(typeof typeDef[propName] == 'function')); + ) : null); } } } function validateMethodOverride(proto, name) { - var specPolicy = ReactCompositeComponentInterface.hasOwnProperty(name) ? - ReactCompositeComponentInterface[name] : + var specPolicy = ReactClassInterface.hasOwnProperty(name) ? + ReactClassInterface[name] : null; // Disallow overriding of base class methods unless explicitly allowed. - if (ReactCompositeComponentMixin.hasOwnProperty(name)) { + if (ReactClassMixin.hasOwnProperty(name)) { ("production" !== process.env.NODE_ENV ? invariant( specPolicy === SpecPolicy.OVERRIDE_BASE, - 'ReactCompositeComponentInterface: You are attempting to override ' + + 'ReactClassInterface: You are attempting to override ' + '`%s` from your class specification. Ensure that your method names ' + 'do not overlap with React methods.', name @@ -9325,7 +9145,7 @@ function validateMethodOverride(proto, name) { ("production" !== process.env.NODE_ENV ? invariant( specPolicy === SpecPolicy.DEFINE_MANY || specPolicy === SpecPolicy.DEFINE_MANY_MERGED, - 'ReactCompositeComponentInterface: You are attempting to define ' + + 'ReactClassInterface: You are attempting to define ' + '`%s` on your component more than once. This conflict may be due ' + 'to a mixin.', name @@ -9334,29 +9154,9 @@ function validateMethodOverride(proto, name) { } } -function validateLifeCycleOnReplaceState(instance) { - var compositeLifeCycleState = instance._compositeLifeCycleState; - ("production" !== process.env.NODE_ENV ? invariant( - instance.isMounted() || - compositeLifeCycleState === CompositeLifeCycle.MOUNTING, - 'replaceState(...): Can only update a mounted or mounting component.' - ) : invariant(instance.isMounted() || - compositeLifeCycleState === CompositeLifeCycle.MOUNTING)); - ("production" !== process.env.NODE_ENV ? invariant( - ReactCurrentOwner.current == null, - 'replaceState(...): Cannot update during an existing state transition ' + - '(such as within `render`). Render methods should be a pure function ' + - 'of props and state.' - ) : invariant(ReactCurrentOwner.current == null)); - ("production" !== process.env.NODE_ENV ? invariant(compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING, - 'replaceState(...): Cannot update while unmounting component. This ' + - 'usually means you called setState() on an unmounted component.' - ) : invariant(compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING)); -} - /** * Mixin helper which handles policy validation and reserved - * specification keys when building `ReactCompositeComponent` classses. + * specification keys when building React classses. */ function mixSpecIntoComponent(Constructor, spec) { if (!spec) { @@ -9364,13 +9164,13 @@ function mixSpecIntoComponent(Constructor, spec) { } ("production" !== process.env.NODE_ENV ? invariant( - !ReactLegacyElement.isValidFactory(spec), - 'ReactCompositeComponent: You\'re attempting to ' + + typeof spec !== 'function', + 'ReactClass: You\'re attempting to ' + 'use a component class as a mixin. Instead, just use a regular object.' - ) : invariant(!ReactLegacyElement.isValidFactory(spec))); + ) : invariant(typeof spec !== 'function')); ("production" !== process.env.NODE_ENV ? invariant( !ReactElement.isValidElement(spec), - 'ReactCompositeComponent: You\'re attempting to ' + + 'ReactClass: You\'re attempting to ' + 'use a component as a mixin. Instead, just use a regular object.' ) : invariant(!ReactElement.isValidElement(spec))); @@ -9401,16 +9201,16 @@ function mixSpecIntoComponent(Constructor, spec) { } else { // Setup methods on prototype: // The following member methods should not be automatically bound: - // 1. Expected ReactCompositeComponent methods (in the "interface"). + // 1. Expected ReactClass methods (in the "interface"). // 2. Overridden methods (that were mixed in). - var isCompositeComponentMethod = - ReactCompositeComponentInterface.hasOwnProperty(name); + var isReactClassMethod = + ReactClassInterface.hasOwnProperty(name); var isAlreadyDefined = proto.hasOwnProperty(name); var markedDontBind = property && property.__reactDontBind; var isFunction = typeof property === 'function'; var shouldAutoBind = isFunction && - !isCompositeComponentMethod && + !isReactClassMethod && !isAlreadyDefined && !markedDontBind; @@ -9422,21 +9222,19 @@ function mixSpecIntoComponent(Constructor, spec) { proto[name] = property; } else { if (isAlreadyDefined) { - var specPolicy = ReactCompositeComponentInterface[name]; + var specPolicy = ReactClassInterface[name]; // These cases should already be caught by validateMethodOverride ("production" !== process.env.NODE_ENV ? invariant( - isCompositeComponentMethod && ( - specPolicy === SpecPolicy.DEFINE_MANY_MERGED || - specPolicy === SpecPolicy.DEFINE_MANY + isReactClassMethod && ( + (specPolicy === SpecPolicy.DEFINE_MANY_MERGED || specPolicy === SpecPolicy.DEFINE_MANY) ), - 'ReactCompositeComponent: Unexpected spec policy %s for key %s ' + + 'ReactClass: Unexpected spec policy %s for key %s ' + 'when mixing in component specs.', specPolicy, name - ) : invariant(isCompositeComponentMethod && ( - specPolicy === SpecPolicy.DEFINE_MANY_MERGED || - specPolicy === SpecPolicy.DEFINE_MANY + ) : invariant(isReactClassMethod && ( + (specPolicy === SpecPolicy.DEFINE_MANY_MERGED || specPolicy === SpecPolicy.DEFINE_MANY) ))); // For methods which are defined more than once, call the existing @@ -9474,7 +9272,7 @@ function mixStaticSpecIntoComponent(Constructor, statics) { var isReserved = name in RESERVED_SPEC_KEYS; ("production" !== process.env.NODE_ENV ? invariant( !isReserved, - 'ReactCompositeComponent: You are attempting to define a reserved ' + + 'ReactClass: You are attempting to define a reserved ' + 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + 'as an instance property instead; it will still be accessible on the ' + 'constructor.', @@ -9484,7 +9282,7 @@ function mixStaticSpecIntoComponent(Constructor, statics) { var isInherited = name in Constructor; ("production" !== process.env.NODE_ENV ? invariant( !isInherited, - 'ReactCompositeComponent: You are attempting to define ' + + 'ReactClass: You are attempting to define ' + '`%s` on your component more than once. This conflict may be ' + 'due to a mixin.', name @@ -9500,24 +9298,26 @@ function mixStaticSpecIntoComponent(Constructor, statics) { * @param {object} two The second object * @return {object} one after it has been mutated to contain everything in two. */ -function mergeObjectsWithNoDuplicateKeys(one, two) { +function mergeIntoWithNoDuplicateKeys(one, two) { ("production" !== process.env.NODE_ENV ? invariant( one && two && typeof one === 'object' && typeof two === 'object', - 'mergeObjectsWithNoDuplicateKeys(): Cannot merge non-objects' + 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.' ) : invariant(one && two && typeof one === 'object' && typeof two === 'object')); - mapObject(two, function(value, key) { - ("production" !== process.env.NODE_ENV ? invariant( - one[key] === undefined, - 'mergeObjectsWithNoDuplicateKeys(): ' + - 'Tried to merge two objects with the same key: `%s`. This conflict ' + - 'may be due to a mixin; in particular, this may be caused by two ' + - 'getInitialState() or getDefaultProps() methods returning objects ' + - 'with clashing keys.', - key - ) : invariant(one[key] === undefined)); - one[key] = value; - }); + for (var key in two) { + if (two.hasOwnProperty(key)) { + ("production" !== process.env.NODE_ENV ? invariant( + one[key] === undefined, + 'mergeIntoWithNoDuplicateKeys(): ' + + 'Tried to merge two objects with the same key: `%s`. This conflict ' + + 'may be due to a mixin; in particular, this may be caused by two ' + + 'getInitialState() or getDefaultProps() methods returning objects ' + + 'with clashing keys.', + key + ) : invariant(one[key] === undefined)); + one[key] = two[key]; + } + } return one; } @@ -9538,7 +9338,10 @@ function createMergedResultFunction(one, two) { } else if (b == null) { return a; } - return mergeObjectsWithNoDuplicateKeys(a, b); + var c = {}; + mergeIntoWithNoDuplicateKeys(c, a); + mergeIntoWithNoDuplicateKeys(c, b); + return c; }; } @@ -9558,48 +9361,680 @@ function createChainedFunction(one, two) { } /** - * `ReactCompositeComponent` maintains an auxiliary life cycle state in - * `this._compositeLifeCycleState` (which can be null). + * Binds a method to the component. + * + * @param {object} component Component whose method is going to be bound. + * @param {function} method Method to be bound. + * @return {function} The bound method. + */ +function bindAutoBindMethod(component, method) { + var boundMethod = method.bind(component); + if ("production" !== process.env.NODE_ENV) { + boundMethod.__reactBoundContext = component; + boundMethod.__reactBoundMethod = method; + boundMethod.__reactBoundArguments = null; + var componentName = component.constructor.displayName; + var _bind = boundMethod.bind; + /* eslint-disable block-scoped-var, no-undef */ + boundMethod.bind = function(newThis ) {for (var args=[],$__0=1,$__1=arguments.length;$__0<$__1;$__0++) args.push(arguments[$__0]); + // User is trying to bind() an autobound method; we effectively will + // ignore the value of "this" that the user is trying to use, so + // let's warn. + if (newThis !== component && newThis !== null) { + ("production" !== process.env.NODE_ENV ? warning( + false, + 'bind(): React component methods may only be bound to the ' + + 'component instance. See %s', + componentName + ) : null); + } else if (!args.length) { + ("production" !== process.env.NODE_ENV ? warning( + false, + 'bind(): You are binding a component method to the component. ' + + 'React does this for you automatically in a high-performance ' + + 'way, so you can safely remove this call. See %s', + componentName + ) : null); + return boundMethod; + } + var reboundMethod = _bind.apply(boundMethod, arguments); + reboundMethod.__reactBoundContext = component; + reboundMethod.__reactBoundMethod = method; + reboundMethod.__reactBoundArguments = args; + return reboundMethod; + /* eslint-enable */ + }; + } + return boundMethod; +} + +/** + * Binds all auto-bound methods in a component. + * + * @param {object} component Component whose method is going to be bound. + */ +function bindAutoBindMethods(component) { + for (var autoBindKey in component.__reactAutoBindMap) { + if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) { + var method = component.__reactAutoBindMap[autoBindKey]; + component[autoBindKey] = bindAutoBindMethod( + component, + ReactErrorUtils.guard( + method, + component.constructor.displayName + '.' + autoBindKey + ) + ); + } + } +} + +var typeDeprecationDescriptor = { + enumerable: false, + get: function() { + var displayName = this.displayName || this.name || 'Component'; + ("production" !== process.env.NODE_ENV ? warning( + false, + '%s.type is deprecated. Use %s directly to access the class.', + displayName, + displayName + ) : null); + Object.defineProperty(this, 'type', { + value: this + }); + return this; + } +}; + +/** + * Add more to the ReactClass base class. These are all legacy features and + * therefore not already part of the modern ReactComponent. + */ +var ReactClassMixin = { + + /** + * TODO: This will be deprecated because state should always keep a consistent + * type signature and the only use case for this, is to avoid that. + */ + replaceState: function(newState, callback) { + ReactUpdateQueue.enqueueReplaceState(this, newState); + if (callback) { + ReactUpdateQueue.enqueueCallback(this, callback); + } + }, + + /** + * Checks whether or not this composite component is mounted. + * @return {boolean} True if mounted, false otherwise. + * @protected + * @final + */ + isMounted: function() { + if ("production" !== process.env.NODE_ENV) { + var owner = ReactCurrentOwner.current; + if (owner !== null) { + ("production" !== process.env.NODE_ENV ? warning( + owner._warnedAboutRefsInRender, + '%s is accessing isMounted inside its render() function. ' + + 'render() should be a pure function of props and state. It should ' + + 'never access something that requires stale data from the previous ' + + 'render, such as refs. Move this logic to componentDidMount and ' + + 'componentDidUpdate instead.', + owner.getName() || 'A component' + ) : null); + owner._warnedAboutRefsInRender = true; + } + } + var internalInstance = ReactInstanceMap.get(this); + return ( + internalInstance && + internalInstance !== ReactLifeCycle.currentlyMountingInstance + ); + }, + + /** + * Sets a subset of the props. + * + * @param {object} partialProps Subset of the next props. + * @param {?function} callback Called after props are updated. + * @final + * @public + * @deprecated + */ + setProps: function(partialProps, callback) { + ReactUpdateQueue.enqueueSetProps(this, partialProps); + if (callback) { + ReactUpdateQueue.enqueueCallback(this, callback); + } + }, + + /** + * Replace all the props. + * + * @param {object} newProps Subset of the next props. + * @param {?function} callback Called after props are updated. + * @final + * @public + * @deprecated + */ + replaceProps: function(newProps, callback) { + ReactUpdateQueue.enqueueReplaceProps(this, newProps); + if (callback) { + ReactUpdateQueue.enqueueCallback(this, callback); + } + } +}; + +var ReactClassComponent = function() {}; +assign( + ReactClassComponent.prototype, + ReactComponent.prototype, + ReactClassMixin +); + +/** + * Module for creating composite components. + * + * @class ReactClass + */ +var ReactClass = { + + /** + * Creates a composite component class given a class specification. + * + * @param {object} spec Class specification (which must define `render`). + * @return {function} Component constructor function. + * @public + */ + createClass: function(spec) { + var Constructor = function(props, context) { + // This constructor is overridden by mocks. The argument is used + // by mocks to assert on what gets mounted. + + if ("production" !== process.env.NODE_ENV) { + ("production" !== process.env.NODE_ENV ? warning( + this instanceof Constructor, + 'Something is calling a React component directly. Use a factory or ' + + 'JSX instead. See: http://fb.me/react-legacyfactory' + ) : null); + } + + // Wire up auto-binding + if (this.__reactAutoBindMap) { + bindAutoBindMethods(this); + } + + this.props = props; + this.context = context; + this.state = null; + + // ReactClasses doesn't have constructors. Instead, they use the + // getInitialState and componentWillMount methods for initialization. + + var initialState = this.getInitialState ? this.getInitialState() : null; + if ("production" !== process.env.NODE_ENV) { + // We allow auto-mocks to proceed as if they're returning null. + if (typeof initialState === 'undefined' && + this.getInitialState._isMockFunction) { + // This is probably bad practice. Consider warning here and + // deprecating this convenience. + initialState = null; + } + } + ("production" !== process.env.NODE_ENV ? invariant( + typeof initialState === 'object' && !Array.isArray(initialState), + '%s.getInitialState(): must return an object or null', + Constructor.displayName || 'ReactCompositeComponent' + ) : invariant(typeof initialState === 'object' && !Array.isArray(initialState))); + + this.state = initialState; + }; + Constructor.prototype = new ReactClassComponent(); + Constructor.prototype.constructor = Constructor; + + injectedMixins.forEach( + mixSpecIntoComponent.bind(null, Constructor) + ); + + mixSpecIntoComponent(Constructor, spec); + + // Initialize the defaultProps property after all mixins have been merged + if (Constructor.getDefaultProps) { + Constructor.defaultProps = Constructor.getDefaultProps(); + } + + if ("production" !== process.env.NODE_ENV) { + // This is a tag to indicate that the use of these method names is ok, + // since it's used with createClass. If it's not, then it's likely a + // mistake so we'll warn you to use the static property, property + // initializer or constructor respectively. + if (Constructor.getDefaultProps) { + Constructor.getDefaultProps.isReactClassApproved = {}; + } + if (Constructor.prototype.getInitialState) { + Constructor.prototype.getInitialState.isReactClassApproved = {}; + } + } + + ("production" !== process.env.NODE_ENV ? invariant( + Constructor.prototype.render, + 'createClass(...): Class specification must implement a `render` method.' + ) : invariant(Constructor.prototype.render)); + + if ("production" !== process.env.NODE_ENV) { + ("production" !== process.env.NODE_ENV ? warning( + !Constructor.prototype.componentShouldUpdate, + '%s has a method called ' + + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + + 'The name is phrased as a question because the function is ' + + 'expected to return a value.', + spec.displayName || 'A component' + ) : null); + } + + // Reduce time spent doing lookups by setting these on the prototype. + for (var methodName in ReactClassInterface) { + if (!Constructor.prototype[methodName]) { + Constructor.prototype[methodName] = null; + } + } + + // Legacy hook + Constructor.type = Constructor; + if ("production" !== process.env.NODE_ENV) { + try { + Object.defineProperty(Constructor, 'type', typeDeprecationDescriptor); + } catch (x) { + // IE will fail on defineProperty (es5-shim/sham too) + } + } + + return Constructor; + }, + + injection: { + injectMixin: function(mixin) { + injectedMixins.push(mixin); + } + } + +}; + +module.exports = ReactClass; + +}).call(this,require('_process')) +},{"./Object.assign":69,"./ReactComponent":79,"./ReactCurrentOwner":85,"./ReactElement":103,"./ReactErrorUtils":106,"./ReactInstanceMap":113,"./ReactLifeCycle":114,"./ReactPropTypeLocationNames":124,"./ReactPropTypeLocations":125,"./ReactUpdateQueue":139,"./invariant":191,"./keyMirror":197,"./keyOf":198,"./warning":212,"_process":1}],79:[function(require,module,exports){ +(function (process){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactComponent + */ + +'use strict'; + +var ReactUpdateQueue = require("./ReactUpdateQueue"); + +var invariant = require("./invariant"); +var warning = require("./warning"); + +/** + * Base class helpers for the updating state of a component. + */ +function ReactComponent(props, context) { + this.props = props; + this.context = context; +} + +/** + * Sets a subset of the state. Always use this to mutate + * state. You should treat `this.state` as immutable. + * + * There is no guarantee that `this.state` will be immediately updated, so + * accessing `this.state` after calling this method may return the old value. + * + * There is no guarantee that calls to `setState` will run synchronously, + * as they may eventually be batched together. You can provide an optional + * callback that will be executed when the call to setState is actually + * completed. + * + * When a function is provided to setState, it will be called at some point in + * the future (not synchronously). It will be called with the up to date + * component arguments (state, props, context). These values can be different + * from this.* because your function may be called after receiveProps but before + * shouldComponentUpdate, and this new state, props, and context will not yet be + * assigned to this. + * + * @param {object|function} partialState Next partial state or function to + * produce next partial state to be merged with current state. + * @param {?function} callback Called after state is updated. + * @final + * @protected + */ +ReactComponent.prototype.setState = function(partialState, callback) { + ("production" !== process.env.NODE_ENV ? invariant( + typeof partialState === 'object' || + typeof partialState === 'function' || + partialState == null, + 'setState(...): takes an object of state variables to update or a ' + + 'function which returns an object of state variables.' + ) : invariant(typeof partialState === 'object' || + typeof partialState === 'function' || + partialState == null)); + if ("production" !== process.env.NODE_ENV) { + ("production" !== process.env.NODE_ENV ? warning( + partialState != null, + 'setState(...): You passed an undefined or null state object; ' + + 'instead, use forceUpdate().' + ) : null); + } + ReactUpdateQueue.enqueueSetState(this, partialState); + if (callback) { + ReactUpdateQueue.enqueueCallback(this, callback); + } +}; + +/** + * Forces an update. This should only be invoked when it is known with + * certainty that we are **not** in a DOM transaction. + * + * You may want to call this when you know that some deeper aspect of the + * component's state has changed but `setState` was not called. + * + * This will not invoke `shouldComponentUpdate`, but it will invoke + * `componentWillUpdate` and `componentDidUpdate`. + * + * @param {?function} callback Called after update is complete. + * @final + * @protected + */ +ReactComponent.prototype.forceUpdate = function(callback) { + ReactUpdateQueue.enqueueForceUpdate(this); + if (callback) { + ReactUpdateQueue.enqueueCallback(this, callback); + } +}; + +/** + * Deprecated APIs. These APIs used to exist on classic React classes but since + * we would like to deprecate them, we're not going to move them over to this + * modern base class. Instead, we define a getter that warns if it's accessed. + */ +if ("production" !== process.env.NODE_ENV) { + var deprecatedAPIs = { + getDOMNode: 'getDOMNode', + isMounted: 'isMounted', + replaceProps: 'replaceProps', + replaceState: 'replaceState', + setProps: 'setProps' + }; + var defineDeprecationWarning = function(methodName, displayName) { + try { + Object.defineProperty(ReactComponent.prototype, methodName, { + get: function() { + ("production" !== process.env.NODE_ENV ? warning( + false, + '%s(...) is deprecated in plain JavaScript React classes.', + displayName + ) : null); + return undefined; + } + }); + } catch (x) { + // IE will fail on defineProperty (es5-shim/sham too) + } + }; + for (var fnName in deprecatedAPIs) { + if (deprecatedAPIs.hasOwnProperty(fnName)) { + defineDeprecationWarning(fnName, deprecatedAPIs[fnName]); + } + } +} + +module.exports = ReactComponent; + +}).call(this,require('_process')) +},{"./ReactUpdateQueue":139,"./invariant":191,"./warning":212,"_process":1}],80:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. * - * This is different from the life cycle state maintained by `ReactComponent` in - * `this._lifeCycleState`. The following diagram shows how the states overlap in - * time. There are times when the CompositeLifeCycle is null - at those times it - * is only meaningful to look at ComponentLifeCycle alone. + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. * - * Top Row: ReactComponent.ComponentLifeCycle - * Low Row: ReactComponent.CompositeLifeCycle + * @providesModule ReactComponentBrowserEnvironment + */ + +/*jslint evil: true */ + +'use strict'; + +var ReactDOMIDOperations = require("./ReactDOMIDOperations"); +var ReactMount = require("./ReactMount"); + +/** + * Abstracts away all functionality of the reconciler that requires knowledge of + * the browser context. TODO: These callers should be refactored to avoid the + * need for this injection. + */ +var ReactComponentBrowserEnvironment = { + + processChildrenUpdates: + ReactDOMIDOperations.dangerouslyProcessChildrenUpdates, + + replaceNodeWithMarkupByID: + ReactDOMIDOperations.dangerouslyReplaceNodeWithMarkupByID, + + /** + * If a particular environment requires that some resources be cleaned up, + * specify this in the injected Mixin. In the DOM, we would likely want to + * purge any cached node ID lookups. + * + * @private + */ + unmountIDFromEnvironment: function(rootNodeID) { + ReactMount.purgeID(rootNodeID); + } + +}; + +module.exports = ReactComponentBrowserEnvironment; + +},{"./ReactDOMIDOperations":90,"./ReactMount":117}],81:[function(require,module,exports){ +(function (process){ +/** + * Copyright 2014-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. * - * +-------+---------------------------------+--------+ - * | UN | MOUNTED | UN | - * |MOUNTED| | MOUNTED| - * +-------+---------------------------------+--------+ - * | ^--------+ +-------+ +--------^ | - * | | | | | | | | - * | 0--|MOUNTING|-0-|RECEIVE|-0-| UN |--->0 | - * | | | |PROPS | |MOUNTING| | - * | | | | | | | | - * | | | | | | | | - * | +--------+ +-------+ +--------+ | - * | | | | - * +-------+---------------------------------+--------+ + * @providesModule ReactComponentEnvironment */ -var CompositeLifeCycle = keyMirror({ + +'use strict'; + +var invariant = require("./invariant"); + +var injected = false; + +var ReactComponentEnvironment = { + /** - * Components in the process of being mounted respond to state changes - * differently. + * Optionally injectable environment dependent cleanup hook. (server vs. + * browser etc). Example: A browser system caches DOM nodes based on component + * ID and must remove that cache entry when this instance is unmounted. */ - MOUNTING: null, + unmountIDFromEnvironment: null, + /** - * Components in the process of being unmounted are guarded against state - * changes. + * Optionally injectable hook for swapping out mount images in the middle of + * the tree. */ - UNMOUNTING: null, + replaceNodeWithMarkupByID: null, + /** - * Components that are mounted and receiving new props respond to state - * changes differently. + * Optionally injectable hook for processing a queue of child updates. Will + * later move into MultiChildComponents. */ - RECEIVING_PROPS: null -}); + processChildrenUpdates: null, + + injection: { + injectEnvironment: function(environment) { + ("production" !== process.env.NODE_ENV ? invariant( + !injected, + 'ReactCompositeComponent: injectEnvironment() can only be called once.' + ) : invariant(!injected)); + ReactComponentEnvironment.unmountIDFromEnvironment = + environment.unmountIDFromEnvironment; + ReactComponentEnvironment.replaceNodeWithMarkupByID = + environment.replaceNodeWithMarkupByID; + ReactComponentEnvironment.processChildrenUpdates = + environment.processChildrenUpdates; + injected = true; + } + } + +}; + +module.exports = ReactComponentEnvironment; + +}).call(this,require('_process')) +},{"./invariant":191,"_process":1}],82:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * +* @providesModule ReactComponentWithPureRenderMixin +*/ + +'use strict'; + +var shallowEqual = require("./shallowEqual"); + +/** + * If your React component's render function is "pure", e.g. it will render the + * same result given the same props and state, provide this Mixin for a + * considerable performance boost. + * + * Most React components have pure render functions. + * + * Example: + * + * var ReactComponentWithPureRenderMixin = + * require('ReactComponentWithPureRenderMixin'); + * React.createClass({ + * mixins: [ReactComponentWithPureRenderMixin], + * + * render: function() { + * return <div className={this.props.className}>foo</div>; + * } + * }); + * + * Note: This only checks shallow equality for props and state. If these contain + * complex data structures this mixin may have false-negatives for deeper + * differences. Only mixin to components which have simple props and state, or + * use `forceUpdate()` when you know deep data structures have changed. + */ +var ReactComponentWithPureRenderMixin = { + shouldComponentUpdate: function(nextProps, nextState) { + return !shallowEqual(this.props, nextProps) || + !shallowEqual(this.state, nextState); + } +}; + +module.exports = ReactComponentWithPureRenderMixin; + +},{"./shallowEqual":207}],83:[function(require,module,exports){ +(function (process){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactCompositeComponent + */ + +'use strict'; + +var ReactComponentEnvironment = require("./ReactComponentEnvironment"); +var ReactContext = require("./ReactContext"); +var ReactCurrentOwner = require("./ReactCurrentOwner"); +var ReactElement = require("./ReactElement"); +var ReactElementValidator = require("./ReactElementValidator"); +var ReactInstanceMap = require("./ReactInstanceMap"); +var ReactLifeCycle = require("./ReactLifeCycle"); +var ReactNativeComponent = require("./ReactNativeComponent"); +var ReactPerf = require("./ReactPerf"); +var ReactPropTypeLocations = require("./ReactPropTypeLocations"); +var ReactPropTypeLocationNames = require("./ReactPropTypeLocationNames"); +var ReactReconciler = require("./ReactReconciler"); +var ReactUpdates = require("./ReactUpdates"); + +var assign = require("./Object.assign"); +var emptyObject = require("./emptyObject"); +var invariant = require("./invariant"); +var shouldUpdateReactComponent = require("./shouldUpdateReactComponent"); +var warning = require("./warning"); + +function getDeclarationErrorAddendum(component) { + var owner = component._currentElement._owner || null; + if (owner) { + var name = owner.getName(); + if (name) { + return ' Check the render method of `' + name + '`.'; + } + } + return ''; +} + +/** + * ------------------ The Life-Cycle of a Composite Component ------------------ + * + * - constructor: Initialization of state. The instance is now retained. + * - componentWillMount + * - render + * - [children's constructors] + * - [children's componentWillMount and render] + * - [children's componentDidMount] + * - componentDidMount + * + * Update Phases: + * - componentWillReceiveProps (only called if parent updated) + * - shouldComponentUpdate + * - componentWillUpdate + * - render + * - [children's constructors or receive props phases] + * - componentDidUpdate + * + * - componentWillUnmount + * - [children's componentWillUnmount] + * - [children destroyed] + * - (destroyed): The instance is now blank, released by React and ready for GC. + * + * ----------------------------------------------------------------------------- + */ + +/** + * An incrementing ID assigned to each component when it is mounted. This is + * used to enforce the order in which `ReactUpdates` updates dirty components. + * + * @private + */ +var nextMountID = 1; /** * @lends {ReactCompositeComponent.prototype} @@ -9614,29 +10049,24 @@ var ReactCompositeComponentMixin = { * @internal */ construct: function(element) { - // Children can be either an array or more than one argument - ReactComponent.Mixin.construct.apply(this, arguments); - ReactOwner.Mixin.construct.apply(this, arguments); + this._currentElement = element; + this._rootNodeID = null; + this._instance = null; - this.state = null; - this._pendingState = null; + // See ReactUpdateQueue + this._pendingElement = null; + this._pendingStateQueue = null; + this._pendingReplaceState = false; + this._pendingForceUpdate = false; - // This is the public post-processed context. The real context and pending - // context lives on the element. - this.context = null; + this._renderedComponent = null; - this._compositeLifeCycleState = null; - }, + this._context = null; + this._mountOrder = 0; + this._isTopLevel = false; - /** - * Checks whether or not this composite component is mounted. - * @return {boolean} True if mounted, false otherwise. - * @protected - * @final - */ - isMounted: function() { - return ReactComponent.Mixin.isMounted.call(this) && - this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING; + // See ReactUpdates and ReactUpdateQueue. + this._pendingCallbacks = null; }, /** @@ -9644,68 +10074,137 @@ var ReactCompositeComponentMixin = { * * @param {string} rootID DOM ID of the root node. * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction - * @param {number} mountDepth number of components in the owner hierarchy * @return {?string} Rendered markup to be inserted into the DOM. * @final * @internal */ - mountComponent: ReactPerf.measure( - 'ReactCompositeComponent', - 'mountComponent', - function(rootID, transaction, mountDepth) { - ReactComponent.Mixin.mountComponent.call( - this, - rootID, - transaction, - mountDepth - ); - this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING; + mountComponent: function(rootID, transaction, context) { + this._context = context; + this._mountOrder = nextMountID++; + this._rootNodeID = rootID; - if (this.__reactAutoBindMap) { - this._bindAutoBindMethods(); - } + var publicProps = this._processProps(this._currentElement.props); + var publicContext = this._processContext(this._currentElement._context); + + var Component = ReactNativeComponent.getComponentClassForElement( + this._currentElement + ); - this.context = this._processContext(this._currentElement._context); - this.props = this._processProps(this.props); + // Initialize the public class + var inst = new Component(publicProps, publicContext); - this.state = this.getInitialState ? this.getInitialState() : null; - ("production" !== process.env.NODE_ENV ? invariant( - typeof this.state === 'object' && !Array.isArray(this.state), - '%s.getInitialState(): must return an object or null', - this.constructor.displayName || 'ReactCompositeComponent' - ) : invariant(typeof this.state === 'object' && !Array.isArray(this.state))); + if ("production" !== process.env.NODE_ENV) { + // This will throw later in _renderValidatedComponent, but add an early + // warning now to help debugging + ("production" !== process.env.NODE_ENV ? warning( + inst.render != null, + '%s(...): No `render` method found on the returned component ' + + 'instance: you may have forgotten to define `render` in your ' + + 'component or you may have accidentally tried to render an element ' + + 'whose type is a function that isn\'t a React component.', + Component.displayName || Component.name || 'Component' + ) : null); + } - this._pendingState = null; - this._pendingForceUpdate = false; + // These should be set up in the constructor, but as a convenience for + // simpler class abstractions, we set them up after the fact. + inst.props = publicProps; + inst.context = publicContext; + inst.refs = emptyObject; + + this._instance = inst; - if (this.componentWillMount) { - this.componentWillMount(); + // Store a reference from the instance back to the internal representation + ReactInstanceMap.set(inst, this); + + if ("production" !== process.env.NODE_ENV) { + this._warnIfContextsDiffer(this._currentElement._context, context); + } + + if ("production" !== process.env.NODE_ENV) { + // Since plain JS classes are defined without any special initialization + // logic, we can not catch common errors early. Therefore, we have to + // catch them here, at initialization time, instead. + ("production" !== process.env.NODE_ENV ? warning( + !inst.getInitialState || + inst.getInitialState.isReactClassApproved, + 'getInitialState was defined on %s, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Did you mean to define a state property instead?', + this.getName() || 'a component' + ) : null); + ("production" !== process.env.NODE_ENV ? warning( + !inst.propTypes, + 'propTypes was defined as an instance property on %s. Use a static ' + + 'property to define propTypes instead.', + this.getName() || 'a component' + ) : null); + ("production" !== process.env.NODE_ENV ? warning( + !inst.contextTypes, + 'contextTypes was defined as an instance property on %s. Use a ' + + 'static property to define contextTypes instead.', + this.getName() || 'a component' + ) : null); + ("production" !== process.env.NODE_ENV ? warning( + typeof inst.componentShouldUpdate !== 'function', + '%s has a method called ' + + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + + 'The name is phrased as a question because the function is ' + + 'expected to return a value.', + (this.getName() || 'A component') + ) : null); + } + + var initialState = inst.state; + if (initialState === undefined) { + inst.state = initialState = null; + } + ("production" !== process.env.NODE_ENV ? invariant( + typeof initialState === 'object' && !Array.isArray(initialState), + '%s.state: must be set to an object or null', + this.getName() || 'ReactCompositeComponent' + ) : invariant(typeof initialState === 'object' && !Array.isArray(initialState))); + + this._pendingStateQueue = null; + this._pendingReplaceState = false; + this._pendingForceUpdate = false; + + var renderedElement; + + var previouslyMounting = ReactLifeCycle.currentlyMountingInstance; + ReactLifeCycle.currentlyMountingInstance = this; + try { + if (inst.componentWillMount) { + inst.componentWillMount(); // When mounting, calls to `setState` by `componentWillMount` will set - // `this._pendingState` without triggering a re-render. - if (this._pendingState) { - this.state = this._pendingState; - this._pendingState = null; + // `this._pendingStateQueue` without triggering a re-render. + if (this._pendingStateQueue) { + inst.state = this._processPendingState(inst.props, inst.context); } } - this._renderedComponent = instantiateReactComponent( - this._renderValidatedComponent(), - this._currentElement.type // The wrapping type - ); + renderedElement = this._renderValidatedComponent(); + } finally { + ReactLifeCycle.currentlyMountingInstance = previouslyMounting; + } - // Done with mounting, `setState` will now trigger UI changes. - this._compositeLifeCycleState = null; - var markup = this._renderedComponent.mountComponent( - rootID, - transaction, - mountDepth + 1 - ); - if (this.componentDidMount) { - transaction.getReactMountReady().enqueue(this.componentDidMount, this); - } - return markup; + this._renderedComponent = this._instantiateReactComponent( + renderedElement, + this._currentElement.type // The wrapping type + ); + + var markup = ReactReconciler.mountComponent( + this._renderedComponent, + rootID, + transaction, + this._processChildContext(context) + ); + if (inst.componentDidMount) { + transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); } - ), + + return markup; + }, /** * Releases any resources allocated by `mountComponent`. @@ -9714,83 +10213,88 @@ var ReactCompositeComponentMixin = { * @internal */ unmountComponent: function() { - this._compositeLifeCycleState = CompositeLifeCycle.UNMOUNTING; - if (this.componentWillUnmount) { - this.componentWillUnmount(); + var inst = this._instance; + + if (inst.componentWillUnmount) { + var previouslyUnmounting = ReactLifeCycle.currentlyUnmountingInstance; + ReactLifeCycle.currentlyUnmountingInstance = this; + try { + inst.componentWillUnmount(); + } finally { + ReactLifeCycle.currentlyUnmountingInstance = previouslyUnmounting; + } } - this._compositeLifeCycleState = null; - this._renderedComponent.unmountComponent(); + ReactReconciler.unmountComponent(this._renderedComponent); this._renderedComponent = null; - ReactComponent.Mixin.unmountComponent.call(this); + // Reset pending fields + this._pendingStateQueue = null; + this._pendingReplaceState = false; + this._pendingForceUpdate = false; + this._pendingCallbacks = null; + this._pendingElement = null; + + // These fields do not really need to be reset since this object is no + // longer accessible. + this._context = null; + this._rootNodeID = null; + + // Delete the reference from the instance to this internal representation + // which allow the internals to be properly cleaned up even if the user + // leaks a reference to the public instance. + ReactInstanceMap.remove(inst); - // Some existing components rely on this.props even after they've been + // Some existing components rely on inst.props even after they've been // destroyed (in event handlers). - // TODO: this.props = null; - // TODO: this.state = null; + // TODO: inst.props = null; + // TODO: inst.state = null; + // TODO: inst.context = null; }, /** - * Sets a subset of the state. Always use this or `replaceState` to mutate - * state. You should treat `this.state` as immutable. + * Schedule a partial update to the props. Only used for internal testing. * - * There is no guarantee that `this.state` will be immediately updated, so - * accessing `this.state` after calling this method may return the old value. - * - * There is no guarantee that calls to `setState` will run synchronously, - * as they may eventually be batched together. You can provide an optional - * callback that will be executed when the call to setState is actually - * completed. - * - * @param {object} partialState Next partial state to be merged with state. - * @param {?function} callback Called after state is updated. + * @param {object} partialProps Subset of the next props. + * @param {?function} callback Called after props are updated. * @final - * @protected + * @internal */ - setState: function(partialState, callback) { - ("production" !== process.env.NODE_ENV ? invariant( - typeof partialState === 'object' || partialState == null, - 'setState(...): takes an object of state variables to update.' - ) : invariant(typeof partialState === 'object' || partialState == null)); - if ("production" !== process.env.NODE_ENV){ - ("production" !== process.env.NODE_ENV ? warning( - partialState != null, - 'setState(...): You passed an undefined or null state object; ' + - 'instead, use forceUpdate().' - ) : null); - } - // Merge with `_pendingState` if it exists, otherwise with existing state. - this.replaceState( - assign({}, this._pendingState || this.state, partialState), - callback + _setPropsInternal: function(partialProps, callback) { + // This is a deoptimized path. We optimize for always having an element. + // This creates an extra internal element. + var element = this._pendingElement || this._currentElement; + this._pendingElement = ReactElement.cloneAndReplaceProps( + element, + assign({}, element.props, partialProps) ); + ReactUpdates.enqueueUpdate(this, callback); }, /** - * Replaces all of the state. Always use this or `setState` to mutate state. - * You should treat `this.state` as immutable. - * - * There is no guarantee that `this.state` will be immediately updated, so - * accessing `this.state` after calling this method may return the old value. + * Filters the context object to only contain keys specified in + * `contextTypes` * - * @param {object} completeState Next state. - * @param {?function} callback Called after state is updated. - * @final - * @protected + * @param {object} context + * @return {?object} + * @private */ - replaceState: function(completeState, callback) { - validateLifeCycleOnReplaceState(this); - this._pendingState = completeState; - if (this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING) { - // If we're in a componentWillMount handler, don't enqueue a rerender - // because ReactUpdates assumes we're in a browser context (which is wrong - // for server rendering) and we're about to do a render anyway. - // TODO: The callback here is ignored when setState is called from - // componentWillMount. Either fix it or disallow doing so completely in - // favor of getInitialState. - ReactUpdates.enqueueUpdate(this, callback); + _maskContext: function(context) { + var maskedContext = null; + // This really should be getting the component class for the element, + // but we know that we're not going to need it for built-ins. + if (typeof this._currentElement.type === 'string') { + return emptyObject; + } + var contextTypes = this._currentElement.type.contextTypes; + if (!contextTypes) { + return emptyObject; } + maskedContext = {}; + for (var contextName in contextTypes) { + maskedContext[contextName] = context[contextName]; + } + return maskedContext; }, /** @@ -9802,16 +10306,14 @@ var ReactCompositeComponentMixin = { * @private */ _processContext: function(context) { - var maskedContext = null; - var contextTypes = this.constructor.contextTypes; - if (contextTypes) { - maskedContext = {}; - for (var contextName in contextTypes) { - maskedContext[contextName] = context[contextName]; - } - if ("production" !== process.env.NODE_ENV) { + var maskedContext = this._maskContext(context); + if ("production" !== process.env.NODE_ENV) { + var Component = ReactNativeComponent.getComponentClassForElement( + this._currentElement + ); + if (Component.contextTypes) { this._checkPropTypes( - contextTypes, + Component.contextTypes, maskedContext, ReactPropTypeLocations.context ); @@ -9826,29 +10328,29 @@ var ReactCompositeComponentMixin = { * @private */ _processChildContext: function(currentContext) { - var childContext = this.getChildContext && this.getChildContext(); - var displayName = this.constructor.displayName || 'ReactCompositeComponent'; + var inst = this._instance; + var childContext = inst.getChildContext && inst.getChildContext(); if (childContext) { ("production" !== process.env.NODE_ENV ? invariant( - typeof this.constructor.childContextTypes === 'object', + typeof inst.constructor.childContextTypes === 'object', '%s.getChildContext(): childContextTypes must be defined in order to ' + 'use getChildContext().', - displayName - ) : invariant(typeof this.constructor.childContextTypes === 'object')); + this.getName() || 'ReactCompositeComponent' + ) : invariant(typeof inst.constructor.childContextTypes === 'object')); if ("production" !== process.env.NODE_ENV) { this._checkPropTypes( - this.constructor.childContextTypes, + inst.constructor.childContextTypes, childContext, ReactPropTypeLocations.childContext ); } for (var name in childContext) { ("production" !== process.env.NODE_ENV ? invariant( - name in this.constructor.childContextTypes, + name in inst.constructor.childContextTypes, '%s.getChildContext(): key "%s" is not defined in childContextTypes.', - displayName, + this.getName() || 'ReactCompositeComponent', name - ) : invariant(name in this.constructor.childContextTypes)); + ) : invariant(name in inst.constructor.childContextTypes)); } return assign({}, currentContext, childContext); } @@ -9866,9 +10368,15 @@ var ReactCompositeComponentMixin = { */ _processProps: function(newProps) { if ("production" !== process.env.NODE_ENV) { - var propTypes = this.constructor.propTypes; - if (propTypes) { - this._checkPropTypes(propTypes, newProps, ReactPropTypeLocations.prop); + var Component = ReactNativeComponent.getComponentClassForElement( + this._currentElement + ); + if (Component.propTypes) { + this._checkPropTypes( + Component.propTypes, + newProps, + ReactPropTypeLocations.prop + ); } } return newProps; @@ -9885,101 +10393,236 @@ var ReactCompositeComponentMixin = { _checkPropTypes: function(propTypes, props, location) { // TODO: Stop validating prop types here and only use the element // validation. - var componentName = this.constructor.displayName; + var componentName = this.getName(); for (var propName in propTypes) { if (propTypes.hasOwnProperty(propName)) { - var error = - propTypes[propName](props, propName, componentName, location); + var error; + try { + // This is intentionally an invariant that gets caught. It's the same + // behavior as without this statement except with a better message. + ("production" !== process.env.NODE_ENV ? invariant( + typeof propTypes[propName] === 'function', + '%s: %s type `%s` is invalid; it must be a function, usually ' + + 'from React.PropTypes.', + componentName || 'React class', + ReactPropTypeLocationNames[location], + propName + ) : invariant(typeof propTypes[propName] === 'function')); + error = propTypes[propName](props, propName, componentName, location); + } catch (ex) { + error = ex; + } if (error instanceof Error) { // We may want to extend this logic for similar errors in - // renderComponent calls, so I'm abstracting it away into + // React.render calls, so I'm abstracting it away into // a function to minimize refactoring in the future var addendum = getDeclarationErrorAddendum(this); - ("production" !== process.env.NODE_ENV ? warning(false, error.message + addendum) : null); + + if (location === ReactPropTypeLocations.prop) { + // Preface gives us something to blacklist in warning module + ("production" !== process.env.NODE_ENV ? warning( + false, + 'Failed Composite propType: %s%s', + error.message, + addendum + ) : null); + } else { + ("production" !== process.env.NODE_ENV ? warning( + false, + 'Failed Context Types: %s%s', + error.message, + addendum + ) : null); + } } } } }, + receiveComponent: function(nextElement, transaction, nextContext) { + var prevElement = this._currentElement; + var prevContext = this._context; + + this._pendingElement = null; + + this.updateComponent( + transaction, + prevElement, + nextElement, + prevContext, + nextContext + ); + }, + /** - * If any of `_pendingElement`, `_pendingState`, or `_pendingForceUpdate` + * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate` * is set, update the component. * * @param {ReactReconcileTransaction} transaction * @internal */ performUpdateIfNecessary: function(transaction) { - var compositeLifeCycleState = this._compositeLifeCycleState; - // Do not trigger a state transition if we are in the middle of mounting or - // receiving props because both of those will already be doing this. - if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING || - compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) { - return; + if (this._pendingElement != null) { + ReactReconciler.receiveComponent( + this, + this._pendingElement || this._currentElement, + transaction, + this._context + ); } - if (this._pendingElement == null && - this._pendingState == null && - !this._pendingForceUpdate) { - return; + if (this._pendingStateQueue !== null || this._pendingForceUpdate) { + if ("production" !== process.env.NODE_ENV) { + ReactElementValidator.checkAndWarnForMutatedProps( + this._currentElement + ); + } + + this.updateComponent( + transaction, + this._currentElement, + this._currentElement, + this._context, + this._context + ); } + }, - var nextContext = this.context; - var nextProps = this.props; - var nextElement = this._currentElement; - if (this._pendingElement != null) { - nextElement = this._pendingElement; - nextContext = this._processContext(nextElement._context); - nextProps = this._processProps(nextElement.props); - this._pendingElement = null; + /** + * Compare two contexts, warning if they are different + * TODO: Remove this check when owner-context is removed + */ + _warnIfContextsDiffer: function(ownerBasedContext, parentBasedContext) { + ownerBasedContext = this._maskContext(ownerBasedContext); + parentBasedContext = this._maskContext(parentBasedContext); + var parentKeys = Object.keys(parentBasedContext).sort(); + var displayName = this.getName() || 'ReactCompositeComponent'; + for (var i = 0; i < parentKeys.length; i++) { + var key = parentKeys[i]; + ("production" !== process.env.NODE_ENV ? warning( + ownerBasedContext[key] === parentBasedContext[key], + 'owner-based and parent-based contexts differ ' + + '(values: `%s` vs `%s`) for key (%s) while mounting %s ' + + '(see: http://fb.me/react-context-by-parent)', + ownerBasedContext[key], + parentBasedContext[key], + key, + displayName + ) : null); + } + }, - this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS; - if (this.componentWillReceiveProps) { - this.componentWillReceiveProps(nextProps, nextContext); + /** + * Perform an update to a mounted component. The componentWillReceiveProps and + * shouldComponentUpdate methods are called, then (assuming the update isn't + * skipped) the remaining update lifecycle methods are called and the DOM + * representation is updated. + * + * By default, this implements React's rendering and reconciliation algorithm. + * Sophisticated clients may wish to override this. + * + * @param {ReactReconcileTransaction} transaction + * @param {ReactElement} prevParentElement + * @param {ReactElement} nextParentElement + * @internal + * @overridable + */ + updateComponent: function( + transaction, + prevParentElement, + nextParentElement, + prevUnmaskedContext, + nextUnmaskedContext + ) { + var inst = this._instance; + + var nextContext = inst.context; + var nextProps = inst.props; + + // Distinguish between a props update versus a simple state update + if (prevParentElement !== nextParentElement) { + nextContext = this._processContext(nextParentElement._context); + nextProps = this._processProps(nextParentElement.props); + + if ("production" !== process.env.NODE_ENV) { + if (nextUnmaskedContext != null) { + this._warnIfContextsDiffer( + nextParentElement._context, + nextUnmaskedContext + ); + } } - } - this._compositeLifeCycleState = null; + // An update here will schedule an update but immediately set + // _pendingStateQueue which will ensure that any state updates gets + // immediately reconciled instead of waiting for the next batch. + + if (inst.componentWillReceiveProps) { + inst.componentWillReceiveProps(nextProps, nextContext); + } + } - var nextState = this._pendingState || this.state; - this._pendingState = null; + var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = this._pendingForceUpdate || - !this.shouldComponentUpdate || - this.shouldComponentUpdate(nextProps, nextState, nextContext); + !inst.shouldComponentUpdate || + inst.shouldComponentUpdate(nextProps, nextState, nextContext); if ("production" !== process.env.NODE_ENV) { - if (typeof shouldUpdate === "undefined") { - console.warn( - (this.constructor.displayName || 'ReactCompositeComponent') + - '.shouldComponentUpdate(): Returned undefined instead of a ' + - 'boolean value. Make sure to return true or false.' - ); - } + ("production" !== process.env.NODE_ENV ? warning( + typeof shouldUpdate !== 'undefined', + '%s.shouldComponentUpdate(): Returned undefined instead of a ' + + 'boolean value. Make sure to return true or false.', + this.getName() || 'ReactCompositeComponent' + ) : null); } if (shouldUpdate) { this._pendingForceUpdate = false; // Will set `this.props`, `this.state` and `this.context`. this._performComponentUpdate( - nextElement, + nextParentElement, nextProps, nextState, nextContext, - transaction + transaction, + nextUnmaskedContext ); } else { // If it's determined that a component should not update, we still want - // to set props and state. - this._currentElement = nextElement; - this.props = nextProps; - this.state = nextState; - this.context = nextContext; + // to set props and state but we shortcut the rest of the update. + this._currentElement = nextParentElement; + this._context = nextUnmaskedContext; + inst.props = nextProps; + inst.state = nextState; + inst.context = nextContext; + } + }, + + _processPendingState: function(props, context) { + var inst = this._instance; + var queue = this._pendingStateQueue; + var replace = this._pendingReplaceState; + this._pendingReplaceState = false; + this._pendingStateQueue = null; - // Owner cannot change because shouldUpdateReactComponent doesn't allow - // it. TODO: Remove this._owner completely. - this._owner = nextElement._owner; + if (!queue) { + return inst.state; } + + var nextState = assign({}, replace ? queue[0] : inst.state); + for (var i = replace ? 1 : 0; i < queue.length; i++) { + var partial = queue[i]; + assign( + nextState, + typeof partial === 'function' ? + partial.call(inst, nextState, props, context) : + partial + ); + } + + return nextState; }, /** @@ -9991,6 +10634,7 @@ var ReactCompositeComponentMixin = { * @param {?object} nextState Next object to set as state. * @param {?object} nextContext Next public object to set as context. * @param {ReactReconcileTransaction} transaction + * @param {?object} unmaskedContext * @private */ _performComponentUpdate: function( @@ -9998,337 +10642,213 @@ var ReactCompositeComponentMixin = { nextProps, nextState, nextContext, - transaction + transaction, + unmaskedContext ) { - var prevElement = this._currentElement; - var prevProps = this.props; - var prevState = this.state; - var prevContext = this.context; + var inst = this._instance; + + var prevProps = inst.props; + var prevState = inst.state; + var prevContext = inst.context; - if (this.componentWillUpdate) { - this.componentWillUpdate(nextProps, nextState, nextContext); + if (inst.componentWillUpdate) { + inst.componentWillUpdate(nextProps, nextState, nextContext); } this._currentElement = nextElement; - this.props = nextProps; - this.state = nextState; - this.context = nextContext; + this._context = unmaskedContext; + inst.props = nextProps; + inst.state = nextState; + inst.context = nextContext; - // Owner cannot change because shouldUpdateReactComponent doesn't allow - // it. TODO: Remove this._owner completely. - this._owner = nextElement._owner; + this._updateRenderedComponent(transaction, unmaskedContext); - this.updateComponent( - transaction, - prevElement - ); - - if (this.componentDidUpdate) { + if (inst.componentDidUpdate) { transaction.getReactMountReady().enqueue( - this.componentDidUpdate.bind(this, prevProps, prevState, prevContext), - this + inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), + inst ); } }, - receiveComponent: function(nextElement, transaction) { - if (nextElement === this._currentElement && - nextElement._owner != null) { - // Since elements are immutable after the owner is rendered, - // we can do a cheap identity compare here to determine if this is a - // superfluous reconcile. It's possible for state to be mutable but such - // change should trigger an update of the owner which would recreate - // the element. We explicitly check for the existence of an owner since - // it's possible for a element created outside a composite to be - // deeply mutated and reused. - return; - } - - ReactComponent.Mixin.receiveComponent.call( - this, - nextElement, - transaction - ); - }, - /** - * Updates the component's currently mounted DOM representation. - * - * By default, this implements React's rendering and reconciliation algorithm. - * Sophisticated clients may wish to override this. + * Call the component's `render` method and update the DOM accordingly. * * @param {ReactReconcileTransaction} transaction - * @param {ReactElement} prevElement * @internal - * @overridable */ - updateComponent: ReactPerf.measure( - 'ReactCompositeComponent', - 'updateComponent', - function(transaction, prevParentElement) { - ReactComponent.Mixin.updateComponent.call( - this, + _updateRenderedComponent: function(transaction, context) { + var prevComponentInstance = this._renderedComponent; + var prevRenderedElement = prevComponentInstance._currentElement; + var nextRenderedElement = this._renderValidatedComponent(); + if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) { + ReactReconciler.receiveComponent( + prevComponentInstance, + nextRenderedElement, transaction, - prevParentElement + this._processChildContext(context) ); - - var prevComponentInstance = this._renderedComponent; - var prevElement = prevComponentInstance._currentElement; - var nextElement = this._renderValidatedComponent(); - if (shouldUpdateReactComponent(prevElement, nextElement)) { - prevComponentInstance.receiveComponent(nextElement, transaction); - } else { - // These two IDs are actually the same! But nothing should rely on that. - var thisID = this._rootNodeID; - var prevComponentID = prevComponentInstance._rootNodeID; - prevComponentInstance.unmountComponent(); - this._renderedComponent = instantiateReactComponent( - nextElement, - this._currentElement.type - ); - var nextMarkup = this._renderedComponent.mountComponent( - thisID, - transaction, - this._mountDepth + 1 - ); - ReactComponent.BackendIDOperations.dangerouslyReplaceNodeWithMarkupByID( - prevComponentID, - nextMarkup - ); - } + } else { + // These two IDs are actually the same! But nothing should rely on that. + var thisID = this._rootNodeID; + var prevComponentID = prevComponentInstance._rootNodeID; + ReactReconciler.unmountComponent(prevComponentInstance); + + this._renderedComponent = this._instantiateReactComponent( + nextRenderedElement, + this._currentElement.type + ); + var nextMarkup = ReactReconciler.mountComponent( + this._renderedComponent, + thisID, + transaction, + context + ); + this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup); } - ), + }, /** - * Forces an update. This should only be invoked when it is known with - * certainty that we are **not** in a DOM transaction. - * - * You may want to call this when you know that some deeper aspect of the - * component's state has changed but `setState` was not called. - * - * This will not invoke `shouldUpdateComponent`, but it will invoke - * `componentWillUpdate` and `componentDidUpdate`. - * - * @param {?function} callback Called after update is complete. - * @final * @protected */ - forceUpdate: function(callback) { - var compositeLifeCycleState = this._compositeLifeCycleState; - ("production" !== process.env.NODE_ENV ? invariant( - this.isMounted() || - compositeLifeCycleState === CompositeLifeCycle.MOUNTING, - 'forceUpdate(...): Can only force an update on mounted or mounting ' + - 'components.' - ) : invariant(this.isMounted() || - compositeLifeCycleState === CompositeLifeCycle.MOUNTING)); - ("production" !== process.env.NODE_ENV ? invariant( - compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING && - ReactCurrentOwner.current == null, - 'forceUpdate(...): Cannot force an update while unmounting component ' + - 'or within a `render` function.' - ) : invariant(compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING && - ReactCurrentOwner.current == null)); - this._pendingForceUpdate = true; - ReactUpdates.enqueueUpdate(this, callback); + _replaceNodeWithMarkupByID: function(prevComponentID, nextMarkup) { + ReactComponentEnvironment.replaceNodeWithMarkupByID( + prevComponentID, + nextMarkup + ); }, /** - * @private + * @protected */ - _renderValidatedComponent: ReactPerf.measure( - 'ReactCompositeComponent', - '_renderValidatedComponent', - function() { - var renderedComponent; - var previousContext = ReactContext.current; - ReactContext.current = this._processChildContext( - this._currentElement._context - ); - ReactCurrentOwner.current = this; - try { - renderedComponent = this.render(); - if (renderedComponent === null || renderedComponent === false) { - renderedComponent = ReactEmptyComponent.getEmptyComponent(); - ReactEmptyComponent.registerNullComponentID(this._rootNodeID); - } else { - ReactEmptyComponent.deregisterNullComponentID(this._rootNodeID); - } - } finally { - ReactContext.current = previousContext; - ReactCurrentOwner.current = null; + _renderValidatedComponentWithoutOwnerOrContext: function() { + var inst = this._instance; + var renderedComponent = inst.render(); + if ("production" !== process.env.NODE_ENV) { + // We allow auto-mocks to proceed as if they're returning null. + if (typeof renderedComponent === 'undefined' && + inst.render._isMockFunction) { + // This is probably bad practice. Consider warning here and + // deprecating this convenience. + renderedComponent = null; } - ("production" !== process.env.NODE_ENV ? invariant( - ReactElement.isValidElement(renderedComponent), - '%s.render(): A valid ReactComponent must be returned. You may have ' + - 'returned undefined, an array or some other invalid object.', - this.constructor.displayName || 'ReactCompositeComponent' - ) : invariant(ReactElement.isValidElement(renderedComponent))); - return renderedComponent; } - ), + + return renderedComponent; + }, /** * @private */ - _bindAutoBindMethods: function() { - for (var autoBindKey in this.__reactAutoBindMap) { - if (!this.__reactAutoBindMap.hasOwnProperty(autoBindKey)) { - continue; - } - var method = this.__reactAutoBindMap[autoBindKey]; - this[autoBindKey] = this._bindAutoBindMethod(ReactErrorUtils.guard( - method, - this.constructor.displayName + '.' + autoBindKey - )); + _renderValidatedComponent: function() { + var renderedComponent; + var previousContext = ReactContext.current; + ReactContext.current = this._processChildContext( + this._currentElement._context + ); + ReactCurrentOwner.current = this; + try { + renderedComponent = + this._renderValidatedComponentWithoutOwnerOrContext(); + } finally { + ReactContext.current = previousContext; + ReactCurrentOwner.current = null; } + ("production" !== process.env.NODE_ENV ? invariant( + // TODO: An `isValidNode` function would probably be more appropriate + renderedComponent === null || renderedComponent === false || + ReactElement.isValidElement(renderedComponent), + '%s.render(): A valid ReactComponent must be returned. You may have ' + + 'returned undefined, an array or some other invalid object.', + this.getName() || 'ReactCompositeComponent' + ) : invariant(// TODO: An `isValidNode` function would probably be more appropriate + renderedComponent === null || renderedComponent === false || + ReactElement.isValidElement(renderedComponent))); + return renderedComponent; }, /** - * Binds a method to the component. + * Lazily allocates the refs object and stores `component` as `ref`. * - * @param {function} method Method to be bound. + * @param {string} ref Reference name. + * @param {component} component Component to store as `ref`. + * @final * @private */ - _bindAutoBindMethod: function(method) { - var component = this; - var boundMethod = method.bind(component); - if ("production" !== process.env.NODE_ENV) { - boundMethod.__reactBoundContext = component; - boundMethod.__reactBoundMethod = method; - boundMethod.__reactBoundArguments = null; - var componentName = component.constructor.displayName; - var _bind = boundMethod.bind; - boundMethod.bind = function(newThis ) {var args=Array.prototype.slice.call(arguments,1); - // User is trying to bind() an autobound method; we effectively will - // ignore the value of "this" that the user is trying to use, so - // let's warn. - if (newThis !== component && newThis !== null) { - monitorCodeUse('react_bind_warning', { component: componentName }); - console.warn( - 'bind(): React component methods may only be bound to the ' + - 'component instance. See ' + componentName - ); - } else if (!args.length) { - monitorCodeUse('react_bind_warning', { component: componentName }); - console.warn( - 'bind(): You are binding a component method to the component. ' + - 'React does this for you automatically in a high-performance ' + - 'way, so you can safely remove this call. See ' + componentName - ); - return boundMethod; - } - var reboundMethod = _bind.apply(boundMethod, arguments); - reboundMethod.__reactBoundContext = component; - reboundMethod.__reactBoundMethod = method; - reboundMethod.__reactBoundArguments = args; - return reboundMethod; - }; - } - return boundMethod; - } -}; - -var ReactCompositeComponentBase = function() {}; -assign( - ReactCompositeComponentBase.prototype, - ReactComponent.Mixin, - ReactOwner.Mixin, - ReactPropTransferer.Mixin, - ReactCompositeComponentMixin -); - -/** - * Module for creating composite components. - * - * @class ReactCompositeComponent - * @extends ReactComponent - * @extends ReactOwner - * @extends ReactPropTransferer - */ -var ReactCompositeComponent = { - - LifeCycle: CompositeLifeCycle, - - Base: ReactCompositeComponentBase, + attachRef: function(ref, component) { + var inst = this.getPublicInstance(); + var refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs; + refs[ref] = component.getPublicInstance(); + }, /** - * Creates a composite component class given a class specification. + * Detaches a reference name. * - * @param {object} spec Class specification (which must define `render`). - * @return {function} Component constructor function. - * @public + * @param {string} ref Name to dereference. + * @final + * @private */ - createClass: function(spec) { - var Constructor = function(props) { - // This constructor is overridden by mocks. The argument is used - // by mocks to assert on what gets mounted. This will later be used - // by the stand-alone class implementation. - }; - Constructor.prototype = new ReactCompositeComponentBase(); - Constructor.prototype.constructor = Constructor; + detachRef: function(ref) { + var refs = this.getPublicInstance().refs; + delete refs[ref]; + }, - injectedMixins.forEach( - mixSpecIntoComponent.bind(null, Constructor) + /** + * Get a text description of the component that can be used to identify it + * in error messages. + * @return {string} The name or null. + * @internal + */ + getName: function() { + var type = this._currentElement.type; + var constructor = this._instance && this._instance.constructor; + return ( + type.displayName || (constructor && constructor.displayName) || + type.name || (constructor && constructor.name) || + null ); + }, - mixSpecIntoComponent(Constructor, spec); + /** + * Get the publicly accessible representation of this component - i.e. what + * is exposed by refs and returned by React.render. Can be null for stateless + * components. + * + * @return {ReactComponent} the public component instance. + * @internal + */ + getPublicInstance: function() { + return this._instance; + }, - // Initialize the defaultProps property after all mixins have been merged - if (Constructor.getDefaultProps) { - Constructor.defaultProps = Constructor.getDefaultProps(); - } + // Stub + _instantiateReactComponent: null - ("production" !== process.env.NODE_ENV ? invariant( - Constructor.prototype.render, - 'createClass(...): Class specification must implement a `render` method.' - ) : invariant(Constructor.prototype.render)); +}; - if ("production" !== process.env.NODE_ENV) { - if (Constructor.prototype.componentShouldUpdate) { - monitorCodeUse( - 'react_component_should_update_warning', - { component: spec.displayName } - ); - console.warn( - (spec.displayName || 'A component') + ' has a method called ' + - 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + - 'The name is phrased as a question because the function is ' + - 'expected to return a value.' - ); - } - } +ReactPerf.measureMethods( + ReactCompositeComponentMixin, + 'ReactCompositeComponent', + { + mountComponent: 'mountComponent', + updateComponent: 'updateComponent', + _renderValidatedComponent: '_renderValidatedComponent' + } +); - // Reduce time spent doing lookups by setting these on the prototype. - for (var methodName in ReactCompositeComponentInterface) { - if (!Constructor.prototype[methodName]) { - Constructor.prototype[methodName] = null; - } - } +var ReactCompositeComponent = { - if ("production" !== process.env.NODE_ENV) { - return ReactLegacyElement.wrapFactory( - ReactElementValidator.createFactory(Constructor) - ); - } - return ReactLegacyElement.wrapFactory( - ReactElement.createFactory(Constructor) - ); - }, + Mixin: ReactCompositeComponentMixin - injection: { - injectMixin: function(mixin) { - injectedMixins.push(mixin); - } - } }; module.exports = ReactCompositeComponent; }).call(this,require('_process')) -},{"./Object.assign":70,"./ReactComponent":78,"./ReactContext":82,"./ReactCurrentOwner":83,"./ReactElement":99,"./ReactElementValidator":100,"./ReactEmptyComponent":101,"./ReactErrorUtils":102,"./ReactLegacyElement":108,"./ReactOwner":115,"./ReactPerf":116,"./ReactPropTransferer":117,"./ReactPropTypeLocationNames":118,"./ReactPropTypeLocations":119,"./ReactUpdates":132,"./instantiateReactComponent":181,"./invariant":182,"./keyMirror":188,"./keyOf":189,"./mapObject":190,"./monitorCodeUse":192,"./shouldUpdateReactComponent":198,"./warning":202,"_process":1}],82:[function(require,module,exports){ +},{"./Object.assign":69,"./ReactComponentEnvironment":81,"./ReactContext":84,"./ReactCurrentOwner":85,"./ReactElement":103,"./ReactElementValidator":104,"./ReactInstanceMap":113,"./ReactLifeCycle":114,"./ReactNativeComponent":120,"./ReactPerf":122,"./ReactPropTypeLocationNames":124,"./ReactPropTypeLocations":125,"./ReactReconciler":129,"./ReactUpdates":140,"./emptyObject":171,"./invariant":191,"./shouldUpdateReactComponent":208,"./warning":212,"_process":1}],84:[function(require,module,exports){ +(function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -10338,9 +10858,13 @@ module.exports = ReactCompositeComponent; * @providesModule ReactContext */ -"use strict"; +'use strict'; var assign = require("./Object.assign"); +var emptyObject = require("./emptyObject"); +var warning = require("./warning"); + +var didWarn = false; /** * Keeps track of the current context. @@ -10354,7 +10878,7 @@ var ReactContext = { * @internal * @type {object} */ - current: {}, + current: emptyObject, /** * Temporarily extends the current context while executing scopedCallback. @@ -10373,6 +10897,16 @@ var ReactContext = { * @return {ReactComponent|array<ReactComponent>} */ withContext: function(newContext, scopedCallback) { + if ("production" !== process.env.NODE_ENV) { + ("production" !== process.env.NODE_ENV ? warning( + didWarn, + 'withContext is deprecated and will be removed in a future version. ' + + 'Use a wrapper component with getChildContext instead.' + ) : null); + + didWarn = true; + } + var result; var previousContext = ReactContext.current; ReactContext.current = assign({}, previousContext, newContext); @@ -10388,9 +10922,10 @@ var ReactContext = { module.exports = ReactContext; -},{"./Object.assign":70}],83:[function(require,module,exports){ +}).call(this,require('_process')) +},{"./Object.assign":69,"./emptyObject":171,"./warning":212,"_process":1}],85:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -10400,7 +10935,7 @@ module.exports = ReactContext; * @providesModule ReactCurrentOwner */ -"use strict"; +'use strict'; /** * Keeps track of the current owner. @@ -10422,10 +10957,10 @@ var ReactCurrentOwner = { module.exports = ReactCurrentOwner; -},{}],84:[function(require,module,exports){ +},{}],86:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -10436,11 +10971,10 @@ module.exports = ReactCurrentOwner; * @typechecks static-only */ -"use strict"; +'use strict'; var ReactElement = require("./ReactElement"); var ReactElementValidator = require("./ReactElementValidator"); -var ReactLegacyElement = require("./ReactLegacyElement"); var mapObject = require("./mapObject"); @@ -10452,13 +10986,9 @@ var mapObject = require("./mapObject"); */ function createDOMFactory(tag) { if ("production" !== process.env.NODE_ENV) { - return ReactLegacyElement.markNonLegacyFactory( - ReactElementValidator.createFactory(tag) - ); + return ReactElementValidator.createFactory(tag); } - return ReactLegacyElement.markNonLegacyFactory( - ReactElement.createFactory(tag) - ); + return ReactElement.createFactory(tag); } /** @@ -10605,9 +11135,9 @@ var ReactDOM = mapObject({ module.exports = ReactDOM; }).call(this,require('_process')) -},{"./ReactElement":99,"./ReactElementValidator":100,"./ReactLegacyElement":108,"./mapObject":190,"_process":1}],85:[function(require,module,exports){ +},{"./ReactElement":103,"./ReactElementValidator":104,"./mapObject":199,"_process":1}],87:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -10617,18 +11147,16 @@ module.exports = ReactDOM; * @providesModule ReactDOMButton */ -"use strict"; +'use strict'; var AutoFocusMixin = require("./AutoFocusMixin"); var ReactBrowserComponentMixin = require("./ReactBrowserComponentMixin"); -var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactClass = require("./ReactClass"); var ReactElement = require("./ReactElement"); -var ReactDOM = require("./ReactDOM"); var keyMirror = require("./keyMirror"); -// Store a reference to the <button> `ReactDOMComponent`. TODO: use string -var button = ReactElement.createFactory(ReactDOM.button.type); +var button = ReactElement.createFactory('button'); var mouseListenerNames = keyMirror({ onClick: true, @@ -10647,8 +11175,9 @@ var mouseListenerNames = keyMirror({ * Implements a <button> native component that does not receive mouse events * when `disabled` is set. */ -var ReactDOMButton = ReactCompositeComponent.createClass({ +var ReactDOMButton = ReactClass.createClass({ displayName: 'ReactDOMButton', + tagName: 'BUTTON', mixins: [AutoFocusMixin, ReactBrowserComponentMixin], @@ -10670,10 +11199,10 @@ var ReactDOMButton = ReactCompositeComponent.createClass({ module.exports = ReactDOMButton; -},{"./AutoFocusMixin":43,"./ReactBrowserComponentMixin":73,"./ReactCompositeComponent":81,"./ReactDOM":84,"./ReactElement":99,"./keyMirror":188}],86:[function(require,module,exports){ +},{"./AutoFocusMixin":42,"./ReactBrowserComponentMixin":72,"./ReactClass":78,"./ReactElement":103,"./keyMirror":197}],88:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -10684,24 +11213,26 @@ module.exports = ReactDOMButton; * @typechecks static-only */ -"use strict"; +/* global hasOwnProperty:true */ + +'use strict'; var CSSPropertyOperations = require("./CSSPropertyOperations"); var DOMProperty = require("./DOMProperty"); var DOMPropertyOperations = require("./DOMPropertyOperations"); -var ReactBrowserComponentMixin = require("./ReactBrowserComponentMixin"); -var ReactComponent = require("./ReactComponent"); var ReactBrowserEventEmitter = require("./ReactBrowserEventEmitter"); +var ReactComponentBrowserEnvironment = + require("./ReactComponentBrowserEnvironment"); var ReactMount = require("./ReactMount"); var ReactMultiChild = require("./ReactMultiChild"); var ReactPerf = require("./ReactPerf"); var assign = require("./Object.assign"); -var escapeTextForBrowser = require("./escapeTextForBrowser"); +var escapeTextContentForBrowser = require("./escapeTextContentForBrowser"); var invariant = require("./invariant"); var isEventSupported = require("./isEventSupported"); var keyOf = require("./keyOf"); -var monitorCodeUse = require("./monitorCodeUse"); +var warning = require("./warning"); var deleteListener = ReactBrowserEventEmitter.deleteListener; var listenTo = ReactBrowserEventEmitter.listenTo; @@ -10715,6 +11246,11 @@ var STYLE = keyOf({style: null}); var ELEMENT_NODE_TYPE = 1; /** + * Optionally injectable operations for mutating the DOM + */ +var BackendIDOperations = null; + +/** * @param {?object} props */ function assertValidProps(props) { @@ -10722,24 +11258,37 @@ function assertValidProps(props) { return; } // Note the use of `==` which checks for null or undefined. - ("production" !== process.env.NODE_ENV ? invariant( - props.children == null || props.dangerouslySetInnerHTML == null, - 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.' - ) : invariant(props.children == null || props.dangerouslySetInnerHTML == null)); + if (props.dangerouslySetInnerHTML != null) { + ("production" !== process.env.NODE_ENV ? invariant( + props.children == null, + 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.' + ) : invariant(props.children == null)); + ("production" !== process.env.NODE_ENV ? invariant( + props.dangerouslySetInnerHTML.__html != null, + '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + + 'Please visit http://fb.me/react-invariant-dangerously-set-inner-html ' + + 'for more information.' + ) : invariant(props.dangerouslySetInnerHTML.__html != null)); + } if ("production" !== process.env.NODE_ENV) { - if (props.contentEditable && props.children != null) { - console.warn( - 'A component is `contentEditable` and contains `children` managed by ' + - 'React. It is now your responsibility to guarantee that none of those '+ - 'nodes are unexpectedly modified or duplicated. This is probably not ' + - 'intentional.' - ); - } + ("production" !== process.env.NODE_ENV ? warning( + props.innerHTML == null, + 'Directly setting property `innerHTML` is not permitted. ' + + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.' + ) : null); + ("production" !== process.env.NODE_ENV ? warning( + !props.contentEditable || props.children == null, + 'A component is `contentEditable` and contains `children` managed by ' + + 'React. It is now your responsibility to guarantee that none of ' + + 'those nodes are unexpectedly modified or duplicated. This is ' + + 'probably not intentional.' + ) : null); } ("production" !== process.env.NODE_ENV ? invariant( props.style == null || typeof props.style === 'object', 'The `style` prop expects a mapping from style properties to values, ' + - 'not a string.' + 'not a string. For example, style={{marginRight: spacing + \'em\'}} when ' + + 'using JSX.' ) : invariant(props.style == null || typeof props.style === 'object')); } @@ -10747,11 +11296,10 @@ function putListener(id, registrationName, listener, transaction) { if ("production" !== process.env.NODE_ENV) { // IE8 has no API for event capturing and the `onScroll` event doesn't // bubble. - if (registrationName === 'onScroll' && - !isEventSupported('scroll', true)) { - monitorCodeUse('react_no_scroll_event'); - console.warn('This browser doesn\'t support the `onScroll` event'); - } + ("production" !== process.env.NODE_ENV ? warning( + registrationName !== 'onScroll' || isEventSupported('scroll', true), + 'This browser doesn\'t support the `onScroll` event' + ) : null); } var container = ReactMount.findReactContainerForID(id); if (container) { @@ -10816,19 +11364,24 @@ function validateDangerousTag(tag) { * object mapping of style properties to values. * * @constructor ReactDOMComponent - * @extends ReactComponent * @extends ReactMultiChild */ function ReactDOMComponent(tag) { validateDangerousTag(tag); this._tag = tag; - this.tagName = tag.toUpperCase(); + this._renderedChildren = null; + this._previousStyleCopy = null; + this._rootNodeID = null; } ReactDOMComponent.displayName = 'ReactDOMComponent'; ReactDOMComponent.Mixin = { + construct: function(element) { + this._currentElement = element; + }, + /** * Generates root tag markup then recurses. This method has side effects and * is not idempotent. @@ -10836,28 +11389,18 @@ ReactDOMComponent.Mixin = { * @internal * @param {string} rootID The root DOM ID for this node. * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction - * @param {number} mountDepth number of components in the owner hierarchy * @return {string} The computed markup. */ - mountComponent: ReactPerf.measure( - 'ReactDOMComponent', - 'mountComponent', - function(rootID, transaction, mountDepth) { - ReactComponent.Mixin.mountComponent.call( - this, - rootID, - transaction, - mountDepth - ); - assertValidProps(this.props); - var closeTag = omittedCloseTags[this._tag] ? '' : '</' + this._tag + '>'; - return ( - this._createOpenTagMarkupAndPutListeners(transaction) + - this._createContentMarkup(transaction) + - closeTag - ); - } - ), + mountComponent: function(rootID, transaction, context) { + this._rootNodeID = rootID; + assertValidProps(this._currentElement.props); + var closeTag = omittedCloseTags[this._tag] ? '' : '</' + this._tag + '>'; + return ( + this._createOpenTagMarkupAndPutListeners(transaction) + + this._createContentMarkup(transaction, context) + + closeTag + ); + }, /** * Creates markup for the open tag and all attributes. @@ -10872,7 +11415,7 @@ ReactDOMComponent.Mixin = { * @return {string} Markup of opening tag. */ _createOpenTagMarkupAndPutListeners: function(transaction) { - var props = this.props; + var props = this._currentElement.props; var ret = '<' + this._tag; for (var propKey in props) { @@ -10888,7 +11431,7 @@ ReactDOMComponent.Mixin = { } else { if (propKey === STYLE) { if (propValue) { - propValue = props.style = assign({}, props.style); + propValue = this._previousStyleCopy = assign({}, props.style); } propValue = CSSPropertyOperations.createMarkupForStyles(propValue); } @@ -10915,50 +11458,50 @@ ReactDOMComponent.Mixin = { * * @private * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction + * @param {object} context * @return {string} Content markup. */ - _createContentMarkup: function(transaction) { + _createContentMarkup: function(transaction, context) { + var prefix = ''; + if (this._tag === 'listing' || + this._tag === 'pre' || + this._tag === 'textarea') { + // Add an initial newline because browsers ignore the first newline in + // a <listing>, <pre>, or <textarea> as an "authoring convenience" -- see + // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody. + prefix = '\n'; + } + + var props = this._currentElement.props; + // Intentional use of != to avoid catching zero/false. - var innerHTML = this.props.dangerouslySetInnerHTML; + var innerHTML = props.dangerouslySetInnerHTML; if (innerHTML != null) { if (innerHTML.__html != null) { - return innerHTML.__html; + return prefix + innerHTML.__html; } } else { var contentToUse = - CONTENT_TYPES[typeof this.props.children] ? this.props.children : null; - var childrenToUse = contentToUse != null ? null : this.props.children; + CONTENT_TYPES[typeof props.children] ? props.children : null; + var childrenToUse = contentToUse != null ? null : props.children; if (contentToUse != null) { - return escapeTextForBrowser(contentToUse); + return prefix + escapeTextContentForBrowser(contentToUse); } else if (childrenToUse != null) { var mountImages = this.mountChildren( childrenToUse, - transaction + transaction, + context ); - return mountImages.join(''); + return prefix + mountImages.join(''); } } - return ''; + return prefix; }, - receiveComponent: function(nextElement, transaction) { - if (nextElement === this._currentElement && - nextElement._owner != null) { - // Since elements are immutable after the owner is rendered, - // we can do a cheap identity compare here to determine if this is a - // superfluous reconcile. It's possible for state to be mutable but such - // change should trigger an update of the owner which would recreate - // the element. We explicitly check for the existence of an owner since - // it's possible for a element created outside a composite to be - // deeply mutated and reused. - return; - } - - ReactComponent.Mixin.receiveComponent.call( - this, - nextElement, - transaction - ); + receiveComponent: function(nextElement, transaction, context) { + var prevElement = this._currentElement; + this._currentElement = nextElement; + this.updateComponent(transaction, prevElement, nextElement, context); }, /** @@ -10967,23 +11510,15 @@ ReactDOMComponent.Mixin = { * * @param {ReactReconcileTransaction} transaction * @param {ReactElement} prevElement + * @param {ReactElement} nextElement * @internal * @overridable */ - updateComponent: ReactPerf.measure( - 'ReactDOMComponent', - 'updateComponent', - function(transaction, prevElement) { - assertValidProps(this._currentElement.props); - ReactComponent.Mixin.updateComponent.call( - this, - transaction, - prevElement - ); - this._updateDOMProperties(prevElement.props, transaction); - this._updateDOMChildren(prevElement.props, transaction); - } - ), + updateComponent: function(transaction, prevElement, nextElement, context) { + assertValidProps(this._currentElement.props); + this._updateDOMProperties(prevElement.props, transaction); + this._updateDOMChildren(prevElement.props, transaction, context); + }, /** * Reconciles the properties by detecting differences in property values and @@ -11001,7 +11536,7 @@ ReactDOMComponent.Mixin = { * @param {ReactReconcileTransaction} transaction */ _updateDOMProperties: function(lastProps, transaction) { - var nextProps = this.props; + var nextProps = this._currentElement.props; var propKey; var styleName; var styleUpdates; @@ -11011,19 +11546,20 @@ ReactDOMComponent.Mixin = { continue; } if (propKey === STYLE) { - var lastStyle = lastProps[propKey]; + var lastStyle = this._previousStyleCopy; for (styleName in lastStyle) { if (lastStyle.hasOwnProperty(styleName)) { styleUpdates = styleUpdates || {}; styleUpdates[styleName] = ''; } } + this._previousStyleCopy = null; } else if (registrationNameModules.hasOwnProperty(propKey)) { deleteListener(this._rootNodeID, propKey); } else if ( DOMProperty.isStandardName[propKey] || DOMProperty.isCustomAttribute(propKey)) { - ReactComponent.BackendIDOperations.deletePropertyByID( + BackendIDOperations.deletePropertyByID( this._rootNodeID, propKey ); @@ -11031,13 +11567,15 @@ ReactDOMComponent.Mixin = { } for (propKey in nextProps) { var nextProp = nextProps[propKey]; - var lastProp = lastProps[propKey]; + var lastProp = propKey === STYLE ? + this._previousStyleCopy : + lastProps[propKey]; if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) { continue; } if (propKey === STYLE) { if (nextProp) { - nextProp = nextProps.style = assign({}, nextProp); + nextProp = this._previousStyleCopy = assign({}, nextProp); } if (lastProp) { // Unset styles on `lastProp` but not on `nextProp`. @@ -11065,7 +11603,7 @@ ReactDOMComponent.Mixin = { } else if ( DOMProperty.isStandardName[propKey] || DOMProperty.isCustomAttribute(propKey)) { - ReactComponent.BackendIDOperations.updatePropertyByID( + BackendIDOperations.updatePropertyByID( this._rootNodeID, propKey, nextProp @@ -11073,7 +11611,7 @@ ReactDOMComponent.Mixin = { } } if (styleUpdates) { - ReactComponent.BackendIDOperations.updateStylesByID( + BackendIDOperations.updateStylesByID( this._rootNodeID, styleUpdates ); @@ -11087,8 +11625,8 @@ ReactDOMComponent.Mixin = { * @param {object} lastProps * @param {ReactReconcileTransaction} transaction */ - _updateDOMChildren: function(lastProps, transaction) { - var nextProps = this.props; + _updateDOMChildren: function(lastProps, transaction, context) { + var nextProps = this._currentElement.props; var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null; @@ -11111,7 +11649,7 @@ ReactDOMComponent.Mixin = { var lastHasContentOrHtml = lastContent != null || lastHtml != null; var nextHasContentOrHtml = nextContent != null || nextHtml != null; if (lastChildren != null && nextChildren == null) { - this.updateChildren(null, transaction); + this.updateChildren(null, transaction, context); } else if (lastHasContentOrHtml && !nextHasContentOrHtml) { this.updateTextContent(''); } @@ -11122,13 +11660,13 @@ ReactDOMComponent.Mixin = { } } else if (nextHtml != null) { if (lastHtml !== nextHtml) { - ReactComponent.BackendIDOperations.updateInnerHTMLByID( + BackendIDOperations.updateInnerHTMLByID( this._rootNodeID, nextHtml ); } } else if (nextChildren != null) { - this.updateChildren(nextChildren, transaction); + this.updateChildren(nextChildren, transaction, context); } }, @@ -11141,25 +11679,35 @@ ReactDOMComponent.Mixin = { unmountComponent: function() { this.unmountChildren(); ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID); - ReactComponent.Mixin.unmountComponent.call(this); + ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID); + this._rootNodeID = null; } }; +ReactPerf.measureMethods(ReactDOMComponent, 'ReactDOMComponent', { + mountComponent: 'mountComponent', + updateComponent: 'updateComponent' +}); + assign( ReactDOMComponent.prototype, - ReactComponent.Mixin, ReactDOMComponent.Mixin, - ReactMultiChild.Mixin, - ReactBrowserComponentMixin + ReactMultiChild.Mixin ); +ReactDOMComponent.injection = { + injectIDOperations: function(IDOperations) { + ReactDOMComponent.BackendIDOperations = BackendIDOperations = IDOperations; + } +}; + module.exports = ReactDOMComponent; }).call(this,require('_process')) -},{"./CSSPropertyOperations":47,"./DOMProperty":53,"./DOMPropertyOperations":54,"./Object.assign":70,"./ReactBrowserComponentMixin":73,"./ReactBrowserEventEmitter":74,"./ReactComponent":78,"./ReactMount":111,"./ReactMultiChild":112,"./ReactPerf":116,"./escapeTextForBrowser":165,"./invariant":182,"./isEventSupported":183,"./keyOf":189,"./monitorCodeUse":192,"_process":1}],87:[function(require,module,exports){ +},{"./CSSPropertyOperations":46,"./DOMProperty":51,"./DOMPropertyOperations":52,"./Object.assign":69,"./ReactBrowserEventEmitter":73,"./ReactComponentBrowserEnvironment":80,"./ReactMount":117,"./ReactMultiChild":118,"./ReactPerf":122,"./escapeTextContentForBrowser":172,"./invariant":191,"./isEventSupported":192,"./keyOf":198,"./warning":212,"_process":1}],89:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -11169,17 +11717,15 @@ module.exports = ReactDOMComponent; * @providesModule ReactDOMForm */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); var LocalEventTrapMixin = require("./LocalEventTrapMixin"); var ReactBrowserComponentMixin = require("./ReactBrowserComponentMixin"); -var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactClass = require("./ReactClass"); var ReactElement = require("./ReactElement"); -var ReactDOM = require("./ReactDOM"); -// Store a reference to the <form> `ReactDOMComponent`. TODO: use string -var form = ReactElement.createFactory(ReactDOM.form.type); +var form = ReactElement.createFactory('form'); /** * Since onSubmit doesn't bubble OR capture on the top level in IE8, we need @@ -11187,8 +11733,9 @@ var form = ReactElement.createFactory(ReactDOM.form.type); * do to accomplish this, but the most reliable is to make <form> a * composite component and use `componentDidMount` to attach the event handlers. */ -var ReactDOMForm = ReactCompositeComponent.createClass({ +var ReactDOMForm = ReactClass.createClass({ displayName: 'ReactDOMForm', + tagName: 'FORM', mixins: [ReactBrowserComponentMixin, LocalEventTrapMixin], @@ -11207,10 +11754,10 @@ var ReactDOMForm = ReactCompositeComponent.createClass({ module.exports = ReactDOMForm; -},{"./EventConstants":58,"./LocalEventTrapMixin":68,"./ReactBrowserComponentMixin":73,"./ReactCompositeComponent":81,"./ReactDOM":84,"./ReactElement":99}],88:[function(require,module,exports){ +},{"./EventConstants":56,"./LocalEventTrapMixin":67,"./ReactBrowserComponentMixin":72,"./ReactClass":78,"./ReactElement":103}],90:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -11223,7 +11770,7 @@ module.exports = ReactDOMForm; /*jslint evil: true */ -"use strict"; +'use strict'; var CSSPropertyOperations = require("./CSSPropertyOperations"); var DOMChildrenOperations = require("./DOMChildrenOperations"); @@ -11248,7 +11795,7 @@ var INVALID_PROPERTY_ERRORS = { /** * Operations used to process updates to DOM nodes. This is made injectable via - * `ReactComponent.BackendIDOperations`. + * `ReactDOMComponent.BackendIDOperations`. */ var ReactDOMIDOperations = { @@ -11261,27 +11808,23 @@ var ReactDOMIDOperations = { * @param {*} value New value of the property. * @internal */ - updatePropertyByID: ReactPerf.measure( - 'ReactDOMIDOperations', - 'updatePropertyByID', - function(id, name, value) { - var node = ReactMount.getNode(id); - ("production" !== process.env.NODE_ENV ? invariant( - !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), - 'updatePropertyByID(...): %s', - INVALID_PROPERTY_ERRORS[name] - ) : invariant(!INVALID_PROPERTY_ERRORS.hasOwnProperty(name))); - - // If we're updating to null or undefined, we should remove the property - // from the DOM node instead of inadvertantly setting to a string. This - // brings us in line with the same behavior we have on initial render. - if (value != null) { - DOMPropertyOperations.setValueForProperty(node, name, value); - } else { - DOMPropertyOperations.deleteValueForProperty(node, name); - } + updatePropertyByID: function(id, name, value) { + var node = ReactMount.getNode(id); + ("production" !== process.env.NODE_ENV ? invariant( + !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), + 'updatePropertyByID(...): %s', + INVALID_PROPERTY_ERRORS[name] + ) : invariant(!INVALID_PROPERTY_ERRORS.hasOwnProperty(name))); + + // If we're updating to null or undefined, we should remove the property + // from the DOM node instead of inadvertantly setting to a string. This + // brings us in line with the same behavior we have on initial render. + if (value != null) { + DOMPropertyOperations.setValueForProperty(node, name, value); + } else { + DOMPropertyOperations.deleteValueForProperty(node, name); } - ), + }, /** * Updates a DOM node to remove a property. This should only be used to remove @@ -11291,19 +11834,15 @@ var ReactDOMIDOperations = { * @param {string} name A property name to remove, see `DOMProperty`. * @internal */ - deletePropertyByID: ReactPerf.measure( - 'ReactDOMIDOperations', - 'deletePropertyByID', - function(id, name, value) { - var node = ReactMount.getNode(id); - ("production" !== process.env.NODE_ENV ? invariant( - !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), - 'updatePropertyByID(...): %s', - INVALID_PROPERTY_ERRORS[name] - ) : invariant(!INVALID_PROPERTY_ERRORS.hasOwnProperty(name))); - DOMPropertyOperations.deleteValueForProperty(node, name, value); - } - ), + deletePropertyByID: function(id, name, value) { + var node = ReactMount.getNode(id); + ("production" !== process.env.NODE_ENV ? invariant( + !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), + 'updatePropertyByID(...): %s', + INVALID_PROPERTY_ERRORS[name] + ) : invariant(!INVALID_PROPERTY_ERRORS.hasOwnProperty(name))); + DOMPropertyOperations.deleteValueForProperty(node, name, value); + }, /** * Updates a DOM node with new style values. If a value is specified as '', @@ -11313,14 +11852,10 @@ var ReactDOMIDOperations = { * @param {object} styles Mapping from styles to values. * @internal */ - updateStylesByID: ReactPerf.measure( - 'ReactDOMIDOperations', - 'updateStylesByID', - function(id, styles) { - var node = ReactMount.getNode(id); - CSSPropertyOperations.setValueForStyles(node, styles); - } - ), + updateStylesByID: function(id, styles) { + var node = ReactMount.getNode(id); + CSSPropertyOperations.setValueForStyles(node, styles); + }, /** * Updates a DOM node's innerHTML. @@ -11329,14 +11864,10 @@ var ReactDOMIDOperations = { * @param {string} html An HTML string. * @internal */ - updateInnerHTMLByID: ReactPerf.measure( - 'ReactDOMIDOperations', - 'updateInnerHTMLByID', - function(id, html) { - var node = ReactMount.getNode(id); - setInnerHTML(node, html); - } - ), + updateInnerHTMLByID: function(id, html) { + var node = ReactMount.getNode(id); + setInnerHTML(node, html); + }, /** * Updates a DOM node's text content set by `props.content`. @@ -11345,14 +11876,10 @@ var ReactDOMIDOperations = { * @param {string} content Text content. * @internal */ - updateTextContentByID: ReactPerf.measure( - 'ReactDOMIDOperations', - 'updateTextContentByID', - function(id, content) { - var node = ReactMount.getNode(id); - DOMChildrenOperations.updateTextContent(node, content); - } - ), + updateTextContentByID: function(id, content) { + var node = ReactMount.getNode(id); + DOMChildrenOperations.updateTextContent(node, content); + }, /** * Replaces a DOM node that exists in the document with markup. @@ -11362,14 +11889,10 @@ var ReactDOMIDOperations = { * @internal * @see {Danger.dangerouslyReplaceNodeWithMarkup} */ - dangerouslyReplaceNodeWithMarkupByID: ReactPerf.measure( - 'ReactDOMIDOperations', - 'dangerouslyReplaceNodeWithMarkupByID', - function(id, markup) { - var node = ReactMount.getNode(id); - DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup); - } - ), + dangerouslyReplaceNodeWithMarkupByID: function(id, markup) { + var node = ReactMount.getNode(id); + DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup); + }, /** * Updates a component's children by processing a series of updates. @@ -11378,24 +11901,75 @@ var ReactDOMIDOperations = { * @param {array<string>} markup List of markup strings. * @internal */ - dangerouslyProcessChildrenUpdates: ReactPerf.measure( - 'ReactDOMIDOperations', - 'dangerouslyProcessChildrenUpdates', - function(updates, markup) { - for (var i = 0; i < updates.length; i++) { - updates[i].parentNode = ReactMount.getNode(updates[i].parentID); - } - DOMChildrenOperations.processUpdates(updates, markup); + dangerouslyProcessChildrenUpdates: function(updates, markup) { + for (var i = 0; i < updates.length; i++) { + updates[i].parentNode = ReactMount.getNode(updates[i].parentID); } - ) + DOMChildrenOperations.processUpdates(updates, markup); + } }; +ReactPerf.measureMethods(ReactDOMIDOperations, 'ReactDOMIDOperations', { + updatePropertyByID: 'updatePropertyByID', + deletePropertyByID: 'deletePropertyByID', + updateStylesByID: 'updateStylesByID', + updateInnerHTMLByID: 'updateInnerHTMLByID', + updateTextContentByID: 'updateTextContentByID', + dangerouslyReplaceNodeWithMarkupByID: 'dangerouslyReplaceNodeWithMarkupByID', + dangerouslyProcessChildrenUpdates: 'dangerouslyProcessChildrenUpdates' +}); + module.exports = ReactDOMIDOperations; }).call(this,require('_process')) -},{"./CSSPropertyOperations":47,"./DOMChildrenOperations":52,"./DOMPropertyOperations":54,"./ReactMount":111,"./ReactPerf":116,"./invariant":182,"./setInnerHTML":196,"_process":1}],89:[function(require,module,exports){ +},{"./CSSPropertyOperations":46,"./DOMChildrenOperations":50,"./DOMPropertyOperations":52,"./ReactMount":117,"./ReactPerf":122,"./invariant":191,"./setInnerHTML":205,"_process":1}],91:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDOMIframe + */ + +'use strict'; + +var EventConstants = require("./EventConstants"); +var LocalEventTrapMixin = require("./LocalEventTrapMixin"); +var ReactBrowserComponentMixin = require("./ReactBrowserComponentMixin"); +var ReactClass = require("./ReactClass"); +var ReactElement = require("./ReactElement"); + +var iframe = ReactElement.createFactory('iframe'); + /** - * Copyright 2013-2014, Facebook, Inc. + * Since onLoad doesn't bubble OR capture on the top level in IE8, we need to + * capture it on the <iframe> element itself. There are lots of hacks we could + * do to accomplish this, but the most reliable is to make <iframe> a composite + * component and use `componentDidMount` to attach the event handlers. + */ +var ReactDOMIframe = ReactClass.createClass({ + displayName: 'ReactDOMIframe', + tagName: 'IFRAME', + + mixins: [ReactBrowserComponentMixin, LocalEventTrapMixin], + + render: function() { + return iframe(this.props); + }, + + componentDidMount: function() { + this.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load'); + } +}); + +module.exports = ReactDOMIframe; + +},{"./EventConstants":56,"./LocalEventTrapMixin":67,"./ReactBrowserComponentMixin":72,"./ReactClass":78,"./ReactElement":103}],92:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -11405,17 +11979,15 @@ module.exports = ReactDOMIDOperations; * @providesModule ReactDOMImg */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); var LocalEventTrapMixin = require("./LocalEventTrapMixin"); var ReactBrowserComponentMixin = require("./ReactBrowserComponentMixin"); -var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactClass = require("./ReactClass"); var ReactElement = require("./ReactElement"); -var ReactDOM = require("./ReactDOM"); -// Store a reference to the <img> `ReactDOMComponent`. TODO: use string -var img = ReactElement.createFactory(ReactDOM.img.type); +var img = ReactElement.createFactory('img'); /** * Since onLoad doesn't bubble OR capture on the top level in IE8, we need to @@ -11423,7 +11995,7 @@ var img = ReactElement.createFactory(ReactDOM.img.type); * to accomplish this, but the most reliable is to make <img> a composite * component and use `componentDidMount` to attach the event handlers. */ -var ReactDOMImg = ReactCompositeComponent.createClass({ +var ReactDOMImg = ReactClass.createClass({ displayName: 'ReactDOMImg', tagName: 'IMG', @@ -11441,10 +12013,10 @@ var ReactDOMImg = ReactCompositeComponent.createClass({ module.exports = ReactDOMImg; -},{"./EventConstants":58,"./LocalEventTrapMixin":68,"./ReactBrowserComponentMixin":73,"./ReactCompositeComponent":81,"./ReactDOM":84,"./ReactElement":99}],90:[function(require,module,exports){ +},{"./EventConstants":56,"./LocalEventTrapMixin":67,"./ReactBrowserComponentMixin":72,"./ReactClass":78,"./ReactElement":103}],93:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -11454,23 +12026,21 @@ module.exports = ReactDOMImg; * @providesModule ReactDOMInput */ -"use strict"; +'use strict'; var AutoFocusMixin = require("./AutoFocusMixin"); var DOMPropertyOperations = require("./DOMPropertyOperations"); var LinkedValueUtils = require("./LinkedValueUtils"); var ReactBrowserComponentMixin = require("./ReactBrowserComponentMixin"); -var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactClass = require("./ReactClass"); var ReactElement = require("./ReactElement"); -var ReactDOM = require("./ReactDOM"); var ReactMount = require("./ReactMount"); var ReactUpdates = require("./ReactUpdates"); var assign = require("./Object.assign"); var invariant = require("./invariant"); -// Store a reference to the <input> `ReactDOMComponent`. TODO: use string -var input = ReactElement.createFactory(ReactDOM.input.type); +var input = ReactElement.createFactory('input'); var instancesByReactID = {}; @@ -11497,8 +12067,9 @@ function forceUpdateIfMounted() { * * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html */ -var ReactDOMInput = ReactCompositeComponent.createClass({ +var ReactDOMInput = ReactClass.createClass({ displayName: 'ReactDOMInput', + tagName: 'INPUT', mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin], @@ -11619,10 +12190,10 @@ var ReactDOMInput = ReactCompositeComponent.createClass({ module.exports = ReactDOMInput; }).call(this,require('_process')) -},{"./AutoFocusMixin":43,"./DOMPropertyOperations":54,"./LinkedValueUtils":67,"./Object.assign":70,"./ReactBrowserComponentMixin":73,"./ReactCompositeComponent":81,"./ReactDOM":84,"./ReactElement":99,"./ReactMount":111,"./ReactUpdates":132,"./invariant":182,"_process":1}],91:[function(require,module,exports){ +},{"./AutoFocusMixin":42,"./DOMPropertyOperations":52,"./LinkedValueUtils":66,"./Object.assign":69,"./ReactBrowserComponentMixin":72,"./ReactClass":78,"./ReactElement":103,"./ReactMount":117,"./ReactUpdates":140,"./invariant":191,"_process":1}],94:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -11632,23 +12203,22 @@ module.exports = ReactDOMInput; * @providesModule ReactDOMOption */ -"use strict"; +'use strict'; var ReactBrowserComponentMixin = require("./ReactBrowserComponentMixin"); -var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactClass = require("./ReactClass"); var ReactElement = require("./ReactElement"); -var ReactDOM = require("./ReactDOM"); var warning = require("./warning"); -// Store a reference to the <option> `ReactDOMComponent`. TODO: use string -var option = ReactElement.createFactory(ReactDOM.option.type); +var option = ReactElement.createFactory('option'); /** * Implements an <option> native component that warns when `selected` is set. */ -var ReactDOMOption = ReactCompositeComponent.createClass({ +var ReactDOMOption = ReactClass.createClass({ displayName: 'ReactDOMOption', + tagName: 'OPTION', mixins: [ReactBrowserComponentMixin], @@ -11672,9 +12242,9 @@ var ReactDOMOption = ReactCompositeComponent.createClass({ module.exports = ReactDOMOption; }).call(this,require('_process')) -},{"./ReactBrowserComponentMixin":73,"./ReactCompositeComponent":81,"./ReactDOM":84,"./ReactElement":99,"./warning":202,"_process":1}],92:[function(require,module,exports){ +},{"./ReactBrowserComponentMixin":72,"./ReactClass":78,"./ReactElement":103,"./warning":212,"_process":1}],95:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -11684,26 +12254,27 @@ module.exports = ReactDOMOption; * @providesModule ReactDOMSelect */ -"use strict"; +'use strict'; var AutoFocusMixin = require("./AutoFocusMixin"); var LinkedValueUtils = require("./LinkedValueUtils"); var ReactBrowserComponentMixin = require("./ReactBrowserComponentMixin"); -var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactClass = require("./ReactClass"); var ReactElement = require("./ReactElement"); -var ReactDOM = require("./ReactDOM"); var ReactUpdates = require("./ReactUpdates"); var assign = require("./Object.assign"); -// Store a reference to the <select> `ReactDOMComponent`. TODO: use string -var select = ReactElement.createFactory(ReactDOM.select.type); +var select = ReactElement.createFactory('select'); -function updateWithPendingValueIfMounted() { +function updateOptionsIfPendingUpdateAndMounted() { /*jshint validthis:true */ - if (this.isMounted()) { - this.setState({value: this._pendingValue}); - this._pendingValue = 0; + if (this._pendingUpdate) { + this._pendingUpdate = false; + var value = LinkedValueUtils.getValue(this); + if (value != null && this.isMounted()) { + updateOptions(this, value); + } } } @@ -11713,7 +12284,7 @@ function updateWithPendingValueIfMounted() { */ function selectValueType(props, propName, componentName) { if (props[propName] == null) { - return; + return null; } if (props.multiple) { if (!Array.isArray(props[propName])) { @@ -11733,32 +12304,37 @@ function selectValueType(props, propName, componentName) { } /** - * If `value` is supplied, updates <option> elements on mount and update. * @param {ReactComponent} component Instance of ReactDOMSelect - * @param {?*} propValue For uncontrolled components, null/undefined. For - * controlled components, a string (or with `multiple`, a list of strings). + * @param {*} propValue A stringable (with `multiple`, a list of stringables). * @private */ function updateOptions(component, propValue) { - var multiple = component.props.multiple; - var value = propValue != null ? propValue : component.state.value; - var options = component.getDOMNode().options; var selectedValue, i, l; - if (multiple) { + var options = component.getDOMNode().options; + + if (component.props.multiple) { selectedValue = {}; - for (i = 0, l = value.length; i < l; ++i) { - selectedValue['' + value[i]] = true; + for (i = 0, l = propValue.length; i < l; i++) { + selectedValue['' + propValue[i]] = true; + } + for (i = 0, l = options.length; i < l; i++) { + var selected = selectedValue.hasOwnProperty(options[i].value); + if (options[i].selected !== selected) { + options[i].selected = selected; + } } } else { - selectedValue = '' + value; - } - for (i = 0, l = options.length; i < l; i++) { - var selected = multiple ? - selectedValue.hasOwnProperty(options[i].value) : - options[i].value === selectedValue; - - if (selected !== options[i].selected) { - options[i].selected = selected; + // Do not set `select.value` as exact behavior isn't consistent across all + // browsers for all cases. + selectedValue = '' + propValue; + for (i = 0, l = options.length; i < l; i++) { + if (options[i].value === selectedValue) { + options[i].selected = true; + return; + } + } + if (options.length) { + options[0].selected = true; } } } @@ -11766,7 +12342,7 @@ function updateOptions(component, propValue) { /** * Implements a <select> native component that allows optionally setting the * props `value` and `defaultValue`. If `multiple` is false, the prop must be a - * string. If `multiple` is true, the prop must be an array of strings. + * stringable. If `multiple` is true, the prop must be an array of stringables. * * If `value` is not supplied (or null/undefined), user actions that change the * selected option will trigger updates to the rendered options. @@ -11778,8 +12354,9 @@ function updateOptions(component, propValue) { * If `defaultValue` is provided, any options with the supplied values will be * selected. */ -var ReactDOMSelect = ReactCompositeComponent.createClass({ +var ReactDOMSelect = ReactClass.createClass({ displayName: 'ReactDOMSelect', + tagName: 'SELECT', mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin], @@ -11788,22 +12365,6 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({ value: selectValueType }, - getInitialState: function() { - return {value: this.props.defaultValue || (this.props.multiple ? [] : '')}; - }, - - componentWillMount: function() { - this._pendingValue = null; - }, - - componentWillReceiveProps: function(nextProps) { - if (!this.props.multiple && nextProps.multiple) { - this.setState({value: [this.state.value]}); - } else if (this.props.multiple && !nextProps.multiple) { - this.setState({value: this.state.value[0]}); - } - }, - render: function() { // Clone `this.props` so we don't mutate the input. var props = assign({}, this.props); @@ -11814,16 +12375,32 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({ return select(props, this.props.children); }, + componentWillMount: function() { + this._pendingUpdate = false; + }, + componentDidMount: function() { - updateOptions(this, LinkedValueUtils.getValue(this)); + var value = LinkedValueUtils.getValue(this); + if (value != null) { + updateOptions(this, value); + } else if (this.props.defaultValue != null) { + updateOptions(this, this.props.defaultValue); + } }, componentDidUpdate: function(prevProps) { var value = LinkedValueUtils.getValue(this); - var prevMultiple = !!prevProps.multiple; - var multiple = !!this.props.multiple; - if (value != null || prevMultiple !== multiple) { + if (value != null) { + this._pendingUpdate = false; updateOptions(this, value); + } else if (!prevProps.multiple !== !this.props.multiple) { + // For simplicity, reapply `defaultValue` if `multiple` is toggled. + if (this.props.defaultValue != null) { + updateOptions(this, this.props.defaultValue); + } else { + // Revert the select back to its default unselected state. + updateOptions(this, this.props.multiple ? [] : ''); + } } }, @@ -11834,21 +12411,8 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({ returnValue = onChange.call(this, event); } - var selectedValue; - if (this.props.multiple) { - selectedValue = []; - var options = event.target.options; - for (var i = 0, l = options.length; i < l; i++) { - if (options[i].selected) { - selectedValue.push(options[i].value); - } - } - } else { - selectedValue = event.target.value; - } - - this._pendingValue = selectedValue; - ReactUpdates.asap(updateWithPendingValueIfMounted, this); + this._pendingUpdate = true; + ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this); return returnValue; } @@ -11856,9 +12420,9 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({ module.exports = ReactDOMSelect; -},{"./AutoFocusMixin":43,"./LinkedValueUtils":67,"./Object.assign":70,"./ReactBrowserComponentMixin":73,"./ReactCompositeComponent":81,"./ReactDOM":84,"./ReactElement":99,"./ReactUpdates":132}],93:[function(require,module,exports){ +},{"./AutoFocusMixin":42,"./LinkedValueUtils":66,"./Object.assign":69,"./ReactBrowserComponentMixin":72,"./ReactClass":78,"./ReactElement":103,"./ReactUpdates":140}],96:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -11868,7 +12432,7 @@ module.exports = ReactDOMSelect; * @providesModule ReactDOMSelection */ -"use strict"; +'use strict'; var ExecutionEnvironment = require("./ExecutionEnvironment"); @@ -12048,7 +12612,11 @@ function setModernOffsets(node, offsets) { } } -var useIEOffsets = ExecutionEnvironment.canUseDOM && document.selection; +var useIEOffsets = ( + ExecutionEnvironment.canUseDOM && + 'selection' in document && + !('getSelection' in window) +); var ReactDOMSelection = { /** @@ -12065,10 +12633,127 @@ var ReactDOMSelection = { module.exports = ReactDOMSelection; -},{"./ExecutionEnvironment":64,"./getNodeForCharacterOffset":175,"./getTextContentAccessor":177}],94:[function(require,module,exports){ +},{"./ExecutionEnvironment":62,"./getNodeForCharacterOffset":184,"./getTextContentAccessor":186}],97:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDOMTextComponent + * @typechecks static-only + */ + +'use strict'; + +var DOMPropertyOperations = require("./DOMPropertyOperations"); +var ReactComponentBrowserEnvironment = + require("./ReactComponentBrowserEnvironment"); +var ReactDOMComponent = require("./ReactDOMComponent"); + +var assign = require("./Object.assign"); +var escapeTextContentForBrowser = require("./escapeTextContentForBrowser"); + +/** + * Text nodes violate a couple assumptions that React makes about components: + * + * - When mounting text into the DOM, adjacent text nodes are merged. + * - Text nodes cannot be assigned a React root ID. + * + * This component is used to wrap strings in elements so that they can undergo + * the same reconciliation that is applied to elements. + * + * TODO: Investigate representing React components in the DOM with text nodes. + * + * @class ReactDOMTextComponent + * @extends ReactComponent + * @internal + */ +var ReactDOMTextComponent = function(props) { + // This constructor and its argument is currently used by mocks. +}; + +assign(ReactDOMTextComponent.prototype, { + + /** + * @param {ReactText} text + * @internal + */ + construct: function(text) { + // TODO: This is really a ReactText (ReactNode), not a ReactElement + this._currentElement = text; + this._stringText = '' + text; + + // Properties + this._rootNodeID = null; + this._mountIndex = 0; + }, + + /** + * Creates the markup for this text node. This node is not intended to have + * any features besides containing text content. + * + * @param {string} rootID DOM ID of the root node. + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction + * @return {string} Markup for this text node. + * @internal + */ + mountComponent: function(rootID, transaction, context) { + this._rootNodeID = rootID; + var escapedText = escapeTextContentForBrowser(this._stringText); + + if (transaction.renderToStaticMarkup) { + // Normally we'd wrap this in a `span` for the reasons stated above, but + // since this is a situation where React won't take over (static pages), + // we can simply return the text as it is. + return escapedText; + } + + return ( + '<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' + + escapedText + + '</span>' + ); + }, + + /** + * Updates this component by updating the text content. + * + * @param {ReactText} nextText The next text content + * @param {ReactReconcileTransaction} transaction + * @internal + */ + receiveComponent: function(nextText, transaction) { + if (nextText !== this._currentElement) { + this._currentElement = nextText; + var nextStringText = '' + nextText; + if (nextStringText !== this._stringText) { + // TODO: Save this as pending props and use performUpdateIfNecessary + // and/or updateComponent to do the actual update for consistency with + // other component types? + this._stringText = nextStringText; + ReactDOMComponent.BackendIDOperations.updateTextContentByID( + this._rootNodeID, + nextStringText + ); + } + } + }, + + unmountComponent: function() { + ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID); + } + +}); + +module.exports = ReactDOMTextComponent; + +},{"./DOMPropertyOperations":52,"./Object.assign":69,"./ReactComponentBrowserEnvironment":80,"./ReactDOMComponent":88,"./escapeTextContentForBrowser":172}],98:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -12078,15 +12763,14 @@ module.exports = ReactDOMSelection; * @providesModule ReactDOMTextarea */ -"use strict"; +'use strict'; var AutoFocusMixin = require("./AutoFocusMixin"); var DOMPropertyOperations = require("./DOMPropertyOperations"); var LinkedValueUtils = require("./LinkedValueUtils"); var ReactBrowserComponentMixin = require("./ReactBrowserComponentMixin"); -var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactClass = require("./ReactClass"); var ReactElement = require("./ReactElement"); -var ReactDOM = require("./ReactDOM"); var ReactUpdates = require("./ReactUpdates"); var assign = require("./Object.assign"); @@ -12094,8 +12778,7 @@ var invariant = require("./invariant"); var warning = require("./warning"); -// Store a reference to the <textarea> `ReactDOMComponent`. TODO: use string -var textarea = ReactElement.createFactory(ReactDOM.textarea.type); +var textarea = ReactElement.createFactory('textarea'); function forceUpdateIfMounted() { /*jshint validthis:true */ @@ -12119,8 +12802,9 @@ function forceUpdateIfMounted() { * The rendered element will be initialized with an empty value, the prop * `defaultValue` if specified, or the children content (deprecated). */ -var ReactDOMTextarea = ReactCompositeComponent.createClass({ +var ReactDOMTextarea = ReactClass.createClass({ displayName: 'ReactDOMTextarea', + tagName: 'TEXTAREA', mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin], @@ -12206,9 +12890,9 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({ module.exports = ReactDOMTextarea; }).call(this,require('_process')) -},{"./AutoFocusMixin":43,"./DOMPropertyOperations":54,"./LinkedValueUtils":67,"./Object.assign":70,"./ReactBrowserComponentMixin":73,"./ReactCompositeComponent":81,"./ReactDOM":84,"./ReactElement":99,"./ReactUpdates":132,"./invariant":182,"./warning":202,"_process":1}],95:[function(require,module,exports){ +},{"./AutoFocusMixin":42,"./DOMPropertyOperations":52,"./LinkedValueUtils":66,"./Object.assign":69,"./ReactBrowserComponentMixin":72,"./ReactClass":78,"./ReactElement":103,"./ReactUpdates":140,"./invariant":191,"./warning":212,"_process":1}],99:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -12218,7 +12902,7 @@ module.exports = ReactDOMTextarea; * @providesModule ReactDefaultBatchingStrategy */ -"use strict"; +'use strict'; var ReactUpdates = require("./ReactUpdates"); var Transaction = require("./Transaction"); @@ -12263,26 +12947,26 @@ var ReactDefaultBatchingStrategy = { * Call the provided function in a context within which calls to `setState` * and friends are batched such that components aren't updated unnecessarily. */ - batchedUpdates: function(callback, a, b) { + batchedUpdates: function(callback, a, b, c, d) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; // The code is written this way to avoid extra allocations if (alreadyBatchingUpdates) { - callback(a, b); + callback(a, b, c, d); } else { - transaction.perform(callback, null, a, b); + transaction.perform(callback, null, a, b, c, d); } } }; module.exports = ReactDefaultBatchingStrategy; -},{"./Object.assign":70,"./ReactUpdates":132,"./Transaction":149,"./emptyFunction":163}],96:[function(require,module,exports){ +},{"./Object.assign":69,"./ReactUpdates":140,"./Transaction":157,"./emptyFunction":170}],100:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -12292,18 +12976,18 @@ module.exports = ReactDefaultBatchingStrategy; * @providesModule ReactDefaultInjection */ -"use strict"; +'use strict'; var BeforeInputEventPlugin = require("./BeforeInputEventPlugin"); var ChangeEventPlugin = require("./ChangeEventPlugin"); var ClientReactRootIndex = require("./ClientReactRootIndex"); -var CompositionEventPlugin = require("./CompositionEventPlugin"); var DefaultEventPluginOrder = require("./DefaultEventPluginOrder"); var EnterLeaveEventPlugin = require("./EnterLeaveEventPlugin"); var ExecutionEnvironment = require("./ExecutionEnvironment"); var HTMLDOMPropertyConfig = require("./HTMLDOMPropertyConfig"); var MobileSafariClickEventPlugin = require("./MobileSafariClickEventPlugin"); var ReactBrowserComponentMixin = require("./ReactBrowserComponentMixin"); +var ReactClass = require("./ReactClass"); var ReactComponentBrowserEnvironment = require("./ReactComponentBrowserEnvironment"); var ReactDefaultBatchingStrategy = require("./ReactDefaultBatchingStrategy"); @@ -12311,14 +12995,19 @@ var ReactDOMComponent = require("./ReactDOMComponent"); var ReactDOMButton = require("./ReactDOMButton"); var ReactDOMForm = require("./ReactDOMForm"); var ReactDOMImg = require("./ReactDOMImg"); +var ReactDOMIDOperations = require("./ReactDOMIDOperations"); +var ReactDOMIframe = require("./ReactDOMIframe"); var ReactDOMInput = require("./ReactDOMInput"); var ReactDOMOption = require("./ReactDOMOption"); var ReactDOMSelect = require("./ReactDOMSelect"); var ReactDOMTextarea = require("./ReactDOMTextarea"); +var ReactDOMTextComponent = require("./ReactDOMTextComponent"); +var ReactElement = require("./ReactElement"); var ReactEventListener = require("./ReactEventListener"); var ReactInjection = require("./ReactInjection"); var ReactInstanceHandles = require("./ReactInstanceHandles"); var ReactMount = require("./ReactMount"); +var ReactReconcileTransaction = require("./ReactReconcileTransaction"); var SelectEventPlugin = require("./SelectEventPlugin"); var ServerReactRootIndex = require("./ServerReactRootIndex"); var SimpleEventPlugin = require("./SimpleEventPlugin"); @@ -12326,6 +13015,22 @@ var SVGDOMPropertyConfig = require("./SVGDOMPropertyConfig"); var createFullPageComponent = require("./createFullPageComponent"); +function autoGenerateWrapperClass(type) { + return ReactClass.createClass({ + tagName: type.toUpperCase(), + render: function() { + return new ReactElement( + type, + null, + null, + null, + null, + this.props + ); + } + }); +} + function inject() { ReactInjection.EventEmitter.injectReactEventListener( ReactEventListener @@ -12346,7 +13051,6 @@ function inject() { SimpleEventPlugin: SimpleEventPlugin, EnterLeaveEventPlugin: EnterLeaveEventPlugin, ChangeEventPlugin: ChangeEventPlugin, - CompositionEventPlugin: CompositionEventPlugin, MobileSafariClickEventPlugin: MobileSafariClickEventPlugin, SelectEventPlugin: SelectEventPlugin, BeforeInputEventPlugin: BeforeInputEventPlugin @@ -12356,9 +13060,22 @@ function inject() { ReactDOMComponent ); + ReactInjection.NativeComponent.injectTextComponentClass( + ReactDOMTextComponent + ); + + ReactInjection.NativeComponent.injectAutoWrapper( + autoGenerateWrapperClass + ); + + // This needs to happen before createFullPageComponent() otherwise the mixin + // won't be included. + ReactInjection.Class.injectMixin(ReactBrowserComponentMixin); + ReactInjection.NativeComponent.injectComponentClasses({ 'button': ReactDOMButton, 'form': ReactDOMForm, + 'iframe': ReactDOMIframe, 'img': ReactDOMImg, 'input': ReactDOMInput, 'option': ReactDOMOption, @@ -12370,17 +13087,13 @@ function inject() { 'body': createFullPageComponent('body') }); - // This needs to happen after createFullPageComponent() otherwise the mixin - // gets double injected. - ReactInjection.CompositeComponent.injectMixin(ReactBrowserComponentMixin); - ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig); ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig); ReactInjection.EmptyComponent.injectEmptyComponent('noscript'); ReactInjection.Updates.injectReconcileTransaction( - ReactComponentBrowserEnvironment.ReactReconcileTransaction + ReactReconcileTransaction ); ReactInjection.Updates.injectBatchingStrategy( ReactDefaultBatchingStrategy @@ -12393,6 +13106,7 @@ function inject() { ); ReactInjection.Component.injectEnvironment(ReactComponentBrowserEnvironment); + ReactInjection.DOMComponent.injectIDOperations(ReactDOMIDOperations); if ("production" !== process.env.NODE_ENV) { var url = (ExecutionEnvironment.canUseDOM && window.location.href) || ''; @@ -12408,9 +13122,9 @@ module.exports = { }; }).call(this,require('_process')) -},{"./BeforeInputEventPlugin":44,"./ChangeEventPlugin":49,"./ClientReactRootIndex":50,"./CompositionEventPlugin":51,"./DefaultEventPluginOrder":56,"./EnterLeaveEventPlugin":57,"./ExecutionEnvironment":64,"./HTMLDOMPropertyConfig":65,"./MobileSafariClickEventPlugin":69,"./ReactBrowserComponentMixin":73,"./ReactComponentBrowserEnvironment":79,"./ReactDOMButton":85,"./ReactDOMComponent":86,"./ReactDOMForm":87,"./ReactDOMImg":89,"./ReactDOMInput":90,"./ReactDOMOption":91,"./ReactDOMSelect":92,"./ReactDOMTextarea":94,"./ReactDefaultBatchingStrategy":95,"./ReactDefaultPerf":97,"./ReactEventListener":104,"./ReactInjection":105,"./ReactInstanceHandles":107,"./ReactMount":111,"./SVGDOMPropertyConfig":134,"./SelectEventPlugin":135,"./ServerReactRootIndex":136,"./SimpleEventPlugin":137,"./createFullPageComponent":158,"_process":1}],97:[function(require,module,exports){ +},{"./BeforeInputEventPlugin":43,"./ChangeEventPlugin":48,"./ClientReactRootIndex":49,"./DefaultEventPluginOrder":54,"./EnterLeaveEventPlugin":55,"./ExecutionEnvironment":62,"./HTMLDOMPropertyConfig":64,"./MobileSafariClickEventPlugin":68,"./ReactBrowserComponentMixin":72,"./ReactClass":78,"./ReactComponentBrowserEnvironment":80,"./ReactDOMButton":87,"./ReactDOMComponent":88,"./ReactDOMForm":89,"./ReactDOMIDOperations":90,"./ReactDOMIframe":91,"./ReactDOMImg":92,"./ReactDOMInput":93,"./ReactDOMOption":94,"./ReactDOMSelect":95,"./ReactDOMTextComponent":97,"./ReactDOMTextarea":98,"./ReactDefaultBatchingStrategy":99,"./ReactDefaultPerf":101,"./ReactElement":103,"./ReactEventListener":108,"./ReactInjection":110,"./ReactInstanceHandles":112,"./ReactMount":117,"./ReactReconcileTransaction":128,"./SVGDOMPropertyConfig":142,"./SelectEventPlugin":143,"./ServerReactRootIndex":144,"./SimpleEventPlugin":145,"./createFullPageComponent":166,"_process":1}],101:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -12421,7 +13135,7 @@ module.exports = { * @typechecks static-only */ -"use strict"; +'use strict'; var DOMProperty = require("./DOMProperty"); var ReactDefaultPerfAnalysis = require("./ReactDefaultPerfAnalysis"); @@ -12548,7 +13262,7 @@ var ReactDefaultPerf = { }, measure: function(moduleName, fnName, func) { - return function() {var args=Array.prototype.slice.call(arguments,0); + return function() {for (var args=[],$__0=0,$__1=arguments.length;$__0<$__1;$__0++) args.push(arguments[$__0]); var totalTime; var rv; var start; @@ -12574,13 +13288,13 @@ var ReactDefaultPerf = { ReactDefaultPerf._allMeasurements.length - 1 ].totalTime = performanceNow() - start; return rv; - } else if (moduleName === 'ReactDOMIDOperations' || - moduleName === 'ReactComponentBrowserEnvironment') { + } else if (fnName === '_mountImageIntoNode' || + moduleName === 'ReactDOMIDOperations') { start = performanceNow(); rv = func.apply(this, args); totalTime = performanceNow() - start; - if (fnName === 'mountImageIntoNode') { + if (fnName === '_mountImageIntoNode') { var mountID = ReactMount.getID(args[1]); ReactDefaultPerf._recordWrite(mountID, fnName, totalTime, args[0]); } else if (fnName === 'dangerouslyProcessChildrenUpdates') { @@ -12617,9 +13331,13 @@ var ReactDefaultPerf = { } return rv; } else if (moduleName === 'ReactCompositeComponent' && ( - fnName === 'mountComponent' || - fnName === 'updateComponent' || // TODO: receiveComponent()? - fnName === '_renderValidatedComponent')) { + (// TODO: receiveComponent()? + (fnName === 'mountComponent' || + fnName === 'updateComponent' || fnName === '_renderValidatedComponent')))) { + + if (typeof this._currentElement.type === 'string') { + return func.apply(this, args); + } var rootNodeID = fnName === 'mountComponent' ? args[0] : @@ -12654,8 +13372,10 @@ var ReactDefaultPerf = { } entry.displayNames[rootNodeID] = { - current: this.constructor.displayName, - owner: this._owner ? this._owner.constructor.displayName : '<root>' + current: this.getName(), + owner: this._currentElement._owner ? + this._currentElement._owner.getName() : + '<root>' }; return rv; @@ -12668,9 +13388,9 @@ var ReactDefaultPerf = { module.exports = ReactDefaultPerf; -},{"./DOMProperty":53,"./ReactDefaultPerfAnalysis":98,"./ReactMount":111,"./ReactPerf":116,"./performanceNow":195}],98:[function(require,module,exports){ +},{"./DOMProperty":51,"./ReactDefaultPerfAnalysis":102,"./ReactMount":117,"./ReactPerf":122,"./performanceNow":203}],102:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -12685,7 +13405,7 @@ var assign = require("./Object.assign"); // Don't try to save users less than 1.2ms (a number I made up) var DONT_CARE_THRESHOLD = 1.2; var DOM_OPERATION_TYPES = { - 'mountImageIntoNode': 'set innerHTML', + '_mountImageIntoNode': 'set innerHTML', INSERT_MARKUP: 'set innerHTML', MOVE_EXISTING: 'move', REMOVE_NODE: 'remove', @@ -12874,10 +13594,10 @@ var ReactDefaultPerfAnalysis = { module.exports = ReactDefaultPerfAnalysis; -},{"./Object.assign":70}],99:[function(require,module,exports){ +},{"./Object.assign":69}],103:[function(require,module,exports){ (function (process){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -12887,11 +13607,12 @@ module.exports = ReactDefaultPerfAnalysis; * @providesModule ReactElement */ -"use strict"; +'use strict'; var ReactContext = require("./ReactContext"); var ReactCurrentOwner = require("./ReactCurrentOwner"); +var assign = require("./Object.assign"); var warning = require("./warning"); var RESERVED_PROPS = { @@ -12922,8 +13643,9 @@ function defineWarningProperty(object, key) { set: function(value) { ("production" !== process.env.NODE_ENV ? warning( false, - 'Don\'t set the ' + key + ' property of the component. ' + - 'Mutate the existing props object instead.' + 'Don\'t set the %s property of the React element. Instead, ' + + 'specify the correct value when initially creating the element.', + key ) : null); this._store[key] = value; } @@ -12984,7 +13706,21 @@ var ReactElement = function(type, key, ref, owner, context, props) { // an external backing store so that we can freeze the whole object. // This can be replaced with a WeakMap once they are implemented in // commonly used development environments. - this._store = { validated: false, props: props }; + this._store = {props: props, originalProps: assign({}, props)}; + + // To make comparing ReactElements easier for testing purposes, we make + // the validation flag non-enumerable (where possible, which should + // include every environment we run tests in), so the test framework + // ignores it. + try { + Object.defineProperty(this._store, 'validated', { + configurable: false, + enumerable: false, + writable: true + }); + } catch (x) { + } + this._store.validated = false; // We're not allowed to set props directly on the object so we early // return and rely on the prototype membrane to forward to the backing @@ -13019,16 +13755,7 @@ ReactElement.createElement = function(type, config, children) { if (config != null) { ref = config.ref === undefined ? null : config.ref; - if ("production" !== process.env.NODE_ENV) { - ("production" !== process.env.NODE_ENV ? warning( - config.key !== null, - 'createElement(...): Encountered component with a `key` of null. In ' + - 'a future version, this will be treated as equivalent to the string ' + - '\'null\'; instead, provide an explicit key or use undefined.' - ) : null); - } - // TODO: Change this back to `config.key === undefined` - key = config.key == null ? null : '' + config.key; + key = config.key === undefined ? null : '' + config.key; // Remaining properties are added to a new props object for (propName in config) { if (config.hasOwnProperty(propName) && @@ -13052,7 +13779,7 @@ ReactElement.createElement = function(type, config, children) { } // Resolve default props - if (type.defaultProps) { + if (type && type.defaultProps) { var defaultProps = type.defaultProps; for (propName in defaultProps) { if (typeof props[propName] === 'undefined') { @@ -13077,6 +13804,7 @@ ReactElement.createFactory = function(type) { // easily accessed on elements. E.g. <Foo />.type === Foo.type. // This should not be named `constructor` since this may not be the function // that created the element, and it may not even be a constructor. + // Legacy hook TODO: Warn if this is accessed factory.type = type; return factory; }; @@ -13098,6 +13826,60 @@ ReactElement.cloneAndReplaceProps = function(oldElement, newProps) { return newElement; }; +ReactElement.cloneElement = function(element, config, children) { + var propName; + + // Original props are copied + var props = assign({}, element.props); + + // Reserved names are extracted + var key = element.key; + var ref = element.ref; + + // Owner will be preserved, unless ref is overridden + var owner = element._owner; + + if (config != null) { + if (config.ref !== undefined) { + // Silently steal the ref from the parent. + ref = config.ref; + owner = ReactCurrentOwner.current; + } + if (config.key !== undefined) { + key = '' + config.key; + } + // Remaining properties override existing props + for (propName in config) { + if (config.hasOwnProperty(propName) && + !RESERVED_PROPS.hasOwnProperty(propName)) { + props[propName] = config[propName]; + } + } + } + + // Children can be more than one argument, and those are transferred onto + // the newly allocated props object. + var childrenLength = arguments.length - 2; + if (childrenLength === 1) { + props.children = children; + } else if (childrenLength > 1) { + var childArray = Array(childrenLength); + for (var i = 0; i < childrenLength; i++) { + childArray[i] = arguments[i + 2]; + } + props.children = childArray; + } + + return new ReactElement( + element.type, + key, + ref, + owner, + element._context, + props + ); +}; + /** * @param {?object} object * @return {boolean} True if `object` is a valid component. @@ -13120,9 +13902,10 @@ ReactElement.isValidElement = function(object) { module.exports = ReactElement; }).call(this,require('_process')) -},{"./ReactContext":82,"./ReactCurrentOwner":83,"./warning":202,"_process":1}],100:[function(require,module,exports){ +},{"./Object.assign":69,"./ReactContext":84,"./ReactCurrentOwner":85,"./warning":212,"_process":1}],104:[function(require,module,exports){ +(function (process){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -13139,30 +13922,59 @@ module.exports = ReactElement; * that support it. */ -"use strict"; +'use strict'; var ReactElement = require("./ReactElement"); +var ReactFragment = require("./ReactFragment"); var ReactPropTypeLocations = require("./ReactPropTypeLocations"); +var ReactPropTypeLocationNames = require("./ReactPropTypeLocationNames"); var ReactCurrentOwner = require("./ReactCurrentOwner"); +var ReactNativeComponent = require("./ReactNativeComponent"); -var monitorCodeUse = require("./monitorCodeUse"); +var getIteratorFn = require("./getIteratorFn"); +var invariant = require("./invariant"); +var warning = require("./warning"); + +function getDeclarationErrorAddendum() { + if (ReactCurrentOwner.current) { + var name = ReactCurrentOwner.current.getName(); + if (name) { + return ' Check the render method of `' + name + '`.'; + } + } + return ''; +} /** * Warn if there's no key explicitly set on dynamic arrays of children or * object keys are not valid. This allows us to keep track of children between * updates. */ -var ownerHasKeyUseWarning = { - 'react_key_warning': {}, - 'react_numeric_key_warning': {} -}; -var ownerHasMonitoredObjectMap = {}; +var ownerHasKeyUseWarning = {}; var loggedTypeFailures = {}; var NUMERIC_PROPERTY_REGEX = /^\d+$/; /** + * Gets the instance's name for use in warnings. + * + * @internal + * @return {?string} Display name or undefined + */ +function getName(instance) { + var publicInstance = instance && instance.getPublicInstance(); + if (!publicInstance) { + return undefined; + } + var constructor = publicInstance.constructor; + if (!constructor) { + return undefined; + } + return constructor.displayName || constructor.name || undefined; +} + +/** * Gets the current owner's displayName for use in warnings. * * @internal @@ -13170,29 +13982,30 @@ var NUMERIC_PROPERTY_REGEX = /^\d+$/; */ function getCurrentOwnerDisplayName() { var current = ReactCurrentOwner.current; - return current && current.constructor.displayName || undefined; + return ( + current && getName(current) || undefined + ); } /** - * Warn if the component doesn't have an explicit key assigned to it. - * This component is in an array. The array could grow and shrink or be + * Warn if the element doesn't have an explicit key assigned to it. + * This element is in an array. The array could grow and shrink or be * reordered. All children that haven't already been validated are required to * have a "key" property assigned to it. * * @internal - * @param {ReactComponent} component Component that requires a key. - * @param {*} parentType component's parent's type. + * @param {ReactElement} element Element that requires a key. + * @param {*} parentType element's parent's type. */ -function validateExplicitKey(component, parentType) { - if (component._store.validated || component.key != null) { +function validateExplicitKey(element, parentType) { + if (element._store.validated || element.key != null) { return; } - component._store.validated = true; + element._store.validated = true; warnAndMonitorForKeyUse( - 'react_key_warning', - 'Each child in an array should have a unique "key" prop.', - component, + 'Each child in an array or iterator should have a unique "key" prop.', + element, parentType ); } @@ -13203,17 +14016,16 @@ function validateExplicitKey(component, parentType) { * * @internal * @param {string} name Property name of the key. - * @param {ReactComponent} component Component that requires a key. - * @param {*} parentType component's parent's type. + * @param {ReactElement} element Component that requires a key. + * @param {*} parentType element's parent's type. */ -function validatePropertyKey(name, component, parentType) { +function validatePropertyKey(name, element, parentType) { if (!NUMERIC_PROPERTY_REGEX.test(name)) { return; } warnAndMonitorForKeyUse( - 'react_numeric_key_warning', 'Child objects should have non-numeric keys so ordering is preserved.', - component, + element, parentType ); } @@ -13222,85 +14034,90 @@ function validatePropertyKey(name, component, parentType) { * Shared warning and monitoring code for the key warnings. * * @internal - * @param {string} warningID The id used when logging. * @param {string} message The base warning that gets output. - * @param {ReactComponent} component Component that requires a key. - * @param {*} parentType component's parent's type. + * @param {ReactElement} element Component that requires a key. + * @param {*} parentType element's parent's type. */ -function warnAndMonitorForKeyUse(warningID, message, component, parentType) { +function warnAndMonitorForKeyUse(message, element, parentType) { var ownerName = getCurrentOwnerDisplayName(); - var parentName = parentType.displayName; + var parentName = typeof parentType === 'string' ? + parentType : parentType.displayName || parentType.name; var useName = ownerName || parentName; - var memoizer = ownerHasKeyUseWarning[warningID]; + var memoizer = ownerHasKeyUseWarning[message] || ( + (ownerHasKeyUseWarning[message] = {}) + ); if (memoizer.hasOwnProperty(useName)) { return; } memoizer[useName] = true; - message += ownerName ? - (" Check the render method of " + ownerName + ".") : - (" Check the renderComponent call using <" + parentName + ">."); + var parentOrOwnerAddendum = + ownerName ? (" Check the render method of " + ownerName + ".") : + parentName ? (" Check the React.render call using <" + parentName + ">.") : + ''; // Usually the current owner is the offender, but if it accepts children as a // property, it may be the creator of the child that's responsible for // assigning it a key. - var childOwnerName = null; - if (component._owner && component._owner !== ReactCurrentOwner.current) { + var childOwnerAddendum = ''; + if (element && + element._owner && + element._owner !== ReactCurrentOwner.current) { // Name of the component that originally created this child. - childOwnerName = component._owner.constructor.displayName; + var childOwnerName = getName(element._owner); - message += (" It was passed a child from " + childOwnerName + "."); + childOwnerAddendum = (" It was passed a child from " + childOwnerName + "."); } - message += ' See http://fb.me/react-warning-keys for more information.'; - monitorCodeUse(warningID, { - component: useName, - componentOwner: childOwnerName - }); - console.warn(message); -} - -/** - * Log that we're using an object map. We're considering deprecating this - * feature and replace it with proper Map and ImmutableMap data structures. - * - * @internal - */ -function monitorUseOfObjectMap() { - var currentName = getCurrentOwnerDisplayName() || ''; - if (ownerHasMonitoredObjectMap.hasOwnProperty(currentName)) { - return; - } - ownerHasMonitoredObjectMap[currentName] = true; - monitorCodeUse('react_object_map_children'); + ("production" !== process.env.NODE_ENV ? warning( + false, + message + '%s%s See http://fb.me/react-warning-keys for more information.', + parentOrOwnerAddendum, + childOwnerAddendum + ) : null); } /** - * Ensure that every component either is passed in a static location, in an + * Ensure that every element either is passed in a static location, in an * array with an explicit keys property defined, or in an object literal * with valid key property. * * @internal - * @param {*} component Statically passed child of any type. - * @param {*} parentType component's parent's type. - * @return {boolean} + * @param {ReactNode} node Statically passed child of any type. + * @param {*} parentType node's parent's type. */ -function validateChildKeys(component, parentType) { - if (Array.isArray(component)) { - for (var i = 0; i < component.length; i++) { - var child = component[i]; +function validateChildKeys(node, parentType) { + if (Array.isArray(node)) { + for (var i = 0; i < node.length; i++) { + var child = node[i]; if (ReactElement.isValidElement(child)) { validateExplicitKey(child, parentType); } } - } else if (ReactElement.isValidElement(component)) { - // This component was passed in a valid location. - component._store.validated = true; - } else if (component && typeof component === 'object') { - monitorUseOfObjectMap(); - for (var name in component) { - validatePropertyKey(name, component[name], parentType); + } else if (ReactElement.isValidElement(node)) { + // This element was passed in a valid location. + node._store.validated = true; + } else if (node) { + var iteratorFn = getIteratorFn(node); + // Entry iterators provide implicit keys. + if (iteratorFn) { + if (iteratorFn !== node.entries) { + var iterator = iteratorFn.call(node); + var step; + while (!(step = iterator.next()).done) { + if (ReactElement.isValidElement(step.value)) { + validateExplicitKey(step.value, parentType); + } + } + } + } else if (typeof node === 'object') { + var fragment = ReactFragment.extractIfFragment(node); + for (var key in fragment) { + if (fragment.hasOwnProperty(key)) { + validatePropertyKey(key, fragment[key], parentType); + } + } } } } @@ -13322,6 +14139,16 @@ function checkPropTypes(componentName, propTypes, props, location) { // fail the render phase where it didn't fail before. So we log it. // After these have been cleaned up, we'll let them throw. try { + // This is intentionally an invariant that gets caught. It's the same + // behavior as without this statement except with a better message. + ("production" !== process.env.NODE_ENV ? invariant( + typeof propTypes[propName] === 'function', + '%s: %s type `%s` is invalid; it must be a function, usually from ' + + 'React.PropTypes.', + componentName || 'React class', + ReactPropTypeLocationNames[location], + propName + ) : invariant(typeof propTypes[propName] === 'function')); error = propTypes[propName](props, propName, componentName, location); } catch (ex) { error = ex; @@ -13330,19 +14157,148 @@ function checkPropTypes(componentName, propTypes, props, location) { // Only monitor this failure once because there tends to be a lot of the // same error. loggedTypeFailures[error.message] = true; - // This will soon use the warning module - monitorCodeUse( - 'react_failed_descriptor_type_check', - { message: error.message } - ); + + var addendum = getDeclarationErrorAddendum(this); + ("production" !== process.env.NODE_ENV ? warning(false, 'Failed propType: %s%s', error.message, addendum) : null); } } } } +var warnedPropsMutations = {}; + +/** + * Warn about mutating props when setting `propName` on `element`. + * + * @param {string} propName The string key within props that was set + * @param {ReactElement} element + */ +function warnForPropsMutation(propName, element) { + var type = element.type; + var elementName = typeof type === 'string' ? type : type.displayName; + var ownerName = element._owner ? + element._owner.getPublicInstance().constructor.displayName : null; + + var warningKey = propName + '|' + elementName + '|' + ownerName; + if (warnedPropsMutations.hasOwnProperty(warningKey)) { + return; + } + warnedPropsMutations[warningKey] = true; + + var elementInfo = ''; + if (elementName) { + elementInfo = ' <' + elementName + ' />'; + } + var ownerInfo = ''; + if (ownerName) { + ownerInfo = ' The element was created by ' + ownerName + '.'; + } + + ("production" !== process.env.NODE_ENV ? warning( + false, + 'Don\'t set .props.%s of the React component%s. ' + + 'Instead, specify the correct value when ' + + 'initially creating the element.%s', + propName, + elementInfo, + ownerInfo + ) : null); +} + +// Inline Object.is polyfill +function is(a, b) { + if (a !== a) { + // NaN + return b !== b; + } + if (a === 0 && b === 0) { + // +-0 + return 1 / a === 1 / b; + } + return a === b; +} + +/** + * Given an element, check if its props have been mutated since element + * creation (or the last call to this function). In particular, check if any + * new props have been added, which we can't directly catch by defining warning + * properties on the props object. + * + * @param {ReactElement} element + */ +function checkAndWarnForMutatedProps(element) { + if (!element._store) { + // Element was created using `new ReactElement` directly or with + // `ReactElement.createElement`; skip mutation checking + return; + } + + var originalProps = element._store.originalProps; + var props = element.props; + + for (var propName in props) { + if (props.hasOwnProperty(propName)) { + if (!originalProps.hasOwnProperty(propName) || + !is(originalProps[propName], props[propName])) { + warnForPropsMutation(propName, element); + + // Copy over the new value so that the two props objects match again + originalProps[propName] = props[propName]; + } + } + } +} + +/** + * Given an element, validate that its props follow the propTypes definition, + * provided by the type. + * + * @param {ReactElement} element + */ +function validatePropTypes(element) { + if (element.type == null) { + // This has already warned. Don't throw. + return; + } + // Extract the component class from the element. Converts string types + // to a composite class which may have propTypes. + // TODO: Validating a string's propTypes is not decoupled from the + // rendering target which is problematic. + var componentClass = ReactNativeComponent.getComponentClassForElement( + element + ); + var name = componentClass.displayName || componentClass.name; + if (componentClass.propTypes) { + checkPropTypes( + name, + componentClass.propTypes, + element.props, + ReactPropTypeLocations.prop + ); + } + if (typeof componentClass.getDefaultProps === 'function') { + ("production" !== process.env.NODE_ENV ? warning( + componentClass.getDefaultProps.isReactClassApproved, + 'getDefaultProps is only used on classic React.createClass ' + + 'definitions. Use a static property named `defaultProps` instead.' + ) : null); + } +} + var ReactElementValidator = { + checkAndWarnForMutatedProps: checkAndWarnForMutatedProps, + createElement: function(type, props, children) { + // We warn in this case but don't throw. We expect the element creation to + // succeed and there will likely be errors in render. + ("production" !== process.env.NODE_ENV ? warning( + type != null, + 'React.createElement: type should not be null or undefined. It should ' + + 'be a string (for DOM elements) or a ReactClass (for composite ' + + 'components).' + ) : null); + var element = ReactElement.createElement.apply(this, arguments); // The result can be nullish if a mock or a custom function is used. @@ -13355,23 +14311,8 @@ var ReactElementValidator = { validateChildKeys(arguments[i], type); } - var name = type.displayName; - if (type.propTypes) { - checkPropTypes( - name, - type.propTypes, - element.props, - ReactPropTypeLocations.prop - ); - } - if (type.contextTypes) { - checkPropTypes( - name, - type.contextTypes, - element._context, - ReactPropTypeLocations.context - ); - } + validatePropTypes(element); + return element; }, @@ -13380,18 +14321,56 @@ var ReactElementValidator = { null, type ); + // Legacy hook TODO: Warn if this is accessed validatedFactory.type = type; + + if ("production" !== process.env.NODE_ENV) { + try { + Object.defineProperty( + validatedFactory, + 'type', + { + enumerable: false, + get: function() { + ("production" !== process.env.NODE_ENV ? warning( + false, + 'Factory.type is deprecated. Access the class directly ' + + 'before passing it to createFactory.' + ) : null); + Object.defineProperty(this, 'type', { + value: type + }); + return type; + } + } + ); + } catch (x) { + // IE will fail on defineProperty (es5-shim/sham too) + } + } + + return validatedFactory; + }, + + cloneElement: function(element, props, children) { + var newElement = ReactElement.cloneElement.apply(this, arguments); + for (var i = 2; i < arguments.length; i++) { + validateChildKeys(arguments[i], newElement.type); + } + validatePropTypes(newElement); + return newElement; } }; module.exports = ReactElementValidator; -},{"./ReactCurrentOwner":83,"./ReactElement":99,"./ReactPropTypeLocations":119,"./monitorCodeUse":192}],101:[function(require,module,exports){ +}).call(this,require('_process')) +},{"./ReactCurrentOwner":85,"./ReactElement":103,"./ReactFragment":109,"./ReactNativeComponent":120,"./ReactPropTypeLocationNames":124,"./ReactPropTypeLocations":125,"./getIteratorFn":182,"./invariant":191,"./warning":212,"_process":1}],105:[function(require,module,exports){ (function (process){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -13401,16 +14380,17 @@ module.exports = ReactElementValidator; * @providesModule ReactEmptyComponent */ -"use strict"; +'use strict'; var ReactElement = require("./ReactElement"); +var ReactInstanceMap = require("./ReactInstanceMap"); var invariant = require("./invariant"); var component; // This registry keeps track of the React IDs of the components that rendered to // `null` (in reality a placeholder such as `noscript`) -var nullComponentIdsRegistry = {}; +var nullComponentIDsRegistry = {}; var ReactEmptyComponentInjection = { injectEmptyComponent: function(emptyComponent) { @@ -13418,24 +14398,43 @@ var ReactEmptyComponentInjection = { } }; -/** - * @return {ReactComponent} component The injected empty component. - */ -function getEmptyComponent() { +var ReactEmptyComponentType = function() {}; +ReactEmptyComponentType.prototype.componentDidMount = function() { + var internalInstance = ReactInstanceMap.get(this); + // TODO: Make sure we run these methods in the correct order, we shouldn't + // need this check. We're going to assume if we're here it means we ran + // componentWillUnmount already so there is no internal instance (it gets + // removed as part of the unmounting process). + if (!internalInstance) { + return; + } + registerNullComponentID(internalInstance._rootNodeID); +}; +ReactEmptyComponentType.prototype.componentWillUnmount = function() { + var internalInstance = ReactInstanceMap.get(this); + // TODO: Get rid of this check. See TODO in componentDidMount. + if (!internalInstance) { + return; + } + deregisterNullComponentID(internalInstance._rootNodeID); +}; +ReactEmptyComponentType.prototype.render = function() { ("production" !== process.env.NODE_ENV ? invariant( component, 'Trying to return null from a render, but no null placeholder component ' + 'was injected.' ) : invariant(component)); return component(); -} +}; + +var emptyElement = ReactElement.createElement(ReactEmptyComponentType); /** * Mark the component as having rendered to null. * @param {string} id Component's `_rootNodeID`. */ function registerNullComponentID(id) { - nullComponentIdsRegistry[id] = true; + nullComponentIDsRegistry[id] = true; } /** @@ -13443,7 +14442,7 @@ function registerNullComponentID(id) { * @param {string} id Component's `_rootNodeID`. */ function deregisterNullComponentID(id) { - delete nullComponentIdsRegistry[id]; + delete nullComponentIDsRegistry[id]; } /** @@ -13451,23 +14450,21 @@ function deregisterNullComponentID(id) { * @return {boolean} True if the component is rendered to null. */ function isNullComponentID(id) { - return nullComponentIdsRegistry[id]; + return !!nullComponentIDsRegistry[id]; } var ReactEmptyComponent = { - deregisterNullComponentID: deregisterNullComponentID, - getEmptyComponent: getEmptyComponent, + emptyElement: emptyElement, injection: ReactEmptyComponentInjection, - isNullComponentID: isNullComponentID, - registerNullComponentID: registerNullComponentID + isNullComponentID: isNullComponentID }; module.exports = ReactEmptyComponent; }).call(this,require('_process')) -},{"./ReactElement":99,"./invariant":182,"_process":1}],102:[function(require,module,exports){ +},{"./ReactElement":103,"./ReactInstanceMap":113,"./invariant":191,"_process":1}],106:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -13497,9 +14494,9 @@ var ReactErrorUtils = { module.exports = ReactErrorUtils; -},{}],103:[function(require,module,exports){ +},{}],107:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -13509,7 +14506,7 @@ module.exports = ReactErrorUtils; * @providesModule ReactEventEmitterMixin */ -"use strict"; +'use strict'; var EventPluginHub = require("./EventPluginHub"); @@ -13547,9 +14544,9 @@ var ReactEventEmitterMixin = { module.exports = ReactEventEmitterMixin; -},{"./EventPluginHub":60}],104:[function(require,module,exports){ +},{"./EventPluginHub":58}],108:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -13560,7 +14557,7 @@ module.exports = ReactEventEmitterMixin; * @typechecks static-only */ -"use strict"; +'use strict'; var EventListener = require("./EventListener"); var ExecutionEnvironment = require("./ExecutionEnvironment"); @@ -13673,7 +14670,7 @@ var ReactEventListener = { trapBubbledEvent: function(topLevelType, handlerBaseName, handle) { var element = handle; if (!element) { - return; + return null; } return EventListener.listen( element, @@ -13695,7 +14692,7 @@ var ReactEventListener = { trapCapturedEvent: function(topLevelType, handlerBaseName, handle) { var element = handle; if (!element) { - return; + return null; } return EventListener.capture( element, @@ -13707,7 +14704,6 @@ var ReactEventListener = { monitorScrollValue: function(refresh) { var callback = scrollValueMonitor.bind(null, refresh); EventListener.listen(window, 'scroll', callback); - EventListener.listen(window, 'resize', callback); }, dispatchEvent: function(topLevelType, nativeEvent) { @@ -13731,9 +14727,194 @@ var ReactEventListener = { module.exports = ReactEventListener; -},{"./EventListener":59,"./ExecutionEnvironment":64,"./Object.assign":70,"./PooledClass":71,"./ReactInstanceHandles":107,"./ReactMount":111,"./ReactUpdates":132,"./getEventTarget":173,"./getUnboundedScrollPosition":178}],105:[function(require,module,exports){ +},{"./EventListener":57,"./ExecutionEnvironment":62,"./Object.assign":69,"./PooledClass":70,"./ReactInstanceHandles":112,"./ReactMount":117,"./ReactUpdates":140,"./getEventTarget":181,"./getUnboundedScrollPosition":187}],109:[function(require,module,exports){ +(function (process){ +/** + * Copyright 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * +* @providesModule ReactFragment +*/ + +'use strict'; + +var ReactElement = require("./ReactElement"); + +var warning = require("./warning"); + +/** + * We used to allow keyed objects to serve as a collection of ReactElements, + * or nested sets. This allowed us a way to explicitly key a set a fragment of + * components. This is now being replaced with an opaque data structure. + * The upgrade path is to call React.addons.createFragment({ key: value }) to + * create a keyed fragment. The resulting data structure is opaque, for now. + */ + +if ("production" !== process.env.NODE_ENV) { + var fragmentKey = '_reactFragment'; + var didWarnKey = '_reactDidWarn'; + var canWarnForReactFragment = false; + + try { + // Feature test. Don't even try to issue this warning if we can't use + // enumerable: false. + + var dummy = function() { + return 1; + }; + + Object.defineProperty( + {}, + fragmentKey, + {enumerable: false, value: true} + ); + + Object.defineProperty( + {}, + 'key', + {enumerable: true, get: dummy} + ); + + canWarnForReactFragment = true; + } catch (x) { } + + var proxyPropertyAccessWithWarning = function(obj, key) { + Object.defineProperty(obj, key, { + enumerable: true, + get: function() { + ("production" !== process.env.NODE_ENV ? warning( + this[didWarnKey], + 'A ReactFragment is an opaque type. Accessing any of its ' + + 'properties is deprecated. Pass it to one of the React.Children ' + + 'helpers.' + ) : null); + this[didWarnKey] = true; + return this[fragmentKey][key]; + }, + set: function(value) { + ("production" !== process.env.NODE_ENV ? warning( + this[didWarnKey], + 'A ReactFragment is an immutable opaque type. Mutating its ' + + 'properties is deprecated.' + ) : null); + this[didWarnKey] = true; + this[fragmentKey][key] = value; + } + }); + }; + + var issuedWarnings = {}; + + var didWarnForFragment = function(fragment) { + // We use the keys and the type of the value as a heuristic to dedupe the + // warning to avoid spamming too much. + var fragmentCacheKey = ''; + for (var key in fragment) { + fragmentCacheKey += key + ':' + (typeof fragment[key]) + ','; + } + var alreadyWarnedOnce = !!issuedWarnings[fragmentCacheKey]; + issuedWarnings[fragmentCacheKey] = true; + return alreadyWarnedOnce; + }; +} + +var ReactFragment = { + // Wrap a keyed object in an opaque proxy that warns you if you access any + // of its properties. + create: function(object) { + if ("production" !== process.env.NODE_ENV) { + if (typeof object !== 'object' || !object || Array.isArray(object)) { + ("production" !== process.env.NODE_ENV ? warning( + false, + 'React.addons.createFragment only accepts a single object.', + object + ) : null); + return object; + } + if (ReactElement.isValidElement(object)) { + ("production" !== process.env.NODE_ENV ? warning( + false, + 'React.addons.createFragment does not accept a ReactElement ' + + 'without a wrapper object.' + ) : null); + return object; + } + if (canWarnForReactFragment) { + var proxy = {}; + Object.defineProperty(proxy, fragmentKey, { + enumerable: false, + value: object + }); + Object.defineProperty(proxy, didWarnKey, { + writable: true, + enumerable: false, + value: false + }); + for (var key in object) { + proxyPropertyAccessWithWarning(proxy, key); + } + Object.preventExtensions(proxy); + return proxy; + } + } + return object; + }, + // Extract the original keyed object from the fragment opaque type. Warn if + // a plain object is passed here. + extract: function(fragment) { + if ("production" !== process.env.NODE_ENV) { + if (canWarnForReactFragment) { + if (!fragment[fragmentKey]) { + ("production" !== process.env.NODE_ENV ? warning( + didWarnForFragment(fragment), + 'Any use of a keyed object should be wrapped in ' + + 'React.addons.createFragment(object) before being passed as a ' + + 'child.' + ) : null); + return fragment; + } + return fragment[fragmentKey]; + } + } + return fragment; + }, + // Check if this is a fragment and if so, extract the keyed object. If it + // is a fragment-like object, warn that it should be wrapped. Ignore if we + // can't determine what kind of object this is. + extractIfFragment: function(fragment) { + if ("production" !== process.env.NODE_ENV) { + if (canWarnForReactFragment) { + // If it is the opaque type, return the keyed object. + if (fragment[fragmentKey]) { + return fragment[fragmentKey]; + } + // Otherwise, check each property if it has an element, if it does + // it is probably meant as a fragment, so we can warn early. Defer, + // the warning to extract. + for (var key in fragment) { + if (fragment.hasOwnProperty(key) && + ReactElement.isValidElement(fragment[key])) { + // This looks like a fragment object, we should provide an + // early warning. + return ReactFragment.extract(fragment); + } + } + } + } + return fragment; + } +}; + +module.exports = ReactFragment; + +}).call(this,require('_process')) +},{"./ReactElement":103,"./warning":212,"_process":1}],110:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -13743,22 +14924,24 @@ module.exports = ReactEventListener; * @providesModule ReactInjection */ -"use strict"; +'use strict'; var DOMProperty = require("./DOMProperty"); var EventPluginHub = require("./EventPluginHub"); -var ReactComponent = require("./ReactComponent"); -var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactComponentEnvironment = require("./ReactComponentEnvironment"); +var ReactClass = require("./ReactClass"); var ReactEmptyComponent = require("./ReactEmptyComponent"); var ReactBrowserEventEmitter = require("./ReactBrowserEventEmitter"); var ReactNativeComponent = require("./ReactNativeComponent"); +var ReactDOMComponent = require("./ReactDOMComponent"); var ReactPerf = require("./ReactPerf"); var ReactRootIndex = require("./ReactRootIndex"); var ReactUpdates = require("./ReactUpdates"); var ReactInjection = { - Component: ReactComponent.injection, - CompositeComponent: ReactCompositeComponent.injection, + Component: ReactComponentEnvironment.injection, + Class: ReactClass.injection, + DOMComponent: ReactDOMComponent.injection, DOMProperty: DOMProperty.injection, EmptyComponent: ReactEmptyComponent.injection, EventPluginHub: EventPluginHub.injection, @@ -13771,9 +14954,9 @@ var ReactInjection = { module.exports = ReactInjection; -},{"./DOMProperty":53,"./EventPluginHub":60,"./ReactBrowserEventEmitter":74,"./ReactComponent":78,"./ReactCompositeComponent":81,"./ReactEmptyComponent":101,"./ReactNativeComponent":114,"./ReactPerf":116,"./ReactRootIndex":123,"./ReactUpdates":132}],106:[function(require,module,exports){ +},{"./DOMProperty":51,"./EventPluginHub":58,"./ReactBrowserEventEmitter":73,"./ReactClass":78,"./ReactComponentEnvironment":81,"./ReactDOMComponent":88,"./ReactEmptyComponent":105,"./ReactNativeComponent":120,"./ReactPerf":122,"./ReactRootIndex":131,"./ReactUpdates":140}],111:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -13783,7 +14966,7 @@ module.exports = ReactInjection; * @providesModule ReactInputSelection */ -"use strict"; +'use strict'; var ReactDOMSelection = require("./ReactDOMSelection"); @@ -13805,9 +14988,8 @@ var ReactInputSelection = { hasSelectionCapabilities: function(elem) { return elem && ( - (elem.nodeName === 'INPUT' && elem.type === 'text') || - elem.nodeName === 'TEXTAREA' || - elem.contentEditable === 'true' + ((elem.nodeName === 'INPUT' && elem.type === 'text') || + elem.nodeName === 'TEXTAREA' || elem.contentEditable === 'true') ); }, @@ -13907,10 +15089,10 @@ var ReactInputSelection = { module.exports = ReactInputSelection; -},{"./ReactDOMSelection":93,"./containsNode":156,"./focusNode":167,"./getActiveElement":169}],107:[function(require,module,exports){ +},{"./ReactDOMSelection":96,"./containsNode":164,"./focusNode":175,"./getActiveElement":177}],112:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -13921,7 +15103,7 @@ module.exports = ReactInputSelection; * @typechecks static-only */ -"use strict"; +'use strict'; var ReactRootIndex = require("./ReactRootIndex"); @@ -14026,7 +15208,8 @@ function getNextDescendantID(ancestorID, destinationID) { // Skip over the ancestor and the immediate separator. Traverse until we hit // another separator or we reach the end of `destinationID`. var start = ancestorID.length + SEPARATOR_LENGTH; - for (var i = start; i < destinationID.length; i++) { + var i; + for (i = start; i < destinationID.length; i++) { if (isBoundary(destinationID, i)) { break; } @@ -14242,256 +15425,95 @@ var ReactInstanceHandles = { module.exports = ReactInstanceHandles; }).call(this,require('_process')) -},{"./ReactRootIndex":123,"./invariant":182,"_process":1}],108:[function(require,module,exports){ -(function (process){ +},{"./ReactRootIndex":131,"./invariant":191,"_process":1}],113:[function(require,module,exports){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @providesModule ReactLegacyElement + * @providesModule ReactInstanceMap */ -"use strict"; - -var ReactCurrentOwner = require("./ReactCurrentOwner"); - -var invariant = require("./invariant"); -var monitorCodeUse = require("./monitorCodeUse"); -var warning = require("./warning"); - -var legacyFactoryLogs = {}; -function warnForLegacyFactoryCall() { - if (!ReactLegacyElementFactory._isLegacyCallWarningEnabled) { - return; - } - var owner = ReactCurrentOwner.current; - var name = owner && owner.constructor ? owner.constructor.displayName : ''; - if (!name) { - name = 'Something'; - } - if (legacyFactoryLogs.hasOwnProperty(name)) { - return; - } - legacyFactoryLogs[name] = true; - ("production" !== process.env.NODE_ENV ? warning( - false, - name + ' is calling a React component directly. ' + - 'Use a factory or JSX instead. See: http://fb.me/react-legacyfactory' - ) : null); - monitorCodeUse('react_legacy_factory_call', { version: 3, name: name }); -} - -function warnForPlainFunctionType(type) { - var isReactClass = - type.prototype && - typeof type.prototype.mountComponent === 'function' && - typeof type.prototype.receiveComponent === 'function'; - if (isReactClass) { - ("production" !== process.env.NODE_ENV ? warning( - false, - 'Did not expect to get a React class here. Use `Component` instead ' + - 'of `Component.type` or `this.constructor`.' - ) : null); - } else { - if (!type._reactWarnedForThisType) { - try { - type._reactWarnedForThisType = true; - } catch (x) { - // just incase this is a frozen object or some special object - } - monitorCodeUse( - 'react_non_component_in_jsx', - { version: 3, name: type.name } - ); - } - ("production" !== process.env.NODE_ENV ? warning( - false, - 'This JSX uses a plain function. Only React components are ' + - 'valid in React\'s JSX transform.' - ) : null); - } -} - -function warnForNonLegacyFactory(type) { - ("production" !== process.env.NODE_ENV ? warning( - false, - 'Do not pass React.DOM.' + type.type + ' to JSX or createFactory. ' + - 'Use the string "' + type.type + '" instead.' - ) : null); -} +'use strict'; /** - * Transfer static properties from the source to the target. Functions are - * rebound to have this reflect the original source. + * `ReactInstanceMap` maintains a mapping from a public facing stateful + * instance (key) and the internal representation (value). This allows public + * methods to accept the user facing instance as an argument and map them back + * to internal methods. */ -function proxyStaticMethods(target, source) { - if (typeof source !== 'function') { - return; - } - for (var key in source) { - if (source.hasOwnProperty(key)) { - var value = source[key]; - if (typeof value === 'function') { - var bound = value.bind(source); - // Copy any properties defined on the function, such as `isRequired` on - // a PropTypes validator. - for (var k in value) { - if (value.hasOwnProperty(k)) { - bound[k] = value[k]; - } - } - target[key] = bound; - } else { - target[key] = value; - } - } - } -} -// We use an object instead of a boolean because booleans are ignored by our -// mocking libraries when these factories gets mocked. -var LEGACY_MARKER = {}; -var NON_LEGACY_MARKER = {}; +// TODO: Replace this with ES6: var ReactInstanceMap = new Map(); +var ReactInstanceMap = { -var ReactLegacyElementFactory = {}; - -ReactLegacyElementFactory.wrapCreateFactory = function(createFactory) { - var legacyCreateFactory = function(type) { - if (typeof type !== 'function') { - // Non-function types cannot be legacy factories - return createFactory(type); - } + /** + * This API should be called `delete` but we'd have to make sure to always + * transform these to strings for IE support. When this transform is fully + * supported we can rename it. + */ + remove: function(key) { + key._reactInternalInstance = undefined; + }, - if (type.isReactNonLegacyFactory) { - // This is probably a factory created by ReactDOM we unwrap it to get to - // the underlying string type. It shouldn't have been passed here so we - // warn. - if ("production" !== process.env.NODE_ENV) { - warnForNonLegacyFactory(type); - } - return createFactory(type.type); - } + get: function(key) { + return key._reactInternalInstance; + }, - if (type.isReactLegacyFactory) { - // This is probably a legacy factory created by ReactCompositeComponent. - // We unwrap it to get to the underlying class. - return createFactory(type.type); - } + has: function(key) { + return key._reactInternalInstance !== undefined; + }, - if ("production" !== process.env.NODE_ENV) { - warnForPlainFunctionType(type); - } + set: function(key, value) { + key._reactInternalInstance = value; + } - // Unless it's a legacy factory, then this is probably a plain function, - // that is expecting to be invoked by JSX. We can just return it as is. - return type; - }; - return legacyCreateFactory; }; -ReactLegacyElementFactory.wrapCreateElement = function(createElement) { - var legacyCreateElement = function(type, props, children) { - if (typeof type !== 'function') { - // Non-function types cannot be legacy factories - return createElement.apply(this, arguments); - } - - var args; +module.exports = ReactInstanceMap; - if (type.isReactNonLegacyFactory) { - // This is probably a factory created by ReactDOM we unwrap it to get to - // the underlying string type. It shouldn't have been passed here so we - // warn. - if ("production" !== process.env.NODE_ENV) { - warnForNonLegacyFactory(type); - } - args = Array.prototype.slice.call(arguments, 0); - args[0] = type.type; - return createElement.apply(this, args); - } - - if (type.isReactLegacyFactory) { - // This is probably a legacy factory created by ReactCompositeComponent. - // We unwrap it to get to the underlying class. - if (type._isMockFunction) { - // If this is a mock function, people will expect it to be called. We - // will actually call the original mock factory function instead. This - // future proofs unit testing that assume that these are classes. - type.type._mockedReactClassConstructor = type; - } - args = Array.prototype.slice.call(arguments, 0); - args[0] = type.type; - return createElement.apply(this, args); - } - - if ("production" !== process.env.NODE_ENV) { - warnForPlainFunctionType(type); - } - - // This is being called with a plain function we should invoke it - // immediately as if this was used with legacy JSX. - return type.apply(null, Array.prototype.slice.call(arguments, 1)); - }; - return legacyCreateElement; -}; - -ReactLegacyElementFactory.wrapFactory = function(factory) { - ("production" !== process.env.NODE_ENV ? invariant( - typeof factory === 'function', - 'This is suppose to accept a element factory' - ) : invariant(typeof factory === 'function')); - var legacyElementFactory = function(config, children) { - // This factory should not be called when JSX is used. Use JSX instead. - if ("production" !== process.env.NODE_ENV) { - warnForLegacyFactoryCall(); - } - return factory.apply(this, arguments); - }; - proxyStaticMethods(legacyElementFactory, factory.type); - legacyElementFactory.isReactLegacyFactory = LEGACY_MARKER; - legacyElementFactory.type = factory.type; - return legacyElementFactory; -}; +},{}],114:[function(require,module,exports){ +/** + * Copyright 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactLifeCycle + */ -// This is used to mark a factory that will remain. E.g. we're allowed to call -// it as a function. However, you're not suppose to pass it to createElement -// or createFactory, so it will warn you if you do. -ReactLegacyElementFactory.markNonLegacyFactory = function(factory) { - factory.isReactNonLegacyFactory = NON_LEGACY_MARKER; - return factory; -}; +'use strict'; -// Checks if a factory function is actually a legacy factory pretending to -// be a class. -ReactLegacyElementFactory.isValidFactory = function(factory) { - // TODO: This will be removed and moved into a class validator or something. - return typeof factory === 'function' && - factory.isReactLegacyFactory === LEGACY_MARKER; -}; +/** + * This module manages the bookkeeping when a component is in the process + * of being mounted or being unmounted. This is used as a way to enforce + * invariants (or warnings) when it is not recommended to call + * setState/forceUpdate. + * + * currentlyMountingInstance: During the construction phase, it is not possible + * to trigger an update since the instance is not fully mounted yet. However, we + * currently allow this as a convenience for mutating the initial state. + * + * currentlyUnmountingInstance: During the unmounting phase, the instance is + * still mounted and can therefore schedule an update. However, this is not + * recommended and probably an error since it's about to be unmounted. + * Therefore we still want to trigger in an error for that case. + */ -ReactLegacyElementFactory.isValidClass = function(factory) { - if ("production" !== process.env.NODE_ENV) { - ("production" !== process.env.NODE_ENV ? warning( - false, - 'isValidClass is deprecated and will be removed in a future release. ' + - 'Use a more specific validator instead.' - ) : null); - } - return ReactLegacyElementFactory.isValidFactory(factory); +var ReactLifeCycle = { + currentlyMountingInstance: null, + currentlyUnmountingInstance: null }; -ReactLegacyElementFactory._isLegacyCallWarningEnabled = true; +module.exports = ReactLifeCycle; -module.exports = ReactLegacyElementFactory; - -}).call(this,require('_process')) -},{"./ReactCurrentOwner":83,"./invariant":182,"./monitorCodeUse":192,"./warning":202,"_process":1}],109:[function(require,module,exports){ +},{}],115:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -14502,7 +15524,7 @@ module.exports = ReactLegacyElementFactory; * @typechecks static-only */ -"use strict"; +'use strict'; /** * ReactLink encapsulates a common pattern in which a component wants to modify @@ -14562,9 +15584,9 @@ ReactLink.PropTypes = { module.exports = ReactLink; -},{"./React":72}],110:[function(require,module,exports){ +},{"./React":71}],116:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -14574,7 +15596,7 @@ module.exports = ReactLink; * @providesModule ReactMarkupChecksum */ -"use strict"; +'use strict'; var adler32 = require("./adler32"); @@ -14610,10 +15632,10 @@ var ReactMarkupChecksum = { module.exports = ReactMarkupChecksum; -},{"./adler32":152}],111:[function(require,module,exports){ +},{"./adler32":160}],117:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -14623,28 +15645,31 @@ module.exports = ReactMarkupChecksum; * @providesModule ReactMount */ -"use strict"; +'use strict'; var DOMProperty = require("./DOMProperty"); var ReactBrowserEventEmitter = require("./ReactBrowserEventEmitter"); var ReactCurrentOwner = require("./ReactCurrentOwner"); var ReactElement = require("./ReactElement"); -var ReactLegacyElement = require("./ReactLegacyElement"); +var ReactElementValidator = require("./ReactElementValidator"); +var ReactEmptyComponent = require("./ReactEmptyComponent"); var ReactInstanceHandles = require("./ReactInstanceHandles"); +var ReactInstanceMap = require("./ReactInstanceMap"); +var ReactMarkupChecksum = require("./ReactMarkupChecksum"); var ReactPerf = require("./ReactPerf"); +var ReactReconciler = require("./ReactReconciler"); +var ReactUpdateQueue = require("./ReactUpdateQueue"); +var ReactUpdates = require("./ReactUpdates"); +var emptyObject = require("./emptyObject"); var containsNode = require("./containsNode"); -var deprecated = require("./deprecated"); var getReactRootElementInContainer = require("./getReactRootElementInContainer"); var instantiateReactComponent = require("./instantiateReactComponent"); var invariant = require("./invariant"); +var setInnerHTML = require("./setInnerHTML"); var shouldUpdateReactComponent = require("./shouldUpdateReactComponent"); var warning = require("./warning"); -var createElement = ReactLegacyElement.wrapCreateElement( - ReactElement.createElement -); - var SEPARATOR = ReactInstanceHandles.SEPARATOR; var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME; @@ -14668,6 +15693,22 @@ if ("production" !== process.env.NODE_ENV) { var findComponentRootReusableArray = []; /** + * Finds the index of the first character + * that's not common between the two given strings. + * + * @return {number} the index of the character where the strings diverge + */ +function firstDifferenceIndex(string1, string2) { + var minLen = Math.min(string1.length, string2.length); + for (var i = 0; i < minLen; i++) { + if (string1.charAt(i) !== string2.charAt(i)) { + return i; + } + } + return string1.length === string2.length ? -1 : minLen; +} + +/** * @param {DOMElement} container DOM element that may contain a React component. * @return {?string} A "reactRoot" ID, if a React component is rendered. */ @@ -14745,6 +15786,24 @@ function getNode(id) { } /** + * Finds the node with the supplied public React instance. + * + * @param {*} instance A public React instance. + * @return {?DOMElement} DOM node with the suppled `id`. + * @internal + */ +function getNodeFromInstance(instance) { + var id = ReactInstanceMap.get(instance)._rootNodeID; + if (ReactEmptyComponent.isNullComponentID(id)) { + return null; + } + if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) { + nodeCache[id] = ReactMount.findReactNodeByID(id); + } + return nodeCache[id]; +} + +/** * A node is "valid" if it is contained by a currently mounted container. * * This means that the node does not have to be contained by a document in @@ -14808,7 +15867,55 @@ function findDeepestCachedAncestor(targetID) { } /** - * Mounting is the process of initializing a React component by creatings its + * Mounts this component and inserts it into the DOM. + * + * @param {ReactComponent} componentInstance The instance to mount. + * @param {string} rootID DOM ID of the root node. + * @param {DOMElement} container DOM element to mount into. + * @param {ReactReconcileTransaction} transaction + * @param {boolean} shouldReuseMarkup If true, do not insert markup + */ +function mountComponentIntoNode( + componentInstance, + rootID, + container, + transaction, + shouldReuseMarkup) { + var markup = ReactReconciler.mountComponent( + componentInstance, rootID, transaction, emptyObject + ); + componentInstance._isTopLevel = true; + ReactMount._mountImageIntoNode(markup, container, shouldReuseMarkup); +} + +/** + * Batched mount. + * + * @param {ReactComponent} componentInstance The instance to mount. + * @param {string} rootID DOM ID of the root node. + * @param {DOMElement} container DOM element to mount into. + * @param {boolean} shouldReuseMarkup If true, do not insert markup + */ +function batchedMountComponentIntoNode( + componentInstance, + rootID, + container, + shouldReuseMarkup) { + var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); + transaction.perform( + mountComponentIntoNode, + null, + componentInstance, + rootID, + container, + transaction, + shouldReuseMarkup + ); + ReactUpdates.ReactReconcileTransaction.release(transaction); +} + +/** + * Mounting is the process of initializing a React component by creating its * representative DOM elements and inserting them into a supplied `container`. * Any prior content inside `container` is destroyed in the process. * @@ -14844,18 +15951,24 @@ var ReactMount = { /** * Take a component that's already mounted into the DOM and replace its props * @param {ReactComponent} prevComponent component instance already in the DOM - * @param {ReactComponent} nextComponent component instance to render + * @param {ReactElement} nextElement component instance to render * @param {DOMElement} container container to render into * @param {?function} callback function triggered on completion */ _updateRootComponent: function( prevComponent, - nextComponent, + nextElement, container, callback) { - var nextProps = nextComponent.props; + if ("production" !== process.env.NODE_ENV) { + ReactElementValidator.checkAndWarnForMutatedProps(nextElement); + } + ReactMount.scrollMonitor(container, function() { - prevComponent.replaceProps(nextProps, callback); + ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement); + if (callback) { + ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback); + } }); if ("production" !== process.env.NODE_ENV) { @@ -14877,13 +15990,11 @@ var ReactMount = { _registerComponent: function(nextComponent, container) { ("production" !== process.env.NODE_ENV ? invariant( container && ( - container.nodeType === ELEMENT_NODE_TYPE || - container.nodeType === DOC_NODE_TYPE + (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE) ), '_registerComponent(...): Target container is not a DOM element.' ) : invariant(container && ( - container.nodeType === ELEMENT_NODE_TYPE || - container.nodeType === DOC_NODE_TYPE + (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE) ))); ReactBrowserEventEmitter.ensureScrollValueMonitoring(); @@ -14895,49 +16006,53 @@ var ReactMount = { /** * Render a new component into the DOM. - * @param {ReactComponent} nextComponent component instance to render + * @param {ReactElement} nextElement element to render * @param {DOMElement} container container to render into * @param {boolean} shouldReuseMarkup if we should skip the markup insertion * @return {ReactComponent} nextComponent */ - _renderNewRootComponent: ReactPerf.measure( - 'ReactMount', - '_renderNewRootComponent', - function( - nextComponent, - container, - shouldReuseMarkup) { - // Various parts of our code (such as ReactCompositeComponent's - // _renderValidatedComponent) assume that calls to render aren't nested; - // verify that that's the case. - ("production" !== process.env.NODE_ENV ? warning( - ReactCurrentOwner.current == null, - '_renderNewRootComponent(): Render methods should be a pure function ' + - 'of props and state; triggering nested component updates from ' + - 'render is not allowed. If necessary, trigger nested updates in ' + - 'componentDidUpdate.' - ) : null); + _renderNewRootComponent: function( + nextElement, + container, + shouldReuseMarkup + ) { + // Various parts of our code (such as ReactCompositeComponent's + // _renderValidatedComponent) assume that calls to render aren't nested; + // verify that that's the case. + ("production" !== process.env.NODE_ENV ? warning( + ReactCurrentOwner.current == null, + '_renderNewRootComponent(): Render methods should be a pure function ' + + 'of props and state; triggering nested component updates from ' + + 'render is not allowed. If necessary, trigger nested updates in ' + + 'componentDidUpdate.' + ) : null); - var componentInstance = instantiateReactComponent(nextComponent, null); - var reactRootID = ReactMount._registerComponent( - componentInstance, - container - ); - componentInstance.mountComponentIntoNode( - reactRootID, - container, - shouldReuseMarkup - ); + var componentInstance = instantiateReactComponent(nextElement, null); + var reactRootID = ReactMount._registerComponent( + componentInstance, + container + ); - if ("production" !== process.env.NODE_ENV) { - // Record the root element in case it later gets transplanted. - rootElementsByReactRootID[reactRootID] = - getReactRootElementInContainer(container); - } + // The initial render is synchronous but any updates that happen during + // rendering, in componentWillMount or componentDidMount, will be batched + // according to the current batching strategy. + + ReactUpdates.batchedUpdates( + batchedMountComponentIntoNode, + componentInstance, + reactRootID, + container, + shouldReuseMarkup + ); - return componentInstance; + if ("production" !== process.env.NODE_ENV) { + // Record the root element in case it later gets transplanted. + rootElementsByReactRootID[reactRootID] = + getReactRootElementInContainer(container); } - ), + + return componentInstance; + }, /** * Renders a React component into the DOM in the supplied `container`. @@ -14954,16 +16069,16 @@ var ReactMount = { render: function(nextElement, container, callback) { ("production" !== process.env.NODE_ENV ? invariant( ReactElement.isValidElement(nextElement), - 'renderComponent(): Invalid component element.%s', + 'React.render(): Invalid component element.%s', ( typeof nextElement === 'string' ? ' Instead of passing an element string, make sure to instantiate ' + 'it by passing it to React.createElement.' : - ReactLegacyElement.isValidFactory(nextElement) ? + typeof nextElement === 'function' ? ' Instead of passing a component class, make sure to instantiate ' + 'it by passing it to React.createElement.' : - // Check if it quacks like a element - typeof nextElement.props !== "undefined" ? + // Check if it quacks like an element + nextElement != null && nextElement.props !== undefined ? ' This may be caused by unintentionally loading two independent ' + 'copies of React.' : '' @@ -14980,7 +16095,7 @@ var ReactMount = { nextElement, container, callback - ); + ).getPublicInstance(); } else { ReactMount.unmountComponentAtNode(container); } @@ -14990,14 +16105,35 @@ var ReactMount = { var containerHasReactMarkup = reactRootElement && ReactMount.isRenderedByReact(reactRootElement); + if ("production" !== process.env.NODE_ENV) { + if (!containerHasReactMarkup || reactRootElement.nextSibling) { + var rootElementSibling = reactRootElement; + while (rootElementSibling) { + if (ReactMount.isRenderedByReact(rootElementSibling)) { + ("production" !== process.env.NODE_ENV ? warning( + false, + 'render(): Target node has markup rendered by React, but there ' + + 'are unrelated nodes as well. This is most commonly caused by ' + + 'white-space inserted around server-rendered markup.' + ) : null); + break; + } + + rootElementSibling = rootElementSibling.nextSibling; + } + } + } + var shouldReuseMarkup = containerHasReactMarkup && !prevComponent; var component = ReactMount._renderNewRootComponent( nextElement, container, shouldReuseMarkup - ); - callback && callback.call(component); + ).getPublicInstance(); + if (callback) { + callback.call(component); + } return component; }, @@ -15011,7 +16147,7 @@ var ReactMount = { * @return {ReactComponent} Component instance rendered in `container`. */ constructAndRenderComponent: function(constructor, props, container) { - var element = createElement(constructor, props); + var element = ReactElement.createElement(constructor, props); return ReactMount.render(element, container); }, @@ -15076,6 +16212,15 @@ var ReactMount = { 'componentDidUpdate.' ) : null); + ("production" !== process.env.NODE_ENV ? invariant( + container && ( + (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE) + ), + 'unmountComponentAtNode(...): Target container is not a DOM element.' + ) : invariant(container && ( + (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE) + ))); + var reactRootID = getReactRootID(container); var component = instancesByReactRootID[reactRootID]; if (!component) { @@ -15100,7 +16245,7 @@ var ReactMount = { * @see {ReactMount.unmountComponentAtNode} */ unmountComponentFromNode: function(instance, container) { - instance.unmountComponent(); + ReactReconciler.unmountComponent(instance); if (container.nodeType === DOC_NODE_TYPE) { container = container.documentElement; @@ -15144,10 +16289,11 @@ var ReactMount = { // warning is when the container is empty. rootElementsByReactRootID[reactRootID] = containerChild; } else { - console.warn( + ("production" !== process.env.NODE_ENV ? warning( + false, 'ReactMount: Root element has been removed from its original ' + 'container. New container:', rootElement.parentNode - ); + ) : null); } } } @@ -15280,6 +16426,77 @@ var ReactMount = { ) : invariant(false)); }, + _mountImageIntoNode: function(markup, container, shouldReuseMarkup) { + ("production" !== process.env.NODE_ENV ? invariant( + container && ( + (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE) + ), + 'mountComponentIntoNode(...): Target container is not valid.' + ) : invariant(container && ( + (container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE) + ))); + + if (shouldReuseMarkup) { + var rootElement = getReactRootElementInContainer(container); + if (ReactMarkupChecksum.canReuseMarkup(markup, rootElement)) { + return; + } else { + var checksum = rootElement.getAttribute( + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + ); + rootElement.removeAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME); + + var rootMarkup = rootElement.outerHTML; + rootElement.setAttribute( + ReactMarkupChecksum.CHECKSUM_ATTR_NAME, + checksum + ); + + var diffIndex = firstDifferenceIndex(markup, rootMarkup); + var difference = ' (client) ' + + markup.substring(diffIndex - 20, diffIndex + 20) + + '\n (server) ' + rootMarkup.substring(diffIndex - 20, diffIndex + 20); + + ("production" !== process.env.NODE_ENV ? invariant( + container.nodeType !== DOC_NODE_TYPE, + 'You\'re trying to render a component to the document using ' + + 'server rendering but the checksum was invalid. This usually ' + + 'means you rendered a different component type or props on ' + + 'the client from the one on the server, or your render() ' + + 'methods are impure. React cannot handle this case due to ' + + 'cross-browser quirks by rendering at the document root. You ' + + 'should look for environment dependent code in your components ' + + 'and ensure the props are the same client and server side:\n%s', + difference + ) : invariant(container.nodeType !== DOC_NODE_TYPE)); + + if ("production" !== process.env.NODE_ENV) { + ("production" !== process.env.NODE_ENV ? warning( + false, + 'React attempted to reuse markup in a container but the ' + + 'checksum was invalid. This generally means that you are ' + + 'using server rendering and the markup generated on the ' + + 'server was not what the client was expecting. React injected ' + + 'new markup to compensate which works but you have lost many ' + + 'of the benefits of server rendering. Instead, figure out ' + + 'why the markup being generated is different on the client ' + + 'or server:\n%s', + difference + ) : null); + } + } + } + + ("production" !== process.env.NODE_ENV ? invariant( + container.nodeType !== DOC_NODE_TYPE, + 'You\'re trying to render a component to the document but ' + + 'you didn\'t use server rendering. We can\'t do this ' + + 'without using server rendering due to cross-browser quirks. ' + + 'See React.renderToString() for server rendering.' + ) : invariant(container.nodeType !== DOC_NODE_TYPE)); + + setInnerHTML(container, markup); + }, /** * React ID utilities. @@ -15293,24 +16510,22 @@ var ReactMount = { getNode: getNode, + getNodeFromInstance: getNodeFromInstance, + purgeID: purgeID }; -// Deprecations (remove for 0.13) -ReactMount.renderComponent = deprecated( - 'ReactMount', - 'renderComponent', - 'render', - this, - ReactMount.render -); +ReactPerf.measureMethods(ReactMount, 'ReactMount', { + _renderNewRootComponent: '_renderNewRootComponent', + _mountImageIntoNode: '_mountImageIntoNode' +}); module.exports = ReactMount; }).call(this,require('_process')) -},{"./DOMProperty":53,"./ReactBrowserEventEmitter":74,"./ReactCurrentOwner":83,"./ReactElement":99,"./ReactInstanceHandles":107,"./ReactLegacyElement":108,"./ReactPerf":116,"./containsNode":156,"./deprecated":162,"./getReactRootElementInContainer":176,"./instantiateReactComponent":181,"./invariant":182,"./shouldUpdateReactComponent":198,"./warning":202,"_process":1}],112:[function(require,module,exports){ +},{"./DOMProperty":51,"./ReactBrowserEventEmitter":73,"./ReactCurrentOwner":85,"./ReactElement":103,"./ReactElementValidator":104,"./ReactEmptyComponent":105,"./ReactInstanceHandles":112,"./ReactInstanceMap":113,"./ReactMarkupChecksum":116,"./ReactPerf":122,"./ReactReconciler":129,"./ReactUpdateQueue":139,"./ReactUpdates":140,"./containsNode":164,"./emptyObject":171,"./getReactRootElementInContainer":185,"./instantiateReactComponent":190,"./invariant":191,"./setInnerHTML":205,"./shouldUpdateReactComponent":208,"./warning":212,"_process":1}],118:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -15321,14 +16536,13 @@ module.exports = ReactMount; * @typechecks static-only */ -"use strict"; +'use strict'; -var ReactComponent = require("./ReactComponent"); +var ReactComponentEnvironment = require("./ReactComponentEnvironment"); var ReactMultiChildUpdateTypes = require("./ReactMultiChildUpdateTypes"); -var flattenChildren = require("./flattenChildren"); -var instantiateReactComponent = require("./instantiateReactComponent"); -var shouldUpdateReactComponent = require("./shouldUpdateReactComponent"); +var ReactReconciler = require("./ReactReconciler"); +var ReactChildReconciler = require("./ReactChildReconciler"); /** * Updating children of a component may trigger recursive updates. The depth is @@ -15446,7 +16660,7 @@ function enqueueTextContent(parentID, textContent) { */ function processQueue() { if (updateQueue.length) { - ReactComponent.BackendIDOperations.dangerouslyProcessChildrenUpdates( + ReactComponentEnvironment.processChildrenUpdates( updateQueue, markupQueue ); @@ -15489,26 +16703,25 @@ var ReactMultiChild = { * @return {array} An array of mounted representations. * @internal */ - mountChildren: function(nestedChildren, transaction) { - var children = flattenChildren(nestedChildren); + mountChildren: function(nestedChildren, transaction, context) { + var children = ReactChildReconciler.instantiateChildren( + nestedChildren, transaction, context + ); + this._renderedChildren = children; var mountImages = []; var index = 0; - this._renderedChildren = children; for (var name in children) { - var child = children[name]; if (children.hasOwnProperty(name)) { - // The rendered children must be turned into instances as they're - // mounted. - var childInstance = instantiateReactComponent(child, null); - children[name] = childInstance; + var child = children[name]; // Inlined for performance, see `ReactInstanceHandles.createReactID`. var rootID = this._rootNodeID + name; - var mountImage = childInstance.mountComponent( + var mountImage = ReactReconciler.mountComponent( + child, rootID, transaction, - this._mountDepth + 1 + context ); - childInstance._mountIndex = index; + child._mountIndex = index; mountImages.push(mountImage); index++; } @@ -15528,6 +16741,8 @@ var ReactMultiChild = { try { var prevChildren = this._renderedChildren; // Remove any rendered children. + ReactChildReconciler.unmountChildren(prevChildren); + // TODO: The setTextContent operation should be enough for (var name in prevChildren) { if (prevChildren.hasOwnProperty(name)) { this._unmountChildByName(prevChildren[name], name); @@ -15539,7 +16754,11 @@ var ReactMultiChild = { } finally { updateDepth--; if (!updateDepth) { - errorThrown ? clearQueue() : processQueue(); + if (errorThrown) { + clearQueue(); + } else { + processQueue(); + } } } }, @@ -15551,17 +16770,22 @@ var ReactMultiChild = { * @param {ReactReconcileTransaction} transaction * @internal */ - updateChildren: function(nextNestedChildren, transaction) { + updateChildren: function(nextNestedChildren, transaction, context) { updateDepth++; var errorThrown = true; try { - this._updateChildren(nextNestedChildren, transaction); + this._updateChildren(nextNestedChildren, transaction, context); errorThrown = false; } finally { updateDepth--; if (!updateDepth) { - errorThrown ? clearQueue() : processQueue(); + if (errorThrown) { + clearQueue(); + } else { + processQueue(); + } } + } }, @@ -15574,9 +16798,12 @@ var ReactMultiChild = { * @final * @protected */ - _updateChildren: function(nextNestedChildren, transaction) { - var nextChildren = flattenChildren(nextNestedChildren); + _updateChildren: function(nextNestedChildren, transaction, context) { var prevChildren = this._renderedChildren; + var nextChildren = ReactChildReconciler.updateChildren( + prevChildren, nextNestedChildren, transaction, context + ); + this._renderedChildren = nextChildren; if (!nextChildren && !prevChildren) { return; } @@ -15590,12 +16817,10 @@ var ReactMultiChild = { continue; } var prevChild = prevChildren && prevChildren[name]; - var prevElement = prevChild && prevChild._currentElement; - var nextElement = nextChildren[name]; - if (shouldUpdateReactComponent(prevElement, nextElement)) { + var nextChild = nextChildren[name]; + if (prevChild === nextChild) { this.moveChild(prevChild, nextIndex, lastIndex); lastIndex = Math.max(prevChild._mountIndex, lastIndex); - prevChild.receiveComponent(nextElement, transaction); prevChild._mountIndex = nextIndex; } else { if (prevChild) { @@ -15604,12 +16829,8 @@ var ReactMultiChild = { this._unmountChildByName(prevChild, name); } // The child must be instantiated before it's mounted. - var nextChildInstance = instantiateReactComponent( - nextElement, - null - ); this._mountChildByNameAtIndex( - nextChildInstance, name, nextIndex, transaction + nextChild, name, nextIndex, transaction, context ); } nextIndex++; @@ -15617,7 +16838,7 @@ var ReactMultiChild = { // Remove children that are no longer present. for (name in prevChildren) { if (prevChildren.hasOwnProperty(name) && - !(nextChildren && nextChildren[name])) { + !(nextChildren && nextChildren.hasOwnProperty(name))) { this._unmountChildByName(prevChildren[name], name); } } @@ -15631,13 +16852,7 @@ var ReactMultiChild = { */ unmountChildren: function() { var renderedChildren = this._renderedChildren; - for (var name in renderedChildren) { - var renderedChild = renderedChildren[name]; - // TODO: When is this not true? - if (renderedChild.unmountComponent) { - renderedChild.unmountComponent(); - } - } + ReactChildReconciler.unmountChildren(renderedChildren); this._renderedChildren = null; }, @@ -15700,18 +16915,22 @@ var ReactMultiChild = { * @param {ReactReconcileTransaction} transaction * @private */ - _mountChildByNameAtIndex: function(child, name, index, transaction) { + _mountChildByNameAtIndex: function( + child, + name, + index, + transaction, + context) { // Inlined for performance, see `ReactInstanceHandles.createReactID`. var rootID = this._rootNodeID + name; - var mountImage = child.mountComponent( + var mountImage = ReactReconciler.mountComponent( + child, rootID, transaction, - this._mountDepth + 1 + context ); child._mountIndex = index; this.createChild(child, mountImage); - this._renderedChildren = this._renderedChildren || {}; - this._renderedChildren[name] = child; }, /** @@ -15726,8 +16945,6 @@ var ReactMultiChild = { _unmountChildByName: function(child, name) { this.removeChild(child); child._mountIndex = null; - child.unmountComponent(); - delete this._renderedChildren[name]; } } @@ -15736,9 +16953,9 @@ var ReactMultiChild = { module.exports = ReactMultiChild; -},{"./ReactComponent":78,"./ReactMultiChildUpdateTypes":113,"./flattenChildren":166,"./instantiateReactComponent":181,"./shouldUpdateReactComponent":198}],113:[function(require,module,exports){ +},{"./ReactChildReconciler":76,"./ReactComponentEnvironment":81,"./ReactMultiChildUpdateTypes":119,"./ReactReconciler":129}],119:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -15748,7 +16965,7 @@ module.exports = ReactMultiChild; * @providesModule ReactMultiChildUpdateTypes */ -"use strict"; +'use strict'; var keyMirror = require("./keyMirror"); @@ -15769,10 +16986,10 @@ var ReactMultiChildUpdateTypes = keyMirror({ module.exports = ReactMultiChildUpdateTypes; -},{"./keyMirror":188}],114:[function(require,module,exports){ +},{"./keyMirror":197}],120:[function(require,module,exports){ (function (process){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -15782,14 +16999,16 @@ module.exports = ReactMultiChildUpdateTypes; * @providesModule ReactNativeComponent */ -"use strict"; +'use strict'; var assign = require("./Object.assign"); var invariant = require("./invariant"); +var autoGenerateWrapperClass = null; var genericComponentClass = null; // This registry keeps track of wrapper classes around native tags var tagToComponentClass = {}; +var textComponentClass = null; var ReactNativeComponentInjection = { // This accepts a class that receives the tag string. This is a catch all @@ -15797,55 +17016,87 @@ var ReactNativeComponentInjection = { injectGenericComponentClass: function(componentClass) { genericComponentClass = componentClass; }, + // This accepts a text component class that takes the text string to be + // rendered as props. + injectTextComponentClass: function(componentClass) { + textComponentClass = componentClass; + }, // This accepts a keyed object with classes as values. Each key represents a // tag. That particular tag will use this class instead of the generic one. injectComponentClasses: function(componentClasses) { assign(tagToComponentClass, componentClasses); + }, + // Temporary hack since we expect DOM refs to behave like composites, + // for this release. + injectAutoWrapper: function(wrapperFactory) { + autoGenerateWrapperClass = wrapperFactory; } }; /** - * Create an internal class for a specific tag. + * Get a composite component wrapper class for a specific tag. * - * @param {string} tag The tag for which to create an internal instance. - * @param {any} props The props passed to the instance constructor. - * @return {ReactComponent} component The injected empty component. + * @param {ReactElement} element The tag for which to get the class. + * @return {function} The React class constructor function. */ -function createInstanceForTag(tag, props, parentType) { +function getComponentClassForElement(element) { + if (typeof element.type === 'function') { + return element.type; + } + var tag = element.type; var componentClass = tagToComponentClass[tag]; if (componentClass == null) { - ("production" !== process.env.NODE_ENV ? invariant( - genericComponentClass, - 'There is no registered component for the tag %s', - tag - ) : invariant(genericComponentClass)); - return new genericComponentClass(tag, props); - } - if (parentType === tag) { - // Avoid recursion - ("production" !== process.env.NODE_ENV ? invariant( - genericComponentClass, - 'There is no registered component for the tag %s', - tag - ) : invariant(genericComponentClass)); - return new genericComponentClass(tag, props); + tagToComponentClass[tag] = componentClass = autoGenerateWrapperClass(tag); } - // Unwrap legacy factories - return new componentClass.type(props); + return componentClass; +} + +/** + * Get a native internal component class for a specific tag. + * + * @param {ReactElement} element The element to create. + * @return {function} The internal class constructor function. + */ +function createInternalComponent(element) { + ("production" !== process.env.NODE_ENV ? invariant( + genericComponentClass, + 'There is no registered component for the tag %s', + element.type + ) : invariant(genericComponentClass)); + return new genericComponentClass(element.type, element.props); +} + +/** + * @param {ReactText} text + * @return {ReactComponent} + */ +function createInstanceForText(text) { + return new textComponentClass(text); +} + +/** + * @param {ReactComponent} component + * @return {boolean} + */ +function isTextComponent(component) { + return component instanceof textComponentClass; } var ReactNativeComponent = { - createInstanceForTag: createInstanceForTag, - injection: ReactNativeComponentInjection, + getComponentClassForElement: getComponentClassForElement, + createInternalComponent: createInternalComponent, + createInstanceForText: createInstanceForText, + isTextComponent: isTextComponent, + injection: ReactNativeComponentInjection }; module.exports = ReactNativeComponent; }).call(this,require('_process')) -},{"./Object.assign":70,"./invariant":182,"_process":1}],115:[function(require,module,exports){ +},{"./Object.assign":69,"./invariant":191,"_process":1}],121:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -15855,9 +17106,8 @@ module.exports = ReactNativeComponent; * @providesModule ReactOwner */ -"use strict"; +'use strict'; -var emptyObject = require("./emptyObject"); var invariant = require("./invariant"); /** @@ -15899,9 +17149,8 @@ var ReactOwner = { */ isValidOwner: function(object) { return !!( - object && - typeof object.attachRef === 'function' && - typeof object.detachRef === 'function' + (object && + typeof object.attachRef === 'function' && typeof object.detachRef === 'function') ); }, @@ -15946,51 +17195,9 @@ var ReactOwner = { ) : invariant(ReactOwner.isValidOwner(owner))); // Check that `component` is still the current ref because we do not want to // detach the ref if another component stole it. - if (owner.refs[ref] === component) { + if (owner.getPublicInstance().refs[ref] === component.getPublicInstance()) { owner.detachRef(ref); } - }, - - /** - * A ReactComponent must mix this in to have refs. - * - * @lends {ReactOwner.prototype} - */ - Mixin: { - - construct: function() { - this.refs = emptyObject; - }, - - /** - * Lazily allocates the refs object and stores `component` as `ref`. - * - * @param {string} ref Reference name. - * @param {component} component Component to store as `ref`. - * @final - * @private - */ - attachRef: function(ref, component) { - ("production" !== process.env.NODE_ENV ? invariant( - component.isOwnedBy(this), - 'attachRef(%s, ...): Only a component\'s owner can store a ref to it.', - ref - ) : invariant(component.isOwnedBy(this))); - var refs = this.refs === emptyObject ? (this.refs = {}) : this.refs; - refs[ref] = component; - }, - - /** - * Detaches a reference name. - * - * @param {string} ref Name to dereference. - * @final - * @private - */ - detachRef: function(ref) { - delete this.refs[ref]; - } - } }; @@ -15998,10 +17205,10 @@ var ReactOwner = { module.exports = ReactOwner; }).call(this,require('_process')) -},{"./emptyObject":164,"./invariant":182,"_process":1}],116:[function(require,module,exports){ +},{"./invariant":191,"_process":1}],122:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -16012,7 +17219,7 @@ module.exports = ReactOwner; * @typechecks static-only */ -"use strict"; +'use strict'; /** * ReactPerf is a general AOP system designed to measure performance. This @@ -16032,6 +17239,26 @@ var ReactPerf = { storedMeasure: _noMeasure, /** + * @param {object} object + * @param {string} objectName + * @param {object<string>} methodNames + */ + measureMethods: function(object, objectName, methodNames) { + if ("production" !== process.env.NODE_ENV) { + for (var key in methodNames) { + if (!methodNames.hasOwnProperty(key)) { + continue; + } + object[key] = ReactPerf.measure( + objectName, + methodNames[key], + object[key] + ); + } + } + }, + + /** * Use this to wrap methods you want to measure. Zero overhead in production. * * @param {string} objName @@ -16082,10 +17309,9 @@ function _noMeasure(objName, fnName, func) { module.exports = ReactPerf; }).call(this,require('_process')) -},{"_process":1}],117:[function(require,module,exports){ -(function (process){ +},{"_process":1}],123:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -16095,15 +17321,11 @@ module.exports = ReactPerf; * @providesModule ReactPropTransferer */ -"use strict"; +'use strict'; var assign = require("./Object.assign"); var emptyFunction = require("./emptyFunction"); -var invariant = require("./invariant"); var joinClasses = require("./joinClasses"); -var warning = require("./warning"); - -var didWarn = false; /** * Creates a transfer strategy that will merge prop values using the supplied @@ -16182,8 +17404,6 @@ function transferInto(props, newProps) { */ var ReactPropTransferer = { - TransferStrategies: TransferStrategies, - /** * Merge two props objects using TransferStrategies. * @@ -16193,66 +17413,16 @@ var ReactPropTransferer = { */ mergeProps: function(oldProps, newProps) { return transferInto(assign({}, oldProps), newProps); - }, - - /** - * @lends {ReactPropTransferer.prototype} - */ - Mixin: { - - /** - * Transfer props from this component to a target component. - * - * Props that do not have an explicit transfer strategy will be transferred - * only if the target component does not already have the prop set. - * - * This is usually used to pass down props to a returned root component. - * - * @param {ReactElement} element Component receiving the properties. - * @return {ReactElement} The supplied `component`. - * @final - * @protected - */ - transferPropsTo: function(element) { - ("production" !== process.env.NODE_ENV ? invariant( - element._owner === this, - '%s: You can\'t call transferPropsTo() on a component that you ' + - 'don\'t own, %s. This usually means you are calling ' + - 'transferPropsTo() on a component passed in as props or children.', - this.constructor.displayName, - typeof element.type === 'string' ? - element.type : - element.type.displayName - ) : invariant(element._owner === this)); - - if ("production" !== process.env.NODE_ENV) { - if (!didWarn) { - didWarn = true; - ("production" !== process.env.NODE_ENV ? warning( - false, - 'transferPropsTo is deprecated. ' + - 'See http://fb.me/react-transferpropsto for more information.' - ) : null); - } - } - - // Because elements are immutable we have to merge into the existing - // props object rather than clone it. - transferInto(element.props, this.props); - - return element; - } - } + }; module.exports = ReactPropTransferer; -}).call(this,require('_process')) -},{"./Object.assign":70,"./emptyFunction":163,"./invariant":182,"./joinClasses":187,"./warning":202,"_process":1}],118:[function(require,module,exports){ +},{"./Object.assign":69,"./emptyFunction":170,"./joinClasses":196}],124:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -16262,7 +17432,7 @@ module.exports = ReactPropTransferer; * @providesModule ReactPropTypeLocationNames */ -"use strict"; +'use strict'; var ReactPropTypeLocationNames = {}; @@ -16277,9 +17447,9 @@ if ("production" !== process.env.NODE_ENV) { module.exports = ReactPropTypeLocationNames; }).call(this,require('_process')) -},{"_process":1}],119:[function(require,module,exports){ +},{"_process":1}],125:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -16289,7 +17459,7 @@ module.exports = ReactPropTypeLocationNames; * @providesModule ReactPropTypeLocations */ -"use strict"; +'use strict'; var keyMirror = require("./keyMirror"); @@ -16301,9 +17471,9 @@ var ReactPropTypeLocations = keyMirror({ module.exports = ReactPropTypeLocations; -},{"./keyMirror":188}],120:[function(require,module,exports){ +},{"./keyMirror":197}],126:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -16313,12 +17483,12 @@ module.exports = ReactPropTypeLocations; * @providesModule ReactPropTypes */ -"use strict"; +'use strict'; var ReactElement = require("./ReactElement"); +var ReactFragment = require("./ReactFragment"); var ReactPropTypeLocationNames = require("./ReactPropTypeLocationNames"); -var deprecated = require("./deprecated"); var emptyFunction = require("./emptyFunction"); /** @@ -16389,22 +17559,7 @@ var ReactPropTypes = { objectOf: createObjectOfTypeChecker, oneOf: createEnumTypeChecker, oneOfType: createUnionTypeChecker, - shape: createShapeTypeChecker, - - component: deprecated( - 'React.PropTypes', - 'component', - 'element', - this, - elementTypeChecker - ), - renderable: deprecated( - 'React.PropTypes', - 'renderable', - 'node', - this, - nodeTypeChecker - ) + shape: createShapeTypeChecker }; function createChainableTypeChecker(validate) { @@ -16414,10 +17569,11 @@ function createChainableTypeChecker(validate) { var locationName = ReactPropTypeLocationNames[location]; if (isRequired) { return new Error( - ("Required " + locationName + " `" + propName + "` was not specified in ")+ + ("Required " + locationName + " `" + propName + "` was not specified in ") + ("`" + componentName + "`.") ); } + return null; } else { return validate(props, propName, componentName, location); } @@ -16445,12 +17601,13 @@ function createPrimitiveTypeChecker(expectedType) { ("supplied to `" + componentName + "`, expected `" + expectedType + "`.") ); } + return null; } return createChainableTypeChecker(validate); } function createAnyTypeChecker() { - return createChainableTypeChecker(emptyFunction.thatReturns()); + return createChainableTypeChecker(emptyFunction.thatReturns(null)); } function createArrayOfTypeChecker(typeChecker) { @@ -16470,6 +17627,7 @@ function createArrayOfTypeChecker(typeChecker) { return error; } } + return null; } return createChainableTypeChecker(validate); } @@ -16483,6 +17641,7 @@ function createElementTypeChecker() { ("`" + componentName + "`, expected a ReactElement.") ); } + return null; } return createChainableTypeChecker(validate); } @@ -16497,6 +17656,7 @@ function createInstanceTypeChecker(expectedClass) { ("`" + componentName + "`, expected instance of `" + expectedClassName + "`.") ); } + return null; } return createChainableTypeChecker(validate); } @@ -16506,7 +17666,7 @@ function createEnumTypeChecker(expectedValues) { var propValue = props[propName]; for (var i = 0; i < expectedValues.length; i++) { if (propValue === expectedValues[i]) { - return; + return null; } } @@ -16539,6 +17699,7 @@ function createObjectOfTypeChecker(typeChecker) { } } } + return null; } return createChainableTypeChecker(validate); } @@ -16548,7 +17709,7 @@ function createUnionTypeChecker(arrayOfTypeCheckers) { for (var i = 0; i < arrayOfTypeCheckers.length; i++) { var checker = arrayOfTypeCheckers[i]; if (checker(props, propName, componentName, location) == null) { - return; + return null; } } @@ -16570,6 +17731,7 @@ function createNodeChecker() { ("`" + componentName + "`, expected a ReactNode.") ); } + return null; } return createChainableTypeChecker(validate); } @@ -16595,14 +17757,16 @@ function createShapeTypeChecker(shapeTypes) { return error; } } + return null; } - return createChainableTypeChecker(validate, 'expected `object`'); + return createChainableTypeChecker(validate); } function isNode(propValue) { - switch(typeof propValue) { + switch (typeof propValue) { case 'number': case 'string': + case 'undefined': return true; case 'boolean': return !propValue; @@ -16610,9 +17774,10 @@ function isNode(propValue) { if (Array.isArray(propValue)) { return propValue.every(isNode); } - if (ReactElement.isValidElement(propValue)) { + if (propValue === null || ReactElement.isValidElement(propValue)) { return true; } + propValue = ReactFragment.extractIfFragment(propValue); for (var k in propValue) { if (!isNode(propValue[k])) { return false; @@ -16655,9 +17820,9 @@ function getPreciseType(propValue) { module.exports = ReactPropTypes; -},{"./ReactElement":99,"./ReactPropTypeLocationNames":118,"./deprecated":162,"./emptyFunction":163}],121:[function(require,module,exports){ +},{"./ReactElement":103,"./ReactFragment":109,"./ReactPropTypeLocationNames":124,"./emptyFunction":170}],127:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -16667,7 +17832,7 @@ module.exports = ReactPropTypes; * @providesModule ReactPutListenerQueue */ -"use strict"; +'use strict'; var PooledClass = require("./PooledClass"); var ReactBrowserEventEmitter = require("./ReactBrowserEventEmitter"); @@ -16711,9 +17876,9 @@ PooledClass.addPoolingTo(ReactPutListenerQueue); module.exports = ReactPutListenerQueue; -},{"./Object.assign":70,"./PooledClass":71,"./ReactBrowserEventEmitter":74}],122:[function(require,module,exports){ +},{"./Object.assign":69,"./PooledClass":70,"./ReactBrowserEventEmitter":73}],128:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -16724,7 +17889,7 @@ module.exports = ReactPutListenerQueue; * @typechecks static-only */ -"use strict"; +'use strict'; var CallbackQueue = require("./CallbackQueue"); var PooledClass = require("./PooledClass"); @@ -16887,9 +18052,204 @@ PooledClass.addPoolingTo(ReactReconcileTransaction); module.exports = ReactReconcileTransaction; -},{"./CallbackQueue":48,"./Object.assign":70,"./PooledClass":71,"./ReactBrowserEventEmitter":74,"./ReactInputSelection":106,"./ReactPutListenerQueue":121,"./Transaction":149}],123:[function(require,module,exports){ +},{"./CallbackQueue":47,"./Object.assign":69,"./PooledClass":70,"./ReactBrowserEventEmitter":73,"./ReactInputSelection":111,"./ReactPutListenerQueue":127,"./Transaction":157}],129:[function(require,module,exports){ +(function (process){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactReconciler + */ + +'use strict'; + +var ReactRef = require("./ReactRef"); +var ReactElementValidator = require("./ReactElementValidator"); + +/** + * Helper to call ReactRef.attachRefs with this composite component, split out + * to avoid allocations in the transaction mount-ready queue. + */ +function attachRefs() { + ReactRef.attachRefs(this, this._currentElement); +} + +var ReactReconciler = { + + /** + * Initializes the component, renders markup, and registers event listeners. + * + * @param {ReactComponent} internalInstance + * @param {string} rootID DOM ID of the root node. + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction + * @return {?string} Rendered markup to be inserted into the DOM. + * @final + * @internal + */ + mountComponent: function(internalInstance, rootID, transaction, context) { + var markup = internalInstance.mountComponent(rootID, transaction, context); + if ("production" !== process.env.NODE_ENV) { + ReactElementValidator.checkAndWarnForMutatedProps( + internalInstance._currentElement + ); + } + transaction.getReactMountReady().enqueue(attachRefs, internalInstance); + return markup; + }, + + /** + * Releases any resources allocated by `mountComponent`. + * + * @final + * @internal + */ + unmountComponent: function(internalInstance) { + ReactRef.detachRefs(internalInstance, internalInstance._currentElement); + internalInstance.unmountComponent(); + }, + + /** + * Update a component using a new element. + * + * @param {ReactComponent} internalInstance + * @param {ReactElement} nextElement + * @param {ReactReconcileTransaction} transaction + * @param {object} context + * @internal + */ + receiveComponent: function( + internalInstance, nextElement, transaction, context + ) { + var prevElement = internalInstance._currentElement; + + if (nextElement === prevElement && nextElement._owner != null) { + // Since elements are immutable after the owner is rendered, + // we can do a cheap identity compare here to determine if this is a + // superfluous reconcile. It's possible for state to be mutable but such + // change should trigger an update of the owner which would recreate + // the element. We explicitly check for the existence of an owner since + // it's possible for an element created outside a composite to be + // deeply mutated and reused. + return; + } + + if ("production" !== process.env.NODE_ENV) { + ReactElementValidator.checkAndWarnForMutatedProps(nextElement); + } + + var refsChanged = ReactRef.shouldUpdateRefs( + prevElement, + nextElement + ); + + if (refsChanged) { + ReactRef.detachRefs(internalInstance, prevElement); + } + + internalInstance.receiveComponent(nextElement, transaction, context); + + if (refsChanged) { + transaction.getReactMountReady().enqueue(attachRefs, internalInstance); + } + }, + + /** + * Flush any dirty changes in a component. + * + * @param {ReactComponent} internalInstance + * @param {ReactReconcileTransaction} transaction + * @internal + */ + performUpdateIfNecessary: function( + internalInstance, + transaction + ) { + internalInstance.performUpdateIfNecessary(transaction); + } + +}; + +module.exports = ReactReconciler; + +}).call(this,require('_process')) +},{"./ReactElementValidator":104,"./ReactRef":130,"_process":1}],130:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactRef + */ + +'use strict'; + +var ReactOwner = require("./ReactOwner"); + +var ReactRef = {}; + +function attachRef(ref, component, owner) { + if (typeof ref === 'function') { + ref(component.getPublicInstance()); + } else { + // Legacy ref + ReactOwner.addComponentAsRefTo(component, ref, owner); + } +} + +function detachRef(ref, component, owner) { + if (typeof ref === 'function') { + ref(null); + } else { + // Legacy ref + ReactOwner.removeComponentAsRefFrom(component, ref, owner); + } +} + +ReactRef.attachRefs = function(instance, element) { + var ref = element.ref; + if (ref != null) { + attachRef(ref, instance, element._owner); + } +}; + +ReactRef.shouldUpdateRefs = function(prevElement, nextElement) { + // If either the owner or a `ref` has changed, make sure the newest owner + // has stored a reference to `this`, and the previous owner (if different) + // has forgotten the reference to `this`. We use the element instead + // of the public this.props because the post processing cannot determine + // a ref. The ref conceptually lives on the element. + + // TODO: Should this even be possible? The owner cannot change because + // it's forbidden by shouldUpdateReactComponent. The ref can change + // if you swap the keys of but not the refs. Reconsider where this check + // is made. It probably belongs where the key checking and + // instantiateReactComponent is done. + + return ( + nextElement._owner !== prevElement._owner || + nextElement.ref !== prevElement.ref + ); +}; + +ReactRef.detachRefs = function(instance, element) { + var ref = element.ref; + if (ref != null) { + detachRef(ref, instance, element._owner); + } +}; + +module.exports = ReactRef; + +},{"./ReactOwner":121}],131:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -16900,7 +18260,7 @@ module.exports = ReactReconcileTransaction; * @typechecks */ -"use strict"; +'use strict'; var ReactRootIndexInjection = { /** @@ -16918,10 +18278,10 @@ var ReactRootIndex = { module.exports = ReactRootIndex; -},{}],124:[function(require,module,exports){ +},{}],132:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -16931,7 +18291,7 @@ module.exports = ReactRootIndex; * @typechecks static-only * @providesModule ReactServerRendering */ -"use strict"; +'use strict'; var ReactElement = require("./ReactElement"); var ReactInstanceHandles = require("./ReactInstanceHandles"); @@ -16939,6 +18299,7 @@ var ReactMarkupChecksum = require("./ReactMarkupChecksum"); var ReactServerRenderingTransaction = require("./ReactServerRenderingTransaction"); +var emptyObject = require("./emptyObject"); var instantiateReactComponent = require("./instantiateReactComponent"); var invariant = require("./invariant"); @@ -16959,7 +18320,8 @@ function renderToString(element) { return transaction.perform(function() { var componentInstance = instantiateReactComponent(element, null); - var markup = componentInstance.mountComponent(id, transaction, 0); + var markup = + componentInstance.mountComponent(id, transaction, emptyObject); return ReactMarkupChecksum.addChecksumToMarkup(markup); }, null); } finally { @@ -16985,7 +18347,7 @@ function renderToStaticMarkup(element) { return transaction.perform(function() { var componentInstance = instantiateReactComponent(element, null); - return componentInstance.mountComponent(id, transaction, 0); + return componentInstance.mountComponent(id, transaction, emptyObject); }, null); } finally { ReactServerRenderingTransaction.release(transaction); @@ -16998,9 +18360,9 @@ module.exports = { }; }).call(this,require('_process')) -},{"./ReactElement":99,"./ReactInstanceHandles":107,"./ReactMarkupChecksum":110,"./ReactServerRenderingTransaction":125,"./instantiateReactComponent":181,"./invariant":182,"_process":1}],125:[function(require,module,exports){ +},{"./ReactElement":103,"./ReactInstanceHandles":112,"./ReactMarkupChecksum":116,"./ReactServerRenderingTransaction":133,"./emptyObject":171,"./instantiateReactComponent":190,"./invariant":191,"_process":1}],133:[function(require,module,exports){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -17011,7 +18373,7 @@ module.exports = { * @typechecks */ -"use strict"; +'use strict'; var PooledClass = require("./PooledClass"); var CallbackQueue = require("./CallbackQueue"); @@ -17111,9 +18473,9 @@ PooledClass.addPoolingTo(ReactServerRenderingTransaction); module.exports = ReactServerRenderingTransaction; -},{"./CallbackQueue":48,"./Object.assign":70,"./PooledClass":71,"./ReactPutListenerQueue":121,"./Transaction":149,"./emptyFunction":163}],126:[function(require,module,exports){ +},{"./CallbackQueue":47,"./Object.assign":69,"./PooledClass":70,"./ReactPutListenerQueue":127,"./Transaction":157,"./emptyFunction":170}],134:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -17123,7 +18485,7 @@ module.exports = ReactServerRenderingTransaction; * @providesModule ReactStateSetters */ -"use strict"; +'use strict'; var ReactStateSetters = { /** @@ -17217,9 +18579,9 @@ ReactStateSetters.Mixin = { module.exports = ReactStateSetters; -},{}],127:[function(require,module,exports){ +},{}],135:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -17229,16 +18591,19 @@ module.exports = ReactStateSetters; * @providesModule ReactTestUtils */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); var EventPluginHub = require("./EventPluginHub"); var EventPropagators = require("./EventPropagators"); var React = require("./React"); var ReactElement = require("./ReactElement"); +var ReactEmptyComponent = require("./ReactEmptyComponent"); var ReactBrowserEventEmitter = require("./ReactBrowserEventEmitter"); +var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactInstanceHandles = require("./ReactInstanceHandles"); +var ReactInstanceMap = require("./ReactInstanceMap"); var ReactMount = require("./ReactMount"); -var ReactTextComponent = require("./ReactTextComponent"); var ReactUpdates = require("./ReactUpdates"); var SyntheticEvent = require("./SyntheticEvent"); @@ -17275,12 +18640,14 @@ var ReactTestUtils = { isElementOfType: function(inst, convenienceConstructor) { return ( ReactElement.isValidElement(inst) && - inst.type === convenienceConstructor.type + inst.type === convenienceConstructor ); }, isDOMComponent: function(inst) { - return !!(inst && inst.mountComponent && inst.tagName); + // TODO: Fix this heuristic. It's just here because composites can currently + // pretend to be DOM components. + return !!(inst && inst.tagName && inst.getDOMNode); }, isDOMComponentElement: function(inst) { @@ -17296,7 +18663,7 @@ var ReactTestUtils = { isCompositeComponentWithType: function(inst, type) { return !!(ReactTestUtils.isCompositeComponent(inst) && - (inst.constructor === type.type)); + (inst.constructor === type)); }, isCompositeComponentElement: function(inst) { @@ -17317,8 +18684,12 @@ var ReactTestUtils = { (inst.constructor === type)); }, - isTextComponent: function(inst) { - return inst instanceof ReactTextComponent.type; + getRenderedChildOfCompositeComponent: function(inst) { + if (!ReactTestUtils.isCompositeComponent(inst)) { + return null; + } + var internalInstance = ReactInstanceMap.get(inst); + return internalInstance._renderedComponent.getPublicInstance(); }, findAllInRenderedTree: function(inst, test) { @@ -17327,19 +18698,31 @@ var ReactTestUtils = { } var ret = test(inst) ? [inst] : []; if (ReactTestUtils.isDOMComponent(inst)) { - var renderedChildren = inst._renderedChildren; + var internalInstance = ReactInstanceMap.get(inst); + var renderedChildren = internalInstance + ._renderedComponent + ._renderedChildren; var key; for (key in renderedChildren) { if (!renderedChildren.hasOwnProperty(key)) { continue; } + if (!renderedChildren[key].getPublicInstance) { + continue; + } ret = ret.concat( - ReactTestUtils.findAllInRenderedTree(renderedChildren[key], test) + ReactTestUtils.findAllInRenderedTree( + renderedChildren[key].getPublicInstance(), + test + ) ); } } else if (ReactTestUtils.isCompositeComponent(inst)) { ret = ret.concat( - ReactTestUtils.findAllInRenderedTree(inst._renderedComponent, test) + ReactTestUtils.findAllInRenderedTree( + ReactTestUtils.getRenderedChildOfCompositeComponent(inst), + test + ) ); } return ret; @@ -17354,8 +18737,7 @@ var ReactTestUtils = { return ReactTestUtils.findAllInRenderedTree(root, function(inst) { var instClassName = inst.props.className; return ReactTestUtils.isDOMComponent(inst) && ( - instClassName && - (' ' + instClassName + ' ').indexOf(' ' + className + ' ') !== -1 + (instClassName && (' ' + instClassName + ' ').indexOf(' ' + className + ' ') !== -1) ); }); }, @@ -17370,7 +18752,9 @@ var ReactTestUtils = { var all = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, className); if (all.length !== 1) { - throw new Error('Did not find exactly one match for class:' + className); + throw new Error('Did not find exactly one match ' + + '(found: ' + all.length + ') for class:' + className + ); } return all[0]; }, @@ -17451,21 +18835,14 @@ var ReactTestUtils = { mockComponent: function(module, mockTagName) { mockTagName = mockTagName || module.mockTagName || "div"; - var ConvenienceConstructor = React.createClass({displayName: 'ConvenienceConstructor', - render: function() { - return React.createElement( - mockTagName, - null, - this.props.children - ); - } + module.prototype.render.mockImplementation(function() { + return React.createElement( + mockTagName, + null, + this.props.children + ); }); - module.mockImplementation(ConvenienceConstructor); - - module.type = ConvenienceConstructor.type; - module.isReactLegacyFactory = true; - return this; }, @@ -17510,11 +18887,94 @@ var ReactTestUtils = { }; }, + createRenderer: function() { + return new ReactShallowRenderer(); + }, + Simulate: null, SimulateNative: {} }; /** + * @class ReactShallowRenderer + */ +var ReactShallowRenderer = function() { + this._instance = null; +}; + +ReactShallowRenderer.prototype.getRenderOutput = function() { + return ( + (this._instance && this._instance._renderedComponent && + this._instance._renderedComponent._renderedOutput) + || null + ); +}; + +var NoopInternalComponent = function(element) { + this._renderedOutput = element; + this._currentElement = element === null || element === false ? + ReactEmptyComponent.emptyElement : + element; +}; + +NoopInternalComponent.prototype = { + + mountComponent: function() { + }, + + receiveComponent: function(element) { + this._renderedOutput = element; + this._currentElement = element === null || element === false ? + ReactEmptyComponent.emptyElement : + element; + }, + + unmountComponent: function() { + } + +}; + +var ShallowComponentWrapper = function() { }; +assign( + ShallowComponentWrapper.prototype, + ReactCompositeComponent.Mixin, { + _instantiateReactComponent: function(element) { + return new NoopInternalComponent(element); + }, + _replaceNodeWithMarkupByID: function() {}, + _renderValidatedComponent: + ReactCompositeComponent.Mixin. + _renderValidatedComponentWithoutOwnerOrContext + } +); + +ReactShallowRenderer.prototype.render = function(element, context) { + var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); + this._render(element, transaction, context); + ReactUpdates.ReactReconcileTransaction.release(transaction); +}; + +ReactShallowRenderer.prototype.unmount = function() { + if (this._instance) { + this._instance.unmountComponent(); + } +}; + +ReactShallowRenderer.prototype._render = function(element, transaction, context) { + if (!this._instance) { + var rootID = ReactInstanceHandles.createReactRootID(); + var instance = new ShallowComponentWrapper(element.type); + instance.construct(element); + + instance.mountComponent(rootID, transaction, context); + + this._instance = instance; + } else { + this._instance.receiveComponent(element, transaction, context); + } +}; + +/** * Exports: * * - `ReactTestUtils.Simulate.click(Element/ReactDOMComponent)` @@ -17629,115 +19089,9 @@ for (eventType in topLevelTypes) { module.exports = ReactTestUtils; -},{"./EventConstants":58,"./EventPluginHub":60,"./EventPropagators":63,"./Object.assign":70,"./React":72,"./ReactBrowserEventEmitter":74,"./ReactElement":99,"./ReactMount":111,"./ReactTextComponent":128,"./ReactUpdates":132,"./SyntheticEvent":141}],128:[function(require,module,exports){ +},{"./EventConstants":56,"./EventPluginHub":58,"./EventPropagators":61,"./Object.assign":69,"./React":71,"./ReactBrowserEventEmitter":73,"./ReactCompositeComponent":83,"./ReactElement":103,"./ReactEmptyComponent":105,"./ReactInstanceHandles":112,"./ReactInstanceMap":113,"./ReactMount":117,"./ReactUpdates":140,"./SyntheticEvent":149}],136:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactTextComponent - * @typechecks static-only - */ - -"use strict"; - -var DOMPropertyOperations = require("./DOMPropertyOperations"); -var ReactComponent = require("./ReactComponent"); -var ReactElement = require("./ReactElement"); - -var assign = require("./Object.assign"); -var escapeTextForBrowser = require("./escapeTextForBrowser"); - -/** - * Text nodes violate a couple assumptions that React makes about components: - * - * - When mounting text into the DOM, adjacent text nodes are merged. - * - Text nodes cannot be assigned a React root ID. - * - * This component is used to wrap strings in elements so that they can undergo - * the same reconciliation that is applied to elements. - * - * TODO: Investigate representing React components in the DOM with text nodes. - * - * @class ReactTextComponent - * @extends ReactComponent - * @internal - */ -var ReactTextComponent = function(props) { - // This constructor and it's argument is currently used by mocks. -}; - -assign(ReactTextComponent.prototype, ReactComponent.Mixin, { - - /** - * Creates the markup for this text node. This node is not intended to have - * any features besides containing text content. - * - * @param {string} rootID DOM ID of the root node. - * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction - * @param {number} mountDepth number of components in the owner hierarchy - * @return {string} Markup for this text node. - * @internal - */ - mountComponent: function(rootID, transaction, mountDepth) { - ReactComponent.Mixin.mountComponent.call( - this, - rootID, - transaction, - mountDepth - ); - - var escapedText = escapeTextForBrowser(this.props); - - if (transaction.renderToStaticMarkup) { - // Normally we'd wrap this in a `span` for the reasons stated above, but - // since this is a situation where React won't take over (static pages), - // we can simply return the text as it is. - return escapedText; - } - - return ( - '<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' + - escapedText + - '</span>' - ); - }, - - /** - * Updates this component by updating the text content. - * - * @param {object} nextComponent Contains the next text content. - * @param {ReactReconcileTransaction} transaction - * @internal - */ - receiveComponent: function(nextComponent, transaction) { - var nextProps = nextComponent.props; - if (nextProps !== this.props) { - this.props = nextProps; - ReactComponent.BackendIDOperations.updateTextContentByID( - this._rootNodeID, - nextProps - ); - } - } - -}); - -var ReactTextComponentFactory = function(text) { - // Bypass validation and configuration - return new ReactElement(ReactTextComponent, null, null, null, null, text); -}; - -ReactTextComponentFactory.type = ReactTextComponent; - -module.exports = ReactTextComponentFactory; - -},{"./DOMPropertyOperations":54,"./Object.assign":70,"./ReactComponent":78,"./ReactElement":99,"./escapeTextForBrowser":165}],129:[function(require,module,exports){ -/** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -17748,9 +19102,10 @@ module.exports = ReactTextComponentFactory; * @providesModule ReactTransitionChildMapping */ -"use strict"; +'use strict'; var ReactChildren = require("./ReactChildren"); +var ReactFragment = require("./ReactFragment"); var ReactTransitionChildMapping = { /** @@ -17761,9 +19116,12 @@ var ReactTransitionChildMapping = { * @return {object} Mapping of key to child */ getChildMapping: function(children) { - return ReactChildren.map(children, function(child) { + if (!children) { + return children; + } + return ReactFragment.extract(ReactChildren.map(children, function(child) { return child; - }); + })); }, /** @@ -17836,9 +19194,9 @@ var ReactTransitionChildMapping = { module.exports = ReactTransitionChildMapping; -},{"./ReactChildren":77}],130:[function(require,module,exports){ +},{"./ReactChildren":77,"./ReactFragment":109}],137:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -17848,7 +19206,7 @@ module.exports = ReactTransitionChildMapping; * @providesModule ReactTransitionEvents */ -"use strict"; +'use strict'; var ExecutionEnvironment = require("./ExecutionEnvironment"); @@ -17947,9 +19305,9 @@ var ReactTransitionEvents = { module.exports = ReactTransitionEvents; -},{"./ExecutionEnvironment":64}],131:[function(require,module,exports){ +},{"./ExecutionEnvironment":62}],138:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -17959,7 +19317,7 @@ module.exports = ReactTransitionEvents; * @providesModule ReactTransitionGroup */ -"use strict"; +'use strict'; var React = require("./React"); var ReactTransitionChildMapping = require("./ReactTransitionChildMapping"); @@ -17989,6 +19347,21 @@ var ReactTransitionGroup = React.createClass({ }; }, + componentWillMount: function() { + this.currentlyTransitioningKeys = {}; + this.keysToEnter = []; + this.keysToLeave = []; + }, + + componentDidMount: function() { + var initialChildMapping = this.state.children; + for (var key in initialChildMapping) { + if (initialChildMapping[key]) { + this.performAppear(key); + } + } + }, + componentWillReceiveProps: function(nextProps) { var nextChildMapping = ReactTransitionChildMapping.getChildMapping( nextProps.children @@ -18023,12 +19396,6 @@ var ReactTransitionGroup = React.createClass({ // If we want to someday check for reordering, we could do it here. }, - componentWillMount: function() { - this.currentlyTransitioningKeys = {}; - this.keysToEnter = []; - this.keysToLeave = []; - }, - componentDidUpdate: function() { var keysToEnter = this.keysToEnter; this.keysToEnter = []; @@ -18039,6 +19406,38 @@ var ReactTransitionGroup = React.createClass({ keysToLeave.forEach(this.performLeave); }, + performAppear: function(key) { + this.currentlyTransitioningKeys[key] = true; + + var component = this.refs[key]; + + if (component.componentWillAppear) { + component.componentWillAppear( + this._handleDoneAppearing.bind(this, key) + ); + } else { + this._handleDoneAppearing(key); + } + }, + + _handleDoneAppearing: function(key) { + var component = this.refs[key]; + if (component.componentDidAppear) { + component.componentDidAppear(); + } + + delete this.currentlyTransitioningKeys[key]; + + var currentChildMapping = ReactTransitionChildMapping.getChildMapping( + this.props.children + ); + + if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) { + // This was removed before it had fully appeared. Remove it. + this.performLeave(key); + } + }, + performEnter: function(key) { this.currentlyTransitioningKeys[key] = true; @@ -18111,7 +19510,7 @@ var ReactTransitionGroup = React.createClass({ render: function() { // TODO: we could get rid of the need for the wrapper node // by cloning a single child - var childrenToRender = {}; + var childrenToRender = []; for (var key in this.state.children) { var child = this.state.children[key]; if (child) { @@ -18120,10 +19519,10 @@ var ReactTransitionGroup = React.createClass({ // already been removed. In case you need this behavior you can provide // a childFactory function to wrap every child, even the ones that are // leaving. - childrenToRender[key] = cloneWithProps( + childrenToRender.push(cloneWithProps( this.props.childFactory(child), - {ref: key} - ); + {ref: key, key: key} + )); } } return React.createElement( @@ -18136,10 +19535,309 @@ var ReactTransitionGroup = React.createClass({ module.exports = ReactTransitionGroup; -},{"./Object.assign":70,"./React":72,"./ReactTransitionChildMapping":129,"./cloneWithProps":155,"./emptyFunction":163}],132:[function(require,module,exports){ +},{"./Object.assign":69,"./React":71,"./ReactTransitionChildMapping":136,"./cloneWithProps":163,"./emptyFunction":170}],139:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactUpdateQueue + */ + +'use strict'; + +var ReactLifeCycle = require("./ReactLifeCycle"); +var ReactCurrentOwner = require("./ReactCurrentOwner"); +var ReactElement = require("./ReactElement"); +var ReactInstanceMap = require("./ReactInstanceMap"); +var ReactUpdates = require("./ReactUpdates"); + +var assign = require("./Object.assign"); +var invariant = require("./invariant"); +var warning = require("./warning"); + +function enqueueUpdate(internalInstance) { + if (internalInstance !== ReactLifeCycle.currentlyMountingInstance) { + // If we're in a componentWillMount handler, don't enqueue a rerender + // because ReactUpdates assumes we're in a browser context (which is + // wrong for server rendering) and we're about to do a render anyway. + // See bug in #1740. + ReactUpdates.enqueueUpdate(internalInstance); + } +} + +function getInternalInstanceReadyForUpdate(publicInstance, callerName) { + ("production" !== process.env.NODE_ENV ? invariant( + ReactCurrentOwner.current == null, + '%s(...): Cannot update during an existing state transition ' + + '(such as within `render`). Render methods should be a pure function ' + + 'of props and state.', + callerName + ) : invariant(ReactCurrentOwner.current == null)); + + var internalInstance = ReactInstanceMap.get(publicInstance); + if (!internalInstance) { + if ("production" !== process.env.NODE_ENV) { + // Only warn when we have a callerName. Otherwise we should be silent. + // We're probably calling from enqueueCallback. We don't want to warn + // there because we already warned for the corresponding lifecycle method. + ("production" !== process.env.NODE_ENV ? warning( + !callerName, + '%s(...): Can only update a mounted or mounting component. ' + + 'This usually means you called %s() on an unmounted ' + + 'component. This is a no-op.', + callerName, + callerName + ) : null); + } + return null; + } + + if (internalInstance === ReactLifeCycle.currentlyUnmountingInstance) { + return null; + } + + return internalInstance; +} + +/** + * ReactUpdateQueue allows for state updates to be scheduled into a later + * reconciliation step. + */ +var ReactUpdateQueue = { + + /** + * Enqueue a callback that will be executed after all the pending updates + * have processed. + * + * @param {ReactClass} publicInstance The instance to use as `this` context. + * @param {?function} callback Called after state is updated. + * @internal + */ + enqueueCallback: function(publicInstance, callback) { + ("production" !== process.env.NODE_ENV ? invariant( + typeof callback === 'function', + 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + + 'isn\'t callable.' + ) : invariant(typeof callback === 'function')); + var internalInstance = getInternalInstanceReadyForUpdate(publicInstance); + + // Previously we would throw an error if we didn't have an internal + // instance. Since we want to make it a no-op instead, we mirror the same + // behavior we have in other enqueue* methods. + // We also need to ignore callbacks in componentWillMount. See + // enqueueUpdates. + if (!internalInstance || + internalInstance === ReactLifeCycle.currentlyMountingInstance) { + return null; + } + + if (internalInstance._pendingCallbacks) { + internalInstance._pendingCallbacks.push(callback); + } else { + internalInstance._pendingCallbacks = [callback]; + } + // TODO: The callback here is ignored when setState is called from + // componentWillMount. Either fix it or disallow doing so completely in + // favor of getInitialState. Alternatively, we can disallow + // componentWillMount during server-side rendering. + enqueueUpdate(internalInstance); + }, + + enqueueCallbackInternal: function(internalInstance, callback) { + ("production" !== process.env.NODE_ENV ? invariant( + typeof callback === 'function', + 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + + 'isn\'t callable.' + ) : invariant(typeof callback === 'function')); + if (internalInstance._pendingCallbacks) { + internalInstance._pendingCallbacks.push(callback); + } else { + internalInstance._pendingCallbacks = [callback]; + } + enqueueUpdate(internalInstance); + }, + + /** + * Forces an update. This should only be invoked when it is known with + * certainty that we are **not** in a DOM transaction. + * + * You may want to call this when you know that some deeper aspect of the + * component's state has changed but `setState` was not called. + * + * This will not invoke `shouldUpdateComponent`, but it will invoke + * `componentWillUpdate` and `componentDidUpdate`. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @internal + */ + enqueueForceUpdate: function(publicInstance) { + var internalInstance = getInternalInstanceReadyForUpdate( + publicInstance, + 'forceUpdate' + ); + + if (!internalInstance) { + return; + } + + internalInstance._pendingForceUpdate = true; + + enqueueUpdate(internalInstance); + }, + + /** + * Replaces all of the state. Always use this or `setState` to mutate state. + * You should treat `this.state` as immutable. + * + * There is no guarantee that `this.state` will be immediately updated, so + * accessing `this.state` after calling this method may return the old value. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @param {object} completeState Next state. + * @internal + */ + enqueueReplaceState: function(publicInstance, completeState) { + var internalInstance = getInternalInstanceReadyForUpdate( + publicInstance, + 'replaceState' + ); + + if (!internalInstance) { + return; + } + + internalInstance._pendingStateQueue = [completeState]; + internalInstance._pendingReplaceState = true; + + enqueueUpdate(internalInstance); + }, + + /** + * Sets a subset of the state. This only exists because _pendingState is + * internal. This provides a merging strategy that is not available to deep + * properties which is confusing. TODO: Expose pendingState or don't use it + * during the merge. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @param {object} partialState Next partial state to be merged with state. + * @internal + */ + enqueueSetState: function(publicInstance, partialState) { + var internalInstance = getInternalInstanceReadyForUpdate( + publicInstance, + 'setState' + ); + + if (!internalInstance) { + return; + } + + var queue = + internalInstance._pendingStateQueue || + (internalInstance._pendingStateQueue = []); + queue.push(partialState); + + enqueueUpdate(internalInstance); + }, + + /** + * Sets a subset of the props. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @param {object} partialProps Subset of the next props. + * @internal + */ + enqueueSetProps: function(publicInstance, partialProps) { + var internalInstance = getInternalInstanceReadyForUpdate( + publicInstance, + 'setProps' + ); + + if (!internalInstance) { + return; + } + + ("production" !== process.env.NODE_ENV ? invariant( + internalInstance._isTopLevel, + 'setProps(...): You called `setProps` on a ' + + 'component with a parent. This is an anti-pattern since props will ' + + 'get reactively updated when rendered. Instead, change the owner\'s ' + + '`render` method to pass the correct value as props to the component ' + + 'where it is created.' + ) : invariant(internalInstance._isTopLevel)); + + // Merge with the pending element if it exists, otherwise with existing + // element props. + var element = internalInstance._pendingElement || + internalInstance._currentElement; + var props = assign({}, element.props, partialProps); + internalInstance._pendingElement = ReactElement.cloneAndReplaceProps( + element, + props + ); + + enqueueUpdate(internalInstance); + }, + + /** + * Replaces all of the props. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @param {object} props New props. + * @internal + */ + enqueueReplaceProps: function(publicInstance, props) { + var internalInstance = getInternalInstanceReadyForUpdate( + publicInstance, + 'replaceProps' + ); + + if (!internalInstance) { + return; + } + + ("production" !== process.env.NODE_ENV ? invariant( + internalInstance._isTopLevel, + 'replaceProps(...): You called `replaceProps` on a ' + + 'component with a parent. This is an anti-pattern since props will ' + + 'get reactively updated when rendered. Instead, change the owner\'s ' + + '`render` method to pass the correct value as props to the component ' + + 'where it is created.' + ) : invariant(internalInstance._isTopLevel)); + + // Merge with the pending element if it exists, otherwise with existing + // element props. + var element = internalInstance._pendingElement || + internalInstance._currentElement; + internalInstance._pendingElement = ReactElement.cloneAndReplaceProps( + element, + props + ); + + enqueueUpdate(internalInstance); + }, + + enqueueElementInternal: function(internalInstance, newElement) { + internalInstance._pendingElement = newElement; + enqueueUpdate(internalInstance); + } + +}; + +module.exports = ReactUpdateQueue; + +}).call(this,require('_process')) +},{"./Object.assign":69,"./ReactCurrentOwner":85,"./ReactElement":103,"./ReactInstanceMap":113,"./ReactLifeCycle":114,"./ReactUpdates":140,"./invariant":191,"./warning":212,"_process":1}],140:[function(require,module,exports){ +(function (process){ +/** + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -18149,12 +19847,13 @@ module.exports = ReactTransitionGroup; * @providesModule ReactUpdates */ -"use strict"; +'use strict'; var CallbackQueue = require("./CallbackQueue"); var PooledClass = require("./PooledClass"); var ReactCurrentOwner = require("./ReactCurrentOwner"); var ReactPerf = require("./ReactPerf"); +var ReactReconciler = require("./ReactReconciler"); var Transaction = require("./Transaction"); var assign = require("./Object.assign"); @@ -18244,20 +19943,20 @@ assign( PooledClass.addPoolingTo(ReactUpdatesFlushTransaction); -function batchedUpdates(callback, a, b) { +function batchedUpdates(callback, a, b, c, d) { ensureInjected(); - batchingStrategy.batchedUpdates(callback, a, b); + batchingStrategy.batchedUpdates(callback, a, b, c, d); } /** - * Array comparator for ReactComponents by owner depth + * Array comparator for ReactComponents by mount ordering. * * @param {ReactComponent} c1 first component you're comparing * @param {ReactComponent} c2 second component you're comparing * @return {number} Return value usable by Array.prototype.sort(). */ -function mountDepthComparator(c1, c2) { - return c1._mountDepth - c2._mountDepth; +function mountOrderComparator(c1, c2) { + return c1._mountOrder - c2._mountOrder; } function runBatchedUpdates(transaction) { @@ -18273,69 +19972,68 @@ function runBatchedUpdates(transaction) { // Since reconciling a component higher in the owner hierarchy usually (not // always -- see shouldComponentUpdate()) will reconcile children, reconcile // them before their children by sorting the array. - dirtyComponents.sort(mountDepthComparator); + dirtyComponents.sort(mountOrderComparator); for (var i = 0; i < len; i++) { - // If a component is unmounted before pending changes apply, ignore them - // TODO: Queue unmounts in the same list to avoid this happening at all + // If a component is unmounted before pending changes apply, it will still + // be here, but we assume that it has cleared its _pendingCallbacks and + // that performUpdateIfNecessary is a noop. var component = dirtyComponents[i]; - if (component.isMounted()) { - // If performUpdateIfNecessary happens to enqueue any new updates, we - // shouldn't execute the callbacks until the next render happens, so - // stash the callbacks first - var callbacks = component._pendingCallbacks; - component._pendingCallbacks = null; - component.performUpdateIfNecessary(transaction.reconcileTransaction); - - if (callbacks) { - for (var j = 0; j < callbacks.length; j++) { - transaction.callbackQueue.enqueue( - callbacks[j], - component - ); - } + + // If performUpdateIfNecessary happens to enqueue any new updates, we + // shouldn't execute the callbacks until the next render happens, so + // stash the callbacks first + var callbacks = component._pendingCallbacks; + component._pendingCallbacks = null; + + ReactReconciler.performUpdateIfNecessary( + component, + transaction.reconcileTransaction + ); + + if (callbacks) { + for (var j = 0; j < callbacks.length; j++) { + transaction.callbackQueue.enqueue( + callbacks[j], + component.getPublicInstance() + ); } } } } -var flushBatchedUpdates = ReactPerf.measure( - 'ReactUpdates', - 'flushBatchedUpdates', - function() { - // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents - // array and perform any updates enqueued by mount-ready handlers (i.e., - // componentDidUpdate) but we need to check here too in order to catch - // updates enqueued by setState callbacks and asap calls. - while (dirtyComponents.length || asapEnqueued) { - if (dirtyComponents.length) { - var transaction = ReactUpdatesFlushTransaction.getPooled(); - transaction.perform(runBatchedUpdates, null, transaction); - ReactUpdatesFlushTransaction.release(transaction); - } +var flushBatchedUpdates = function() { + // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents + // array and perform any updates enqueued by mount-ready handlers (i.e., + // componentDidUpdate) but we need to check here too in order to catch + // updates enqueued by setState callbacks and asap calls. + while (dirtyComponents.length || asapEnqueued) { + if (dirtyComponents.length) { + var transaction = ReactUpdatesFlushTransaction.getPooled(); + transaction.perform(runBatchedUpdates, null, transaction); + ReactUpdatesFlushTransaction.release(transaction); + } - if (asapEnqueued) { - asapEnqueued = false; - var queue = asapCallbackQueue; - asapCallbackQueue = CallbackQueue.getPooled(); - queue.notifyAll(); - CallbackQueue.release(queue); - } + if (asapEnqueued) { + asapEnqueued = false; + var queue = asapCallbackQueue; + asapCallbackQueue = CallbackQueue.getPooled(); + queue.notifyAll(); + CallbackQueue.release(queue); } } +}; +flushBatchedUpdates = ReactPerf.measure( + 'ReactUpdates', + 'flushBatchedUpdates', + flushBatchedUpdates ); /** * Mark a component as needing a rerender, adding an optional callback to a * list of functions which will be executed once the rerender occurs. */ -function enqueueUpdate(component, callback) { - ("production" !== process.env.NODE_ENV ? invariant( - !callback || typeof callback === "function", - 'enqueueUpdate(...): You called `setProps`, `replaceProps`, ' + - '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + - 'isn\'t callable.' - ) : invariant(!callback || typeof callback === "function")); +function enqueueUpdate(component) { ensureInjected(); // Various parts of our code (such as ReactCompositeComponent's @@ -18352,19 +20050,11 @@ function enqueueUpdate(component, callback) { ) : null); if (!batchingStrategy.isBatchingUpdates) { - batchingStrategy.batchedUpdates(enqueueUpdate, component, callback); + batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); - - if (callback) { - if (component._pendingCallbacks) { - component._pendingCallbacks.push(callback); - } else { - component._pendingCallbacks = [callback]; - } - } } /** @@ -18426,10 +20116,10 @@ var ReactUpdates = { module.exports = ReactUpdates; }).call(this,require('_process')) -},{"./CallbackQueue":48,"./Object.assign":70,"./PooledClass":71,"./ReactCurrentOwner":83,"./ReactPerf":116,"./Transaction":149,"./invariant":182,"./warning":202,"_process":1}],133:[function(require,module,exports){ +},{"./CallbackQueue":47,"./Object.assign":69,"./PooledClass":70,"./ReactCurrentOwner":85,"./ReactPerf":122,"./ReactReconciler":129,"./Transaction":157,"./invariant":191,"./warning":212,"_process":1}],141:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -18446,13 +20136,14 @@ module.exports = ReactUpdates; * place to live inside React core. */ -"use strict"; +'use strict'; var LinkedStateMixin = require("./LinkedStateMixin"); var React = require("./React"); var ReactComponentWithPureRenderMixin = require("./ReactComponentWithPureRenderMixin"); var ReactCSSTransitionGroup = require("./ReactCSSTransitionGroup"); +var ReactFragment = require("./ReactFragment"); var ReactTransitionGroup = require("./ReactTransitionGroup"); var ReactUpdates = require("./ReactUpdates"); @@ -18469,6 +20160,7 @@ React.addons = { batchedUpdates: ReactUpdates.batchedUpdates, classSet: cx, cloneWithProps: cloneWithProps, + createFragment: ReactFragment.create, update: update }; @@ -18480,9 +20172,9 @@ if ("production" !== process.env.NODE_ENV) { module.exports = React; }).call(this,require('_process')) -},{"./LinkedStateMixin":66,"./React":72,"./ReactCSSTransitionGroup":75,"./ReactComponentWithPureRenderMixin":80,"./ReactDefaultPerf":97,"./ReactTestUtils":127,"./ReactTransitionGroup":131,"./ReactUpdates":132,"./cloneWithProps":155,"./cx":160,"./update":201,"_process":1}],134:[function(require,module,exports){ +},{"./LinkedStateMixin":65,"./React":71,"./ReactCSSTransitionGroup":74,"./ReactComponentWithPureRenderMixin":82,"./ReactDefaultPerf":101,"./ReactFragment":109,"./ReactTestUtils":135,"./ReactTransitionGroup":138,"./ReactUpdates":140,"./cloneWithProps":163,"./cx":168,"./update":211,"_process":1}],142:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -18494,7 +20186,7 @@ module.exports = React; /*jslint bitwise: true*/ -"use strict"; +'use strict'; var DOMProperty = require("./DOMProperty"); @@ -18572,9 +20264,9 @@ var SVGDOMPropertyConfig = { module.exports = SVGDOMPropertyConfig; -},{"./DOMProperty":53}],135:[function(require,module,exports){ +},{"./DOMProperty":51}],143:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -18584,7 +20276,7 @@ module.exports = SVGDOMPropertyConfig; * @providesModule SelectEventPlugin */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); var EventPropagators = require("./EventPropagators"); @@ -18669,8 +20361,8 @@ function constructSelectEvent(nativeEvent) { // won't dispatch. if (mouseDown || activeElement == null || - activeElement != getActiveElement()) { - return; + activeElement !== getActiveElement()) { + return null; } // Only fire when selection has actually changed. @@ -18767,9 +20459,9 @@ var SelectEventPlugin = { module.exports = SelectEventPlugin; -},{"./EventConstants":58,"./EventPropagators":63,"./ReactInputSelection":106,"./SyntheticEvent":141,"./getActiveElement":169,"./isTextInputElement":185,"./keyOf":189,"./shallowEqual":197}],136:[function(require,module,exports){ +},{"./EventConstants":56,"./EventPropagators":61,"./ReactInputSelection":111,"./SyntheticEvent":149,"./getActiveElement":177,"./isTextInputElement":194,"./keyOf":198,"./shallowEqual":207}],144:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -18780,7 +20472,7 @@ module.exports = SelectEventPlugin; * @typechecks */ -"use strict"; +'use strict'; /** * Size of the reactRoot ID space. We generate random numbers for React root @@ -18798,10 +20490,10 @@ var ServerReactRootIndex = { module.exports = ServerReactRootIndex; -},{}],137:[function(require,module,exports){ +},{}],145:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -18811,7 +20503,7 @@ module.exports = ServerReactRootIndex; * @providesModule SimpleEventPlugin */ -"use strict"; +'use strict'; var EventConstants = require("./EventConstants"); var EventPluginUtils = require("./EventPluginUtils"); @@ -19087,8 +20779,8 @@ var topLevelEventsToDispatchConfig = { topWheel: eventTypes.wheel }; -for (var topLevelType in topLevelEventsToDispatchConfig) { - topLevelEventsToDispatchConfig[topLevelType].dependencies = [topLevelType]; +for (var type in topLevelEventsToDispatchConfig) { + topLevelEventsToDispatchConfig[type].dependencies = [type]; } var SimpleEventPlugin = { @@ -19226,9 +20918,9 @@ var SimpleEventPlugin = { module.exports = SimpleEventPlugin; }).call(this,require('_process')) -},{"./EventConstants":58,"./EventPluginUtils":62,"./EventPropagators":63,"./SyntheticClipboardEvent":138,"./SyntheticDragEvent":140,"./SyntheticEvent":141,"./SyntheticFocusEvent":142,"./SyntheticKeyboardEvent":144,"./SyntheticMouseEvent":145,"./SyntheticTouchEvent":146,"./SyntheticUIEvent":147,"./SyntheticWheelEvent":148,"./getEventCharCode":170,"./invariant":182,"./keyOf":189,"./warning":202,"_process":1}],138:[function(require,module,exports){ +},{"./EventConstants":56,"./EventPluginUtils":60,"./EventPropagators":61,"./SyntheticClipboardEvent":146,"./SyntheticDragEvent":148,"./SyntheticEvent":149,"./SyntheticFocusEvent":150,"./SyntheticKeyboardEvent":152,"./SyntheticMouseEvent":153,"./SyntheticTouchEvent":154,"./SyntheticUIEvent":155,"./SyntheticWheelEvent":156,"./getEventCharCode":178,"./invariant":191,"./keyOf":198,"./warning":212,"_process":1}],146:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19239,7 +20931,7 @@ module.exports = SimpleEventPlugin; * @typechecks static-only */ -"use strict"; +'use strict'; var SyntheticEvent = require("./SyntheticEvent"); @@ -19271,10 +20963,9 @@ SyntheticEvent.augmentClass(SyntheticClipboardEvent, ClipboardEventInterface); module.exports = SyntheticClipboardEvent; - -},{"./SyntheticEvent":141}],139:[function(require,module,exports){ +},{"./SyntheticEvent":149}],147:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19285,7 +20976,7 @@ module.exports = SyntheticClipboardEvent; * @typechecks static-only */ -"use strict"; +'use strict'; var SyntheticEvent = require("./SyntheticEvent"); @@ -19317,10 +21008,9 @@ SyntheticEvent.augmentClass( module.exports = SyntheticCompositionEvent; - -},{"./SyntheticEvent":141}],140:[function(require,module,exports){ +},{"./SyntheticEvent":149}],148:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19331,7 +21021,7 @@ module.exports = SyntheticCompositionEvent; * @typechecks static-only */ -"use strict"; +'use strict'; var SyntheticMouseEvent = require("./SyntheticMouseEvent"); @@ -19357,9 +21047,9 @@ SyntheticMouseEvent.augmentClass(SyntheticDragEvent, DragEventInterface); module.exports = SyntheticDragEvent; -},{"./SyntheticMouseEvent":145}],141:[function(require,module,exports){ +},{"./SyntheticMouseEvent":153}],149:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19370,7 +21060,7 @@ module.exports = SyntheticDragEvent; * @typechecks static-only */ -"use strict"; +'use strict'; var PooledClass = require("./PooledClass"); @@ -19448,13 +21138,21 @@ assign(SyntheticEvent.prototype, { preventDefault: function() { this.defaultPrevented = true; var event = this.nativeEvent; - event.preventDefault ? event.preventDefault() : event.returnValue = false; + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } this.isDefaultPrevented = emptyFunction.thatReturnsTrue; }, stopPropagation: function() { var event = this.nativeEvent; - event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; + if (event.stopPropagation) { + event.stopPropagation(); + } else { + event.cancelBubble = true; + } this.isPropagationStopped = emptyFunction.thatReturnsTrue; }, @@ -19515,9 +21213,9 @@ PooledClass.addPoolingTo(SyntheticEvent, PooledClass.threeArgumentPooler); module.exports = SyntheticEvent; -},{"./Object.assign":70,"./PooledClass":71,"./emptyFunction":163,"./getEventTarget":173}],142:[function(require,module,exports){ +},{"./Object.assign":69,"./PooledClass":70,"./emptyFunction":170,"./getEventTarget":181}],150:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19528,7 +21226,7 @@ module.exports = SyntheticEvent; * @typechecks static-only */ -"use strict"; +'use strict'; var SyntheticUIEvent = require("./SyntheticUIEvent"); @@ -19554,9 +21252,9 @@ SyntheticUIEvent.augmentClass(SyntheticFocusEvent, FocusEventInterface); module.exports = SyntheticFocusEvent; -},{"./SyntheticUIEvent":147}],143:[function(require,module,exports){ +},{"./SyntheticUIEvent":155}],151:[function(require,module,exports){ /** - * Copyright 2013 Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19567,7 +21265,7 @@ module.exports = SyntheticFocusEvent; * @typechecks static-only */ -"use strict"; +'use strict'; var SyntheticEvent = require("./SyntheticEvent"); @@ -19600,10 +21298,9 @@ SyntheticEvent.augmentClass( module.exports = SyntheticInputEvent; - -},{"./SyntheticEvent":141}],144:[function(require,module,exports){ +},{"./SyntheticEvent":149}],152:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19614,7 +21311,7 @@ module.exports = SyntheticInputEvent; * @typechecks static-only */ -"use strict"; +'use strict'; var SyntheticUIEvent = require("./SyntheticUIEvent"); @@ -19688,9 +21385,9 @@ SyntheticUIEvent.augmentClass(SyntheticKeyboardEvent, KeyboardEventInterface); module.exports = SyntheticKeyboardEvent; -},{"./SyntheticUIEvent":147,"./getEventCharCode":170,"./getEventKey":171,"./getEventModifierState":172}],145:[function(require,module,exports){ +},{"./SyntheticUIEvent":155,"./getEventCharCode":178,"./getEventKey":179,"./getEventModifierState":180}],153:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19701,7 +21398,7 @@ module.exports = SyntheticKeyboardEvent; * @typechecks static-only */ -"use strict"; +'use strict'; var SyntheticUIEvent = require("./SyntheticUIEvent"); var ViewportMetrics = require("./ViewportMetrics"); @@ -19739,9 +21436,7 @@ var MouseEventInterface = { buttons: null, relatedTarget: function(event) { return event.relatedTarget || ( - event.fromElement === event.srcElement ? - event.toElement : - event.fromElement + ((event.fromElement === event.srcElement ? event.toElement : event.fromElement)) ); }, // "Proprietary" Interface. @@ -19771,9 +21466,9 @@ SyntheticUIEvent.augmentClass(SyntheticMouseEvent, MouseEventInterface); module.exports = SyntheticMouseEvent; -},{"./SyntheticUIEvent":147,"./ViewportMetrics":150,"./getEventModifierState":172}],146:[function(require,module,exports){ +},{"./SyntheticUIEvent":155,"./ViewportMetrics":158,"./getEventModifierState":180}],154:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19784,7 +21479,7 @@ module.exports = SyntheticMouseEvent; * @typechecks static-only */ -"use strict"; +'use strict'; var SyntheticUIEvent = require("./SyntheticUIEvent"); @@ -19819,9 +21514,9 @@ SyntheticUIEvent.augmentClass(SyntheticTouchEvent, TouchEventInterface); module.exports = SyntheticTouchEvent; -},{"./SyntheticUIEvent":147,"./getEventModifierState":172}],147:[function(require,module,exports){ +},{"./SyntheticUIEvent":155,"./getEventModifierState":180}],155:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19832,7 +21527,7 @@ module.exports = SyntheticTouchEvent; * @typechecks static-only */ -"use strict"; +'use strict'; var SyntheticEvent = require("./SyntheticEvent"); @@ -19881,9 +21576,9 @@ SyntheticEvent.augmentClass(SyntheticUIEvent, UIEventInterface); module.exports = SyntheticUIEvent; -},{"./SyntheticEvent":141,"./getEventTarget":173}],148:[function(require,module,exports){ +},{"./SyntheticEvent":149,"./getEventTarget":181}],156:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19894,7 +21589,7 @@ module.exports = SyntheticUIEvent; * @typechecks static-only */ -"use strict"; +'use strict'; var SyntheticMouseEvent = require("./SyntheticMouseEvent"); @@ -19942,10 +21637,10 @@ SyntheticMouseEvent.augmentClass(SyntheticWheelEvent, WheelEventInterface); module.exports = SyntheticWheelEvent; -},{"./SyntheticMouseEvent":145}],149:[function(require,module,exports){ +},{"./SyntheticMouseEvent":153}],157:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -19955,7 +21650,7 @@ module.exports = SyntheticWheelEvent; * @providesModule Transaction */ -"use strict"; +'use strict'; var invariant = require("./invariant"); @@ -20007,7 +21702,7 @@ var invariant = require("./invariant"); * content. * - (Future use case): Wrapping particular flushes of the `ReactWorker` queue * to preserve the `scrollTop` (an automatic scroll aware DOM). - * - (Future use case): Layout calculations before and after DOM upates. + * - (Future use case): Layout calculations before and after DOM updates. * * Transactional plugin API: * - A module that has an `initialize` method that returns any precomputation. @@ -20149,8 +21844,8 @@ var Mixin = { // close -- if it's still set to true in the finally block, it means // wrapper.close threw. errorThrown = true; - if (initData !== Transaction.OBSERVED_ERROR) { - wrapper.close && wrapper.close.call(this, initData); + if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) { + wrapper.close.call(this, initData); } errorThrown = false; } finally { @@ -20183,9 +21878,9 @@ var Transaction = { module.exports = Transaction; }).call(this,require('_process')) -},{"./invariant":182,"_process":1}],150:[function(require,module,exports){ +},{"./invariant":191,"_process":1}],158:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20195,9 +21890,7 @@ module.exports = Transaction; * @providesModule ViewportMetrics */ -"use strict"; - -var getUnboundedScrollPosition = require("./getUnboundedScrollPosition"); +'use strict'; var ViewportMetrics = { @@ -20205,8 +21898,7 @@ var ViewportMetrics = { currentScrollTop: 0, - refreshScrollValues: function() { - var scrollPosition = getUnboundedScrollPosition(window); + refreshScrollValues: function(scrollPosition) { ViewportMetrics.currentScrollLeft = scrollPosition.x; ViewportMetrics.currentScrollTop = scrollPosition.y; } @@ -20215,10 +21907,10 @@ var ViewportMetrics = { module.exports = ViewportMetrics; -},{"./getUnboundedScrollPosition":178}],151:[function(require,module,exports){ +},{}],159:[function(require,module,exports){ (function (process){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20228,7 +21920,7 @@ module.exports = ViewportMetrics; * @providesModule accumulateInto */ -"use strict"; +'use strict'; var invariant = require("./invariant"); @@ -20281,9 +21973,9 @@ function accumulateInto(current, next) { module.exports = accumulateInto; }).call(this,require('_process')) -},{"./invariant":182,"_process":1}],152:[function(require,module,exports){ +},{"./invariant":191,"_process":1}],160:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20295,7 +21987,7 @@ module.exports = accumulateInto; /* jslint bitwise:true */ -"use strict"; +'use strict'; var MOD = 65521; @@ -20315,9 +22007,9 @@ function adler32(data) { module.exports = adler32; -},{}],153:[function(require,module,exports){ +},{}],161:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20347,9 +22039,9 @@ function camelize(string) { module.exports = camelize; -},{}],154:[function(require,module,exports){ +},{}],162:[function(require,module,exports){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20389,21 +22081,21 @@ function camelizeStyleName(string) { module.exports = camelizeStyleName; -},{"./camelize":153}],155:[function(require,module,exports){ +},{"./camelize":161}],163:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @typechecks + * @typechecks static-only * @providesModule cloneWithProps */ -"use strict"; +'use strict'; var ReactElement = require("./ReactElement"); var ReactPropTransferer = require("./ReactPropTransferer"); @@ -20417,10 +22109,10 @@ var CHILDREN_PROP = keyOf({children: null}); * Sometimes you want to change the props of a child passed to you. Usually * this is to add a CSS class. * - * @param {object} child child component you'd like to clone - * @param {object} props props you'd like to modify. They will be merged - * as if you used `transferPropsTo()`. - * @return {object} a clone of child with props merged in. + * @param {ReactElement} child child element you'd like to clone + * @param {object} props props you'd like to modify. className and style will be + * merged automatically. + * @return {ReactElement} a clone of child with props merged in. */ function cloneWithProps(child, props) { if ("production" !== process.env.NODE_ENV) { @@ -20448,9 +22140,9 @@ function cloneWithProps(child, props) { module.exports = cloneWithProps; }).call(this,require('_process')) -},{"./ReactElement":99,"./ReactPropTransferer":117,"./keyOf":189,"./warning":202,"_process":1}],156:[function(require,module,exports){ +},{"./ReactElement":103,"./ReactPropTransferer":123,"./keyOf":198,"./warning":212,"_process":1}],164:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20492,16 +22184,16 @@ function containsNode(outerNode, innerNode) { module.exports = containsNode; -},{"./isTextNode":186}],157:[function(require,module,exports){ +},{"./isTextNode":195}],165:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @providesModule createArrayFrom + * @providesModule createArrayFromMixed * @typechecks */ @@ -20551,10 +22243,10 @@ function hasArrayNature(obj) { * * This is mostly useful idiomatically: * - * var createArrayFrom = require('createArrayFrom'); + * var createArrayFromMixed = require('createArrayFromMixed'); * * function takesOneOrMoreThings(things) { - * things = createArrayFrom(things); + * things = createArrayFromMixed(things); * ... * } * @@ -20566,7 +22258,7 @@ function hasArrayNature(obj) { * @param {*} obj * @return {array} */ -function createArrayFrom(obj) { +function createArrayFromMixed(obj) { if (!hasArrayNature(obj)) { return [obj]; } else if (Array.isArray(obj)) { @@ -20576,12 +22268,12 @@ function createArrayFrom(obj) { } } -module.exports = createArrayFrom; +module.exports = createArrayFromMixed; -},{"./toArray":199}],158:[function(require,module,exports){ +},{"./toArray":209}],166:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20592,10 +22284,10 @@ module.exports = createArrayFrom; * @typechecks */ -"use strict"; +'use strict'; // Defeat circular references by requiring this directly. -var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactClass = require("./ReactClass"); var ReactElement = require("./ReactElement"); var invariant = require("./invariant"); @@ -20614,7 +22306,8 @@ var invariant = require("./invariant"); function createFullPageComponent(tag) { var elementFactory = ReactElement.createFactory(tag); - var FullPageComponent = ReactCompositeComponent.createClass({ + var FullPageComponent = ReactClass.createClass({ + tagName: tag.toUpperCase(), displayName: 'ReactFullPageComponent' + tag, componentWillUnmount: function() { @@ -20639,10 +22332,10 @@ function createFullPageComponent(tag) { module.exports = createFullPageComponent; }).call(this,require('_process')) -},{"./ReactCompositeComponent":81,"./ReactElement":99,"./invariant":182,"_process":1}],159:[function(require,module,exports){ +},{"./ReactClass":78,"./ReactElement":103,"./invariant":191,"_process":1}],167:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20657,7 +22350,7 @@ module.exports = createFullPageComponent; var ExecutionEnvironment = require("./ExecutionEnvironment"); -var createArrayFrom = require("./createArrayFrom"); +var createArrayFromMixed = require("./createArrayFromMixed"); var getMarkupWrap = require("./getMarkupWrap"); var invariant = require("./invariant"); @@ -20716,10 +22409,10 @@ function createNodesFromMarkup(markup, handleScript) { handleScript, 'createNodesFromMarkup(...): Unexpected <script> element rendered.' ) : invariant(handleScript)); - createArrayFrom(scripts).forEach(handleScript); + createArrayFromMixed(scripts).forEach(handleScript); } - var nodes = createArrayFrom(node.childNodes); + var nodes = createArrayFromMixed(node.childNodes); while (node.lastChild) { node.removeChild(node.lastChild); } @@ -20729,9 +22422,10 @@ function createNodesFromMarkup(markup, handleScript) { module.exports = createNodesFromMarkup; }).call(this,require('_process')) -},{"./ExecutionEnvironment":64,"./createArrayFrom":157,"./getMarkupWrap":174,"./invariant":182,"_process":1}],160:[function(require,module,exports){ +},{"./ExecutionEnvironment":62,"./createArrayFromMixed":165,"./getMarkupWrap":183,"./invariant":191,"_process":1}],168:[function(require,module,exports){ +(function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20756,7 +22450,22 @@ module.exports = createNodesFromMarkup; * @param [string ...] Variable list of classNames in the string case. * @return string Renderable space-separated CSS className. */ + +'use strict'; +var warning = require("./warning"); + +var warned = false; + function cx(classNames) { + if ("production" !== process.env.NODE_ENV) { + ("production" !== process.env.NODE_ENV ? warning( + warned, + 'React.addons.classSet will be deprecated in a future version. See ' + + 'http://fb.me/react-addons-classset' + ) : null); + warned = true; + } + if (typeof classNames == 'object') { return Object.keys(classNames).filter(function(className) { return classNames[className]; @@ -20768,9 +22477,10 @@ function cx(classNames) { module.exports = cx; -},{}],161:[function(require,module,exports){ +}).call(this,require('_process')) +},{"./warning":212,"_process":1}],169:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20781,7 +22491,7 @@ module.exports = cx; * @typechecks static-only */ -"use strict"; +'use strict'; var CSSProperty = require("./CSSProperty"); @@ -20826,60 +22536,9 @@ function dangerousStyleValue(name, value) { module.exports = dangerousStyleValue; -},{"./CSSProperty":46}],162:[function(require,module,exports){ -(function (process){ -/** - * Copyright 2013-2014, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule deprecated - */ - -var assign = require("./Object.assign"); -var warning = require("./warning"); - -/** - * This will log a single deprecation notice per function and forward the call - * on to the new API. - * - * @param {string} namespace The namespace of the call, eg 'React' - * @param {string} oldName The old function name, eg 'renderComponent' - * @param {string} newName The new function name, eg 'render' - * @param {*} ctx The context this forwarded call should run in - * @param {function} fn The function to forward on to - * @return {*} Will be the value as returned from `fn` - */ -function deprecated(namespace, oldName, newName, ctx, fn) { - var warned = false; - if ("production" !== process.env.NODE_ENV) { - var newFn = function() { - ("production" !== process.env.NODE_ENV ? warning( - warned, - (namespace + "." + oldName + " will be deprecated in a future version. ") + - ("Use " + namespace + "." + newName + " instead.") - ) : null); - warned = true; - return fn.apply(ctx, arguments); - }; - newFn.displayName = (namespace + "_" + oldName); - // We need to make sure all properties of the original fn are copied over. - // In particular, this is needed to support PropTypes - return assign(newFn, fn); - } - - return fn; -} - -module.exports = deprecated; - -}).call(this,require('_process')) -},{"./Object.assign":70,"./warning":202,"_process":1}],163:[function(require,module,exports){ +},{"./CSSProperty":45}],170:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20911,10 +22570,10 @@ emptyFunction.thatReturnsArgument = function(arg) { return arg; }; module.exports = emptyFunction; -},{}],164:[function(require,module,exports){ +},{}],171:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -20935,27 +22594,26 @@ if ("production" !== process.env.NODE_ENV) { module.exports = emptyObject; }).call(this,require('_process')) -},{"_process":1}],165:[function(require,module,exports){ +},{"_process":1}],172:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @providesModule escapeTextForBrowser - * @typechecks static-only + * @providesModule escapeTextContentForBrowser */ -"use strict"; +'use strict'; var ESCAPE_LOOKUP = { - "&": "&", - ">": ">", - "<": "<", - "\"": """, - "'": "'" + '&': '&', + '>': '>', + '<': '<', + '"': '"', + '\'': ''' }; var ESCAPE_REGEX = /[&><"']/g; @@ -20970,28 +22628,99 @@ function escaper(match) { * @param {*} text Text value to escape. * @return {string} An escaped string. */ -function escapeTextForBrowser(text) { +function escapeTextContentForBrowser(text) { return ('' + text).replace(ESCAPE_REGEX, escaper); } -module.exports = escapeTextForBrowser; +module.exports = escapeTextContentForBrowser; -},{}],166:[function(require,module,exports){ +},{}],173:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @providesModule flattenChildren + * @providesModule findDOMNode + * @typechecks static-only */ -"use strict"; +'use strict'; + +var ReactCurrentOwner = require("./ReactCurrentOwner"); +var ReactInstanceMap = require("./ReactInstanceMap"); +var ReactMount = require("./ReactMount"); + +var invariant = require("./invariant"); +var isNode = require("./isNode"); +var warning = require("./warning"); + +/** + * Returns the DOM node rendered by this element. + * + * @param {ReactComponent|DOMElement} componentOrElement + * @return {DOMElement} The root node of this element. + */ +function findDOMNode(componentOrElement) { + if ("production" !== process.env.NODE_ENV) { + var owner = ReactCurrentOwner.current; + if (owner !== null) { + ("production" !== process.env.NODE_ENV ? warning( + owner._warnedAboutRefsInRender, + '%s is accessing getDOMNode or findDOMNode inside its render(). ' + + 'render() should be a pure function of props and state. It should ' + + 'never access something that requires stale data from the previous ' + + 'render, such as refs. Move this logic to componentDidMount and ' + + 'componentDidUpdate instead.', + owner.getName() || 'A component' + ) : null); + owner._warnedAboutRefsInRender = true; + } + } + if (componentOrElement == null) { + return null; + } + if (isNode(componentOrElement)) { + return componentOrElement; + } + if (ReactInstanceMap.has(componentOrElement)) { + return ReactMount.getNodeFromInstance(componentOrElement); + } + ("production" !== process.env.NODE_ENV ? invariant( + componentOrElement.render == null || + typeof componentOrElement.render !== 'function', + 'Component (with keys: %s) contains `render` method ' + + 'but is not mounted in the DOM', + Object.keys(componentOrElement) + ) : invariant(componentOrElement.render == null || + typeof componentOrElement.render !== 'function')); + ("production" !== process.env.NODE_ENV ? invariant( + false, + 'Element appears to be neither ReactComponent nor DOMNode (keys: %s)', + Object.keys(componentOrElement) + ) : invariant(false)); +} + +module.exports = findDOMNode; + +}).call(this,require('_process')) +},{"./ReactCurrentOwner":85,"./ReactInstanceMap":113,"./ReactMount":117,"./invariant":191,"./isNode":193,"./warning":212,"_process":1}],174:[function(require,module,exports){ +(function (process){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule flattenChildren + */ -var ReactTextComponent = require("./ReactTextComponent"); +'use strict'; var traverseAllChildren = require("./traverseAllChildren"); var warning = require("./warning"); @@ -21005,26 +22734,17 @@ function flattenSingleChildIntoContext(traverseContext, child, name) { // We found a component instance. var result = traverseContext; var keyUnique = !result.hasOwnProperty(name); - ("production" !== process.env.NODE_ENV ? warning( - keyUnique, - 'flattenChildren(...): Encountered two children with the same key, ' + - '`%s`. Child keys must be unique; when two children share a key, only ' + - 'the first child will be used.', - name - ) : null); + if ("production" !== process.env.NODE_ENV) { + ("production" !== process.env.NODE_ENV ? warning( + keyUnique, + 'flattenChildren(...): Encountered two children with the same key, ' + + '`%s`. Child keys must be unique; when two children share a key, only ' + + 'the first child will be used.', + name + ) : null); + } if (keyUnique && child != null) { - var type = typeof child; - var normalizedValue; - - if (type === 'string') { - normalizedValue = ReactTextComponent(child); - } else if (type === 'number') { - normalizedValue = ReactTextComponent('' + child); - } else { - normalizedValue = child; - } - - result[name] = normalizedValue; + result[name] = child; } } @@ -21045,9 +22765,9 @@ function flattenChildren(children) { module.exports = flattenChildren; }).call(this,require('_process')) -},{"./ReactTextComponent":128,"./traverseAllChildren":200,"./warning":202,"_process":1}],167:[function(require,module,exports){ +},{"./traverseAllChildren":210,"./warning":212,"_process":1}],175:[function(require,module,exports){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21074,9 +22794,9 @@ function focusNode(node) { module.exports = focusNode; -},{}],168:[function(require,module,exports){ +},{}],176:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21086,7 +22806,7 @@ module.exports = focusNode; * @providesModule forEachAccumulated */ -"use strict"; +'use strict'; /** * @param {array} an "accumulation" of items which is either an Array or @@ -21105,9 +22825,9 @@ var forEachAccumulated = function(arr, cb, scope) { module.exports = forEachAccumulated; -},{}],169:[function(require,module,exports){ +},{}],177:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21134,9 +22854,9 @@ function getActiveElement() /*?DOMElement*/ { module.exports = getActiveElement; -},{}],170:[function(require,module,exports){ +},{}],178:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21147,7 +22867,7 @@ module.exports = getActiveElement; * @typechecks static-only */ -"use strict"; +'use strict'; /** * `charCode` represents the actual "character code" and is safe to use with @@ -21186,9 +22906,9 @@ function getEventCharCode(nativeEvent) { module.exports = getEventCharCode; -},{}],171:[function(require,module,exports){ +},{}],179:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21199,7 +22919,7 @@ module.exports = getEventCharCode; * @typechecks static-only */ -"use strict"; +'use strict'; var getEventCharCode = require("./getEventCharCode"); @@ -21291,9 +23011,9 @@ function getEventKey(nativeEvent) { module.exports = getEventKey; -},{"./getEventCharCode":170}],172:[function(require,module,exports){ +},{"./getEventCharCode":178}],180:[function(require,module,exports){ /** - * Copyright 2013 Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21304,7 +23024,7 @@ module.exports = getEventKey; * @typechecks static-only */ -"use strict"; +'use strict'; /** * Translation from modifier key to the associated property in the event. @@ -21338,9 +23058,9 @@ function getEventModifierState(nativeEvent) { module.exports = getEventModifierState; -},{}],173:[function(require,module,exports){ +},{}],181:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21351,7 +23071,7 @@ module.exports = getEventModifierState; * @typechecks static-only */ -"use strict"; +'use strict'; /** * Gets the target node from a native browser event by accounting for @@ -21369,10 +23089,54 @@ function getEventTarget(nativeEvent) { module.exports = getEventTarget; -},{}],174:[function(require,module,exports){ +},{}],182:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule getIteratorFn + * @typechecks static-only + */ + +'use strict'; + +/* global Symbol */ +var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; +var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. + +/** + * Returns the iterator method function contained on the iterable object. + * + * Be sure to invoke the function with the iterable as context: + * + * var iteratorFn = getIteratorFn(myIterable); + * if (iteratorFn) { + * var iterator = iteratorFn.call(myIterable); + * ... + * } + * + * @param {?object} maybeIterable + * @return {?function} + */ +function getIteratorFn(maybeIterable) { + var iteratorFn = maybeIterable && ( + (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]) + ); + if (typeof iteratorFn === 'function') { + return iteratorFn; + } +} + +module.exports = getIteratorFn; + +},{}],183:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21486,9 +23250,9 @@ function getMarkupWrap(nodeName) { module.exports = getMarkupWrap; }).call(this,require('_process')) -},{"./ExecutionEnvironment":64,"./invariant":182,"_process":1}],175:[function(require,module,exports){ +},{"./ExecutionEnvironment":62,"./invariant":191,"_process":1}],184:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21498,7 +23262,7 @@ module.exports = getMarkupWrap; * @providesModule getNodeForCharacterOffset */ -"use strict"; +'use strict'; /** * Given any node return the first leaf node without children. @@ -21542,7 +23306,7 @@ function getNodeForCharacterOffset(root, offset) { var nodeEnd = 0; while (node) { - if (node.nodeType == 3) { + if (node.nodeType === 3) { nodeEnd = nodeStart + node.textContent.length; if (nodeStart <= offset && nodeEnd >= offset) { @@ -21561,9 +23325,9 @@ function getNodeForCharacterOffset(root, offset) { module.exports = getNodeForCharacterOffset; -},{}],176:[function(require,module,exports){ +},{}],185:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21573,7 +23337,7 @@ module.exports = getNodeForCharacterOffset; * @providesModule getReactRootElementInContainer */ -"use strict"; +'use strict'; var DOC_NODE_TYPE = 9; @@ -21596,9 +23360,9 @@ function getReactRootElementInContainer(container) { module.exports = getReactRootElementInContainer; -},{}],177:[function(require,module,exports){ +},{}],186:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21608,7 +23372,7 @@ module.exports = getReactRootElementInContainer; * @providesModule getTextContentAccessor */ -"use strict"; +'use strict'; var ExecutionEnvironment = require("./ExecutionEnvironment"); @@ -21633,9 +23397,9 @@ function getTextContentAccessor() { module.exports = getTextContentAccessor; -},{"./ExecutionEnvironment":64}],178:[function(require,module,exports){ +},{"./ExecutionEnvironment":62}],187:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21673,9 +23437,9 @@ function getUnboundedScrollPosition(scrollable) { module.exports = getUnboundedScrollPosition; -},{}],179:[function(require,module,exports){ +},{}],188:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21706,9 +23470,9 @@ function hyphenate(string) { module.exports = hyphenate; -},{}],180:[function(require,module,exports){ +},{}],189:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21747,10 +23511,10 @@ function hyphenateStyleName(string) { module.exports = hyphenateStyleName; -},{"./hyphenate":179}],181:[function(require,module,exports){ +},{"./hyphenate":188}],190:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21761,99 +23525,122 @@ module.exports = hyphenateStyleName; * @typechecks static-only */ -"use strict"; +'use strict'; +var ReactCompositeComponent = require("./ReactCompositeComponent"); +var ReactEmptyComponent = require("./ReactEmptyComponent"); +var ReactNativeComponent = require("./ReactNativeComponent"); + +var assign = require("./Object.assign"); +var invariant = require("./invariant"); var warning = require("./warning"); -var ReactElement = require("./ReactElement"); -var ReactLegacyElement = require("./ReactLegacyElement"); -var ReactNativeComponent = require("./ReactNativeComponent"); -var ReactEmptyComponent = require("./ReactEmptyComponent"); +// To avoid a cyclic dependency, we create the final class in this module +var ReactCompositeComponentWrapper = function() { }; +assign( + ReactCompositeComponentWrapper.prototype, + ReactCompositeComponent.Mixin, + { + _instantiateReactComponent: instantiateReactComponent + } +); /** - * Given an `element` create an instance that will actually be mounted. + * Check if the type reference is a known internal type. I.e. not a user + * provided composite type. * - * @param {object} element + * @param {function} type + * @return {boolean} Returns true if this is a valid internal type. + */ +function isInternalComponentType(type) { + return ( + typeof type === 'function' && + typeof type.prototype.mountComponent === 'function' && + typeof type.prototype.receiveComponent === 'function' + ); +} + +/** + * Given a ReactNode, create an instance that will actually be mounted. + * + * @param {ReactNode} node * @param {*} parentCompositeType The composite type that resolved this. * @return {object} A new instance of the element's constructor. * @protected */ -function instantiateReactComponent(element, parentCompositeType) { +function instantiateReactComponent(node, parentCompositeType) { var instance; - if ("production" !== process.env.NODE_ENV) { - ("production" !== process.env.NODE_ENV ? warning( - element && (typeof element.type === 'function' || - typeof element.type === 'string'), - 'Only functions or strings can be mounted as React components.' - ) : null); - - // Resolve mock instances - if (element.type._mockedReactClassConstructor) { - // If this is a mocked class, we treat the legacy factory as if it was the - // class constructor for future proofing unit tests. Because this might - // be mocked as a legacy factory, we ignore any warnings triggerd by - // this temporary hack. - ReactLegacyElement._isLegacyCallWarningEnabled = false; - try { - instance = new element.type._mockedReactClassConstructor( - element.props - ); - } finally { - ReactLegacyElement._isLegacyCallWarningEnabled = true; - } - - // If the mock implementation was a legacy factory, then it returns a - // element. We need to turn this into a real component instance. - if (ReactElement.isValidElement(instance)) { - instance = new instance.type(instance.props); - } + if (node === null || node === false) { + node = ReactEmptyComponent.emptyElement; + } - var render = instance.render; - if (!render) { - // For auto-mocked factories, the prototype isn't shimmed and therefore - // there is no render function on the instance. We replace the whole - // component with an empty component instance instead. - element = ReactEmptyComponent.getEmptyComponent(); - } else { - if (render._isMockFunction && !render._getMockImplementation()) { - // Auto-mocked components may have a prototype with a mocked render - // function. For those, we'll need to mock the result of the render - // since we consider undefined to be invalid results from render. - render.mockImplementation( - ReactEmptyComponent.getEmptyComponent - ); - } - instance.construct(element); - return instance; - } + if (typeof node === 'object') { + var element = node; + if ("production" !== process.env.NODE_ENV) { + ("production" !== process.env.NODE_ENV ? warning( + element && (typeof element.type === 'function' || + typeof element.type === 'string'), + 'Only functions or strings can be mounted as React components.' + ) : null); } - } - // Special case string values - if (typeof element.type === 'string') { - instance = ReactNativeComponent.createInstanceForTag( - element.type, - element.props, - parentCompositeType - ); + // Special case string values + if (parentCompositeType === element.type && + typeof element.type === 'string') { + // Avoid recursion if the wrapper renders itself. + instance = ReactNativeComponent.createInternalComponent(element); + // All native components are currently wrapped in a composite so we're + // safe to assume that this is what we should instantiate. + } else if (isInternalComponentType(element.type)) { + // This is temporarily available for custom components that are not string + // represenations. I.e. ART. Once those are updated to use the string + // representation, we can drop this code path. + instance = new element.type(element); + } else { + instance = new ReactCompositeComponentWrapper(); + } + } else if (typeof node === 'string' || typeof node === 'number') { + instance = ReactNativeComponent.createInstanceForText(node); } else { - // Normal case for non-mocks and non-strings - instance = new element.type(element.props); + ("production" !== process.env.NODE_ENV ? invariant( + false, + 'Encountered invalid React node of type %s', + typeof node + ) : invariant(false)); } if ("production" !== process.env.NODE_ENV) { ("production" !== process.env.NODE_ENV ? warning( typeof instance.construct === 'function' && typeof instance.mountComponent === 'function' && - typeof instance.receiveComponent === 'function', + typeof instance.receiveComponent === 'function' && + typeof instance.unmountComponent === 'function', 'Only React Components can be mounted.' ) : null); } - // This actually sets up the internal instance. This will become decoupled - // from the public instance in a future diff. - instance.construct(element); + // Sets up the instance. This can probably just move into the constructor now. + instance.construct(node); + + // These two fields are used by the DOM and ART diffing algorithms + // respectively. Instead of using expandos on components, we should be + // storing the state needed by the diffing algorithms elsewhere. + instance._mountIndex = 0; + instance._mountImage = null; + + if ("production" !== process.env.NODE_ENV) { + instance._isOwnerNecessary = false; + instance._warnedAboutRefsInRender = false; + } + + // Internal instances should fully constructed at this point, so they should + // not get any new fields added to them at this point. + if ("production" !== process.env.NODE_ENV) { + if (Object.preventExtensions) { + Object.preventExtensions(instance); + } + } return instance; } @@ -21861,10 +23648,10 @@ function instantiateReactComponent(element, parentCompositeType) { module.exports = instantiateReactComponent; }).call(this,require('_process')) -},{"./ReactElement":99,"./ReactEmptyComponent":101,"./ReactLegacyElement":108,"./ReactNativeComponent":114,"./warning":202,"_process":1}],182:[function(require,module,exports){ +},{"./Object.assign":69,"./ReactCompositeComponent":83,"./ReactEmptyComponent":105,"./ReactNativeComponent":120,"./invariant":191,"./warning":212,"_process":1}],191:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21918,9 +23705,9 @@ var invariant = function(condition, format, a, b, c, d, e, f) { module.exports = invariant; }).call(this,require('_process')) -},{"_process":1}],183:[function(require,module,exports){ +},{"_process":1}],192:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -21930,7 +23717,7 @@ module.exports = invariant; * @providesModule isEventSupported */ -"use strict"; +'use strict'; var ExecutionEnvironment = require("./ExecutionEnvironment"); @@ -21983,9 +23770,9 @@ function isEventSupported(eventNameSuffix, capture) { module.exports = isEventSupported; -},{"./ExecutionEnvironment":64}],184:[function(require,module,exports){ +},{"./ExecutionEnvironment":62}],193:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22002,18 +23789,17 @@ module.exports = isEventSupported; */ function isNode(object) { return !!(object && ( - typeof Node === 'function' ? object instanceof Node : - typeof object === 'object' && - typeof object.nodeType === 'number' && - typeof object.nodeName === 'string' + ((typeof Node === 'function' ? object instanceof Node : typeof object === 'object' && + typeof object.nodeType === 'number' && + typeof object.nodeName === 'string')) )); } module.exports = isNode; -},{}],185:[function(require,module,exports){ +},{}],194:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22023,7 +23809,7 @@ module.exports = isNode; * @providesModule isTextInputElement */ -"use strict"; +'use strict'; /** * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary @@ -22048,16 +23834,15 @@ var supportedInputTypes = { function isTextInputElement(elem) { return elem && ( - (elem.nodeName === 'INPUT' && supportedInputTypes[elem.type]) || - elem.nodeName === 'TEXTAREA' + (elem.nodeName === 'INPUT' && supportedInputTypes[elem.type] || elem.nodeName === 'TEXTAREA') ); } module.exports = isTextInputElement; -},{}],186:[function(require,module,exports){ +},{}],195:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22080,9 +23865,9 @@ function isTextNode(object) { module.exports = isTextNode; -},{"./isNode":184}],187:[function(require,module,exports){ +},{"./isNode":193}],196:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22093,7 +23878,7 @@ module.exports = isTextNode; * @typechecks static-only */ -"use strict"; +'use strict'; /** * Combines multiple className strings into one. @@ -22121,10 +23906,10 @@ function joinClasses(className/*, ... */) { module.exports = joinClasses; -},{}],188:[function(require,module,exports){ +},{}],197:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22135,7 +23920,7 @@ module.exports = joinClasses; * @typechecks static-only */ -"use strict"; +'use strict'; var invariant = require("./invariant"); @@ -22176,9 +23961,9 @@ var keyMirror = function(obj) { module.exports = keyMirror; }).call(this,require('_process')) -},{"./invariant":182,"_process":1}],189:[function(require,module,exports){ +},{"./invariant":191,"_process":1}],198:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22212,9 +23997,9 @@ var keyOf = function(oneKeyObj) { module.exports = keyOf; -},{}],190:[function(require,module,exports){ +},{}],199:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22265,9 +24050,9 @@ function mapObject(object, callback, context) { module.exports = mapObject; -},{}],191:[function(require,module,exports){ +},{}],200:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22278,7 +24063,7 @@ module.exports = mapObject; * @typechecks static-only */ -"use strict"; +'use strict'; /** * Memoizes the return value of a function that accepts one string argument. @@ -22289,54 +24074,19 @@ module.exports = mapObject; function memoizeStringOnly(callback) { var cache = {}; return function(string) { - if (cache.hasOwnProperty(string)) { - return cache[string]; - } else { - return cache[string] = callback.call(this, string); + if (!cache.hasOwnProperty(string)) { + cache[string] = callback.call(this, string); } + return cache[string]; }; } module.exports = memoizeStringOnly; -},{}],192:[function(require,module,exports){ +},{}],201:[function(require,module,exports){ (function (process){ /** - * Copyright 2014, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule monitorCodeUse - */ - -"use strict"; - -var invariant = require("./invariant"); - -/** - * Provides open-source compatible instrumentation for monitoring certain API - * uses before we're ready to issue a warning or refactor. It accepts an event - * name which may only contain the characters [a-z0-9_] and an optional data - * object with further information. - */ - -function monitorCodeUse(eventName, data) { - ("production" !== process.env.NODE_ENV ? invariant( - eventName && !/[^a-z0-9_]/.test(eventName), - 'You must provide an eventName using only the characters [a-z0-9_]' - ) : invariant(eventName && !/[^a-z0-9_]/.test(eventName))); -} - -module.exports = monitorCodeUse; - -}).call(this,require('_process')) -},{"./invariant":182,"_process":1}],193:[function(require,module,exports){ -(function (process){ -/** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22345,7 +24095,7 @@ module.exports = monitorCodeUse; * * @providesModule onlyChild */ -"use strict"; +'use strict'; var ReactElement = require("./ReactElement"); @@ -22373,9 +24123,9 @@ function onlyChild(children) { module.exports = onlyChild; }).call(this,require('_process')) -},{"./ReactElement":99,"./invariant":182,"_process":1}],194:[function(require,module,exports){ +},{"./ReactElement":103,"./invariant":191,"_process":1}],202:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22401,9 +24151,9 @@ if (ExecutionEnvironment.canUseDOM) { module.exports = performance || {}; -},{"./ExecutionEnvironment":64}],195:[function(require,module,exports){ +},{"./ExecutionEnvironment":62}],203:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22429,9 +24179,37 @@ var performanceNow = performance.now.bind(performance); module.exports = performanceNow; -},{"./performance":194}],196:[function(require,module,exports){ +},{"./performance":202}],204:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule quoteAttributeValueForBrowser + */ + +'use strict'; + +var escapeTextContentForBrowser = require("./escapeTextContentForBrowser"); + +/** + * Escapes attribute value to prevent scripting attacks. + * + * @param {*} value Value to escape. + * @return {string} An escaped string. + */ +function quoteAttributeValueForBrowser(value) { + return '"' + escapeTextContentForBrowser(value) + '"'; +} + +module.exports = quoteAttributeValueForBrowser; + +},{"./escapeTextContentForBrowser":172}],205:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22441,7 +24219,9 @@ module.exports = performanceNow; * @providesModule setInnerHTML */ -"use strict"; +/* globals MSApp */ + +'use strict'; var ExecutionEnvironment = require("./ExecutionEnvironment"); @@ -22460,6 +24240,15 @@ var setInnerHTML = function(node, html) { node.innerHTML = html; }; +// Win8 apps: Allow all html to be inserted +if (typeof MSApp !== 'undefined' && MSApp.execUnsafeLocalFunction) { + setInnerHTML = function(node, html) { + MSApp.execUnsafeLocalFunction(function() { + node.innerHTML = html; + }); + }; +} + if (ExecutionEnvironment.canUseDOM) { // IE8: When updating a just created node with innerHTML only leading // whitespace is removed. When updating an existing node with innerHTML @@ -22507,9 +24296,51 @@ if (ExecutionEnvironment.canUseDOM) { module.exports = setInnerHTML; -},{"./ExecutionEnvironment":64}],197:[function(require,module,exports){ +},{"./ExecutionEnvironment":62}],206:[function(require,module,exports){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule setTextContent + */ + +'use strict'; + +var ExecutionEnvironment = require("./ExecutionEnvironment"); +var escapeTextContentForBrowser = require("./escapeTextContentForBrowser"); +var setInnerHTML = require("./setInnerHTML"); + +/** + * Set the textContent property of a node, ensuring that whitespace is preserved + * even in IE8. innerText is a poor substitute for textContent and, among many + * issues, inserts <br> instead of the literal newline chars. innerHTML behaves + * as it should. + * + * @param {DOMElement} node + * @param {string} text + * @internal + */ +var setTextContent = function(node, text) { + node.textContent = text; +}; + +if (ExecutionEnvironment.canUseDOM) { + if (!('textContent' in document.documentElement)) { + setTextContent = function(node, text) { + setInnerHTML(node, escapeTextContentForBrowser(text)); + }; + } +} + +module.exports = setTextContent; + +},{"./ExecutionEnvironment":62,"./escapeTextContentForBrowser":172,"./setInnerHTML":205}],207:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22519,7 +24350,7 @@ module.exports = setInnerHTML; * @providesModule shallowEqual */ -"use strict"; +'use strict'; /** * Performs equality by iterating through keys on an object and returning @@ -22551,9 +24382,10 @@ function shallowEqual(objA, objB) { module.exports = shallowEqual; -},{}],198:[function(require,module,exports){ +},{}],208:[function(require,module,exports){ +(function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22564,7 +24396,9 @@ module.exports = shallowEqual; * @typechecks static-only */ -"use strict"; +'use strict'; + +var warning = require("./warning"); /** * Given a `prevElement` and `nextElement`, determines if the existing @@ -22578,21 +24412,84 @@ module.exports = shallowEqual; * @protected */ function shouldUpdateReactComponent(prevElement, nextElement) { - if (prevElement && nextElement && - prevElement.type === nextElement.type && - prevElement.key === nextElement.key && - prevElement._owner === nextElement._owner) { - return true; + if (prevElement != null && nextElement != null) { + var prevType = typeof prevElement; + var nextType = typeof nextElement; + if (prevType === 'string' || prevType === 'number') { + return (nextType === 'string' || nextType === 'number'); + } else { + if (nextType === 'object' && + prevElement.type === nextElement.type && + prevElement.key === nextElement.key) { + var ownersMatch = prevElement._owner === nextElement._owner; + var prevName = null; + var nextName = null; + var nextDisplayName = null; + if ("production" !== process.env.NODE_ENV) { + if (!ownersMatch) { + if (prevElement._owner != null && + prevElement._owner.getPublicInstance() != null && + prevElement._owner.getPublicInstance().constructor != null) { + prevName = + prevElement._owner.getPublicInstance().constructor.displayName; + } + if (nextElement._owner != null && + nextElement._owner.getPublicInstance() != null && + nextElement._owner.getPublicInstance().constructor != null) { + nextName = + nextElement._owner.getPublicInstance().constructor.displayName; + } + if (nextElement.type != null && + nextElement.type.displayName != null) { + nextDisplayName = nextElement.type.displayName; + } + if (nextElement.type != null && typeof nextElement.type === 'string') { + nextDisplayName = nextElement.type; + } + if (typeof nextElement.type !== 'string' || + nextElement.type === 'input' || + nextElement.type === 'textarea') { + if ((prevElement._owner != null && + prevElement._owner._isOwnerNecessary === false) || + (nextElement._owner != null && + nextElement._owner._isOwnerNecessary === false)) { + if (prevElement._owner != null) { + prevElement._owner._isOwnerNecessary = true; + } + if (nextElement._owner != null) { + nextElement._owner._isOwnerNecessary = true; + } + ("production" !== process.env.NODE_ENV ? warning( + false, + '<%s /> is being rendered by both %s and %s using the same ' + + 'key (%s) in the same place. Currently, this means that ' + + 'they don\'t preserve state. This behavior should be very ' + + 'rare so we\'re considering deprecating it. Please contact ' + + 'the React team and explain your use case so that we can ' + + 'take that into consideration.', + nextDisplayName || 'Unknown Component', + prevName || '[Unknown]', + nextName || '[Unknown]', + prevElement.key + ) : null); + } + } + } + } + return ownersMatch; + } + } } return false; } module.exports = shouldUpdateReactComponent; -},{}],199:[function(require,module,exports){ +}).call(this,require('_process')) +},{"./warning":212,"_process":1}],209:[function(require,module,exports){ (function (process){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22609,7 +24506,7 @@ var invariant = require("./invariant"); * Convert array-like objects to arrays. * * This API assumes the caller knows the contents of the data type. For less - * well defined inputs use createArrayFrom. + * well defined inputs use createArrayFromMixed. * * @param {object|function|filelist} obj * @return {array} @@ -22661,10 +24558,10 @@ function toArray(obj) { module.exports = toArray; }).call(this,require('_process')) -},{"./invariant":182,"_process":1}],200:[function(require,module,exports){ +},{"./invariant":191,"_process":1}],210:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22674,22 +24571,22 @@ module.exports = toArray; * @providesModule traverseAllChildren */ -"use strict"; +'use strict'; var ReactElement = require("./ReactElement"); +var ReactFragment = require("./ReactFragment"); var ReactInstanceHandles = require("./ReactInstanceHandles"); +var getIteratorFn = require("./getIteratorFn"); var invariant = require("./invariant"); +var warning = require("./warning"); var SEPARATOR = ReactInstanceHandles.SEPARATOR; var SUBSEPARATOR = ':'; /** - * TODO: Test that: - * 1. `mapChildren` transforms strings and numbers into `ReactTextComponent`. - * 2. it('should fail when supplied duplicate key', function() { - * 3. That a single child and an array with one item have the same key pattern. - * }); + * TODO: Test that a single child and an array with one item have the same key + * pattern. */ var userProvidedKeyEscaperLookup = { @@ -22700,6 +24597,8 @@ var userProvidedKeyEscaperLookup = { var userProvidedKeyEscapeRegex = /[=.:]/g; +var didWarnAboutMaps = false; + function userProvidedKeyEscaper(match) { return userProvidedKeyEscaperLookup[match]; } @@ -22753,58 +24652,99 @@ function wrapUserProvidedKey(key) { * process. * @return {!number} The number of children in this subtree. */ -var traverseAllChildrenImpl = - function(children, nameSoFar, indexSoFar, callback, traverseContext) { - var nextName, nextIndex; - var subtreeCount = 0; // Count of children found in the current subtree. - if (Array.isArray(children)) { - for (var i = 0; i < children.length; i++) { - var child = children[i]; - nextName = ( - nameSoFar + - (nameSoFar ? SUBSEPARATOR : SEPARATOR) + - getComponentKey(child, i) - ); - nextIndex = indexSoFar + subtreeCount; - subtreeCount += traverseAllChildrenImpl( - child, - nextName, - nextIndex, - callback, - traverseContext - ); - } - } else { - var type = typeof children; - var isOnlyChild = nameSoFar === ''; +function traverseAllChildrenImpl( + children, + nameSoFar, + indexSoFar, + callback, + traverseContext +) { + var type = typeof children; + + if (type === 'undefined' || type === 'boolean') { + // All of the above are perceived as null. + children = null; + } + + if (children === null || + type === 'string' || + type === 'number' || + ReactElement.isValidElement(children)) { + callback( + traverseContext, + children, // If it's the only child, treat the name as if it was wrapped in an array - // so that it's consistent if the number of children grows - var storageName = - isOnlyChild ? SEPARATOR + getComponentKey(children, 0) : nameSoFar; - if (children == null || type === 'boolean') { - // All of the above are perceived as null. - callback(traverseContext, null, storageName, indexSoFar); - subtreeCount = 1; - } else if (type === 'string' || type === 'number' || - ReactElement.isValidElement(children)) { - callback(traverseContext, children, storageName, indexSoFar); - subtreeCount = 1; - } else if (type === 'object') { - ("production" !== process.env.NODE_ENV ? invariant( - !children || children.nodeType !== 1, - 'traverseAllChildren(...): Encountered an invalid child; DOM ' + - 'elements are not valid children of React components.' - ) : invariant(!children || children.nodeType !== 1)); - for (var key in children) { - if (children.hasOwnProperty(key)) { + // so that it's consistent if the number of children grows. + nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar, + indexSoFar + ); + return 1; + } + + var child, nextName, nextIndex; + var subtreeCount = 0; // Count of children found in the current subtree. + + if (Array.isArray(children)) { + for (var i = 0; i < children.length; i++) { + child = children[i]; + nextName = ( + (nameSoFar !== '' ? nameSoFar + SUBSEPARATOR : SEPARATOR) + + getComponentKey(child, i) + ); + nextIndex = indexSoFar + subtreeCount; + subtreeCount += traverseAllChildrenImpl( + child, + nextName, + nextIndex, + callback, + traverseContext + ); + } + } else { + var iteratorFn = getIteratorFn(children); + if (iteratorFn) { + var iterator = iteratorFn.call(children); + var step; + if (iteratorFn !== children.entries) { + var ii = 0; + while (!(step = iterator.next()).done) { + child = step.value; + nextName = ( + (nameSoFar !== '' ? nameSoFar + SUBSEPARATOR : SEPARATOR) + + getComponentKey(child, ii++) + ); + nextIndex = indexSoFar + subtreeCount; + subtreeCount += traverseAllChildrenImpl( + child, + nextName, + nextIndex, + callback, + traverseContext + ); + } + } else { + if ("production" !== process.env.NODE_ENV) { + ("production" !== process.env.NODE_ENV ? warning( + didWarnAboutMaps, + 'Using Maps as children is not yet fully supported. It is an ' + + 'experimental feature that might be removed. Convert it to a ' + + 'sequence / iterable of keyed ReactElements instead.' + ) : null); + didWarnAboutMaps = true; + } + // Iterator will provide entry [k,v] tuples rather than values. + while (!(step = iterator.next()).done) { + var entry = step.value; + if (entry) { + child = entry[1]; nextName = ( - nameSoFar + (nameSoFar ? SUBSEPARATOR : SEPARATOR) + - wrapUserProvidedKey(key) + SUBSEPARATOR + - getComponentKey(children[key], 0) + (nameSoFar !== '' ? nameSoFar + SUBSEPARATOR : SEPARATOR) + + wrapUserProvidedKey(entry[0]) + SUBSEPARATOR + + getComponentKey(child, 0) ); nextIndex = indexSoFar + subtreeCount; subtreeCount += traverseAllChildrenImpl( - children[key], + child, nextName, nextIndex, callback, @@ -22813,9 +24753,36 @@ var traverseAllChildrenImpl = } } } + } else if (type === 'object') { + ("production" !== process.env.NODE_ENV ? invariant( + children.nodeType !== 1, + 'traverseAllChildren(...): Encountered an invalid child; DOM ' + + 'elements are not valid children of React components.' + ) : invariant(children.nodeType !== 1)); + var fragment = ReactFragment.extract(children); + for (var key in fragment) { + if (fragment.hasOwnProperty(key)) { + child = fragment[key]; + nextName = ( + (nameSoFar !== '' ? nameSoFar + SUBSEPARATOR : SEPARATOR) + + wrapUserProvidedKey(key) + SUBSEPARATOR + + getComponentKey(child, 0) + ); + nextIndex = indexSoFar + subtreeCount; + subtreeCount += traverseAllChildrenImpl( + child, + nextName, + nextIndex, + callback, + traverseContext + ); + } + } } - return subtreeCount; - }; + } + + return subtreeCount; +} /** * Traverses children that are typically specified as `props.children`, but @@ -22844,10 +24811,10 @@ function traverseAllChildren(children, callback, traverseContext) { module.exports = traverseAllChildren; }).call(this,require('_process')) -},{"./ReactElement":99,"./ReactInstanceHandles":107,"./invariant":182,"_process":1}],201:[function(require,module,exports){ +},{"./ReactElement":103,"./ReactFragment":109,"./ReactInstanceHandles":112,"./getIteratorFn":182,"./invariant":191,"./warning":212,"_process":1}],211:[function(require,module,exports){ (function (process){ /** - * Copyright 2013-2014, Facebook, Inc. + * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -22857,7 +24824,7 @@ module.exports = traverseAllChildren; * @providesModule update */ -"use strict"; +'use strict'; var assign = require("./Object.assign"); var keyOf = require("./keyOf"); @@ -23012,10 +24979,10 @@ function update(value, spec) { module.exports = update; }).call(this,require('_process')) -},{"./Object.assign":70,"./invariant":182,"./keyOf":189,"_process":1}],202:[function(require,module,exports){ +},{"./Object.assign":69,"./invariant":191,"./keyOf":198,"_process":1}],212:[function(require,module,exports){ (function (process){ /** - * Copyright 2014, Facebook, Inc. + * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -23039,7 +25006,7 @@ var emptyFunction = require("./emptyFunction"); var warning = emptyFunction; if ("production" !== process.env.NODE_ENV) { - warning = function(condition, format ) {var args=Array.prototype.slice.call(arguments,2); + warning = function(condition, format ) {for (var args=[],$__0=2,$__1=arguments.length;$__0<$__1;$__0++) args.push(arguments[$__0]); if (format === undefined) { throw new Error( '`warning(condition, format, ...args)` requires a warning ' + @@ -23047,9 +25014,27 @@ if ("production" !== process.env.NODE_ENV) { ); } + if (format.length < 10 || /^[s\W]*$/.test(format)) { + throw new Error( + 'The warning format should be able to uniquely identify this ' + + 'warning. Please, use a more descriptive format than: ' + format + ); + } + + if (format.indexOf('Failed Composite propType: ') === 0) { + return; // Ignore CompositeComponent proptype check. + } + if (!condition) { var argIndex = 0; - console.warn('Warning: ' + format.replace(/%s/g, function() {return args[argIndex++];})); + var message = 'Warning: ' + format.replace(/%s/g, function() {return args[argIndex++];}); + console.warn(message); + try { + // --- Welcome to debugging React --- + // This error was thrown as a convenience so that you can use this stack + // to find the callsite that caused this warning to fire. + throw new Error(message); + } catch(x) {} } }; } @@ -23057,7 +25042,7 @@ if ("production" !== process.env.NODE_ENV) { module.exports = warning; }).call(this,require('_process')) -},{"./emptyFunction":163,"_process":1}],"flux":[function(require,module,exports){ +},{"./emptyFunction":170,"_process":1}],"flux":[function(require,module,exports){ /** * Copyright (c) 2014, Facebook, Inc. * All rights reserved. @@ -32280,7 +34265,7 @@ return jQuery; (function (global){ /** * @license - * lodash 3.5.0 (Custom Build) <https://lodash.com/> + * lodash 3.6.0 (Custom Build) <https://lodash.com/> * Build: `lodash modern -d -o ./index.js` * Copyright 2012-2015 The Dojo Foundation <http://dojofoundation.org/> * Based on Underscore.js 1.8.2 <http://underscorejs.org/LICENSE> @@ -32293,7 +34278,7 @@ return jQuery; var undefined; /** Used as the semantic version number. */ - var VERSION = '3.5.0'; + var VERSION = '3.6.0'; /** Used to compose bitmasks for wrapper metadata. */ var BIND_FLAG = 1, @@ -32303,8 +34288,8 @@ return jQuery; CURRY_RIGHT_FLAG = 16, PARTIAL_FLAG = 32, PARTIAL_RIGHT_FLAG = 64, - REARG_FLAG = 128, - ARY_FLAG = 256; + ARY_FLAG = 128, + REARG_FLAG = 256; /** Used as default options for `_.trunc`. */ var DEFAULT_TRUNC_LENGTH = 30, @@ -32368,18 +34353,18 @@ return jQuery; reInterpolate = /<%=([\s\S]+?)%>/g; /** - * Used to match ES template delimiters. - * See the [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-template-literal-lexical-components) - * for more details. + * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). + */ + var reComboMarks = /[\u0300-\u036f\ufe20-\ufe23]/g; + + /** + * Used to match [ES template delimiters](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-template-literal-lexical-components). */ var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; /** Used to match `RegExp` flags from their coerced string values. */ var reFlags = /\w*$/; - /** Used to detect named functions. */ - var reFuncName = /^\s*function[ \n\r\t]+\w/; - /** Used to detect hexadecimal string values. */ var reHexPrefix = /^0[xX]/; @@ -32393,16 +34378,13 @@ return jQuery; var reNoMatch = /($^)/; /** - * Used to match `RegExp` special characters. - * See this [article on `RegExp` characters](http://www.regular-expressions.info/characters.html#special) - * for more details. + * Used to match `RegExp` [special characters](http://www.regular-expressions.info/characters.html#special). + * In addition to special characters the forward slash is escaped to allow for + * easier `eval` use and `Function` compilation. */ var reRegExpChars = /[.*+?^${}()|[\]\/\\]/g, reHasRegExpChars = RegExp(reRegExpChars.source); - /** Used to detect functions containing a `this` reference. */ - var reThis = /\bthis\b/; - /** Used to match unescaped characters in compiled string literals. */ var reUnescapedString = /['\n\r\u2028\u2029\\]/g; @@ -32433,7 +34415,7 @@ return jQuery; 'Object', 'RegExp', 'Set', 'String', '_', 'clearTimeout', 'document', 'isFinite', 'parseInt', 'setTimeout', 'TypeError', 'Uint8Array', 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', - 'window', 'WinRTError' + 'window' ]; /** Used to make template sourceURLs easier to identify. */ @@ -32542,8 +34524,11 @@ return jQuery; /** Detect free variable `global` from Node.js. */ var freeGlobal = freeExports && freeModule && typeof global == 'object' && global; + /** Detect free variable `self`. */ + var freeSelf = objectTypes[typeof self] && self && self.Object && self; + /** Detect free variable `window`. */ - var freeWindow = objectTypes[typeof window] && window; + var freeWindow = objectTypes[typeof window] && window && window.Object && window; /** Detect the popular CommonJS extension `module.exports`. */ var moduleExports = freeModule && freeModule.exports === freeExports && freeExports; @@ -32554,7 +34539,7 @@ return jQuery; * The `this` value is used if it is the global object to avoid Greasemonkey's * restricted `window` object, otherwise the `window` object is used. */ - var root = freeGlobal || ((freeWindow !== (this && this.window)) && freeWindow) || this; + var root = freeGlobal || ((freeWindow !== (this && this.window)) && freeWindow) || freeSelf || this; /*--------------------------------------------------------------------------*/ @@ -32583,6 +34568,28 @@ return jQuery; } /** + * The base implementation of `_.findIndex` and `_.findLastIndex` without + * support for callback shorthands and `this` binding. + * + * @private + * @param {Array} array The array to search. + * @param {Function} predicate The function invoked per iteration. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function baseFindIndex(array, predicate, fromRight) { + var length = array.length, + index = fromRight ? length : -1; + + while ((fromRight ? index-- : ++index < length)) { + if (predicate(array[index], index, array)) { + return index; + } + } + return -1; + } + + /** * The base implementation of `_.indexOf` without support for binary searches. * * @private @@ -32768,7 +34775,6 @@ return jQuery; /** * Gets the index at which the first occurrence of `NaN` is found in `array`. - * If `fromRight` is provided elements of `array` are iterated from right to left. * * @private * @param {Array} array The array to search. @@ -32797,7 +34803,7 @@ return jQuery; * @returns {boolean} Returns `true` if `value` is object-like, else `false`. */ function isObjectLike(value) { - return (value && typeof value == 'object') || false; + return !!value && typeof value == 'object'; } /** @@ -32919,19 +34925,19 @@ return jQuery; * @returns {Function} Returns a new `lodash` function. * @example * - * _.mixin({ 'add': function(a, b) { return a + b; } }); + * _.mixin({ 'foo': _.constant('foo') }); * * var lodash = _.runInContext(); - * lodash.mixin({ 'sub': function(a, b) { return a - b; } }); + * lodash.mixin({ 'bar': lodash.constant('bar') }); * - * _.isFunction(_.add); + * _.isFunction(_.foo); * // => true - * _.isFunction(_.sub); + * _.isFunction(_.bar); * // => false * - * lodash.isFunction(lodash.add); + * lodash.isFunction(lodash.foo); * // => false - * lodash.isFunction(lodash.sub); + * lodash.isFunction(lodash.bar); * // => true * * // using `context` to mock `Date#getTime` use in `_.now` @@ -32984,9 +34990,8 @@ return jQuery; var idCounter = 0; /** - * Used to resolve the `toStringTag` of values. - * See the [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.prototype.tostring) - * for more details. + * Used to resolve the [`toStringTag`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.prototype.tostring) + * of values. */ var objToString = objectProto.toString; @@ -33051,15 +35056,17 @@ return jQuery; var FLOAT64_BYTES_PER_ELEMENT = Float64Array ? Float64Array.BYTES_PER_ELEMENT : 0; /** - * Used as the maximum length of an array-like value. - * See the [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-number.max_safe_integer) - * for more details. + * Used as the [maximum length](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-number.max_safe_integer) + * of an array-like value. */ var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; /** Used to store function metadata. */ var metaMap = WeakMap && new WeakMap; + /** Used to lookup unminified function names. */ + var realNames = {}; + /*------------------------------------------------------------------------*/ /** @@ -33209,7 +35216,7 @@ return jQuery; * @memberOf _.support * @type boolean */ - support.funcDecomp = !isNative(context.WinRTError) && reThis.test(runInContext); + support.funcDecomp = /\bthis\b/.test(function() { return this; }); /** * Detect if `Function#name` is supported (all but IE). @@ -33585,7 +35592,7 @@ return jQuery; /** * A specialized version of `_.forEach` for arrays without support for callback - * shorthands or `this` binding. + * shorthands and `this` binding. * * @private * @param {Array} array The array to iterate over. @@ -33606,7 +35613,7 @@ return jQuery; /** * A specialized version of `_.forEachRight` for arrays without support for - * callback shorthands or `this` binding. + * callback shorthands and `this` binding. * * @private * @param {Array} array The array to iterate over. @@ -33626,7 +35633,7 @@ return jQuery; /** * A specialized version of `_.every` for arrays without support for callback - * shorthands or `this` binding. + * shorthands and `this` binding. * * @private * @param {Array} array The array to iterate over. @@ -33648,7 +35655,7 @@ return jQuery; /** * A specialized version of `_.filter` for arrays without support for callback - * shorthands or `this` binding. + * shorthands and `this` binding. * * @private * @param {Array} array The array to iterate over. @@ -33672,7 +35679,7 @@ return jQuery; /** * A specialized version of `_.map` for arrays without support for callback - * shorthands or `this` binding. + * shorthands and `this` binding. * * @private * @param {Array} array The array to iterate over. @@ -33734,7 +35741,7 @@ return jQuery; /** * A specialized version of `_.reduce` for arrays without support for callback - * shorthands or `this` binding. + * shorthands and `this` binding. * * @private * @param {Array} array The array to iterate over. @@ -33759,7 +35766,7 @@ return jQuery; /** * A specialized version of `_.reduceRight` for arrays without support for - * callback shorthands or `this` binding. + * callback shorthands and `this` binding. * * @private * @param {Array} array The array to iterate over. @@ -33782,7 +35789,7 @@ return jQuery; /** * A specialized version of `_.some` for arrays without support for callback - * shorthands or `this` binding. + * shorthands and `this` binding. * * @private * @param {Array} array The array to iterate over. @@ -33803,6 +35810,23 @@ return jQuery; } /** + * A specialized version of `_.sum` for arrays without support for iteratees. + * + * @private + * @param {Array} array The array to iterate over. + * @returns {number} Returns the sum. + */ + function arraySum(array) { + var length = array.length, + result = 0; + + while (length--) { + result += +array[length] || 0; + } + return result; + } + + /** * Used by `_.defaults` to customize its `_.assign` use. * * @private @@ -33917,26 +35941,6 @@ return jQuery; } /** - * The base implementation of `_.bindAll` without support for individual - * method name arguments. - * - * @private - * @param {Object} object The object to bind and assign the bound methods to. - * @param {string[]} methodNames The object method names to bind. - * @returns {Object} Returns `object`. - */ - function baseBindAll(object, methodNames) { - var index = -1, - length = methodNames.length; - - while (++index < length) { - var key = methodNames[index]; - object[key] = createWrapper(object[key], BIND_FLAG, object); - } - return object; - } - - /** * The base implementation of `_.callback` which supports specifying the * number of arguments to provide to `func`. * @@ -33949,9 +35953,9 @@ return jQuery; function baseCallback(func, thisArg, argCount) { var type = typeof func; if (type == 'function') { - return (typeof thisArg != 'undefined' && isBindable(func)) - ? bindCallback(func, thisArg, argCount) - : func; + return typeof thisArg == 'undefined' + ? func + : bindCallback(func, thisArg, argCount); } if (func == null) { return identity; @@ -34058,14 +36062,14 @@ return jQuery; * @private * @param {Function} func The function to delay. * @param {number} wait The number of milliseconds to delay invocation. - * @param {Object} args The `arguments` object to slice and provide to `func`. + * @param {Object} args The arguments provide to `func`. * @returns {number} Returns the timer id. */ - function baseDelay(func, wait, args, fromIndex) { + function baseDelay(func, wait, args) { if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } - return setTimeout(function() { func.apply(undefined, baseSlice(args, fromIndex)); }, wait); + return setTimeout(function() { func.apply(undefined, args); }, wait); } /** @@ -34124,21 +36128,7 @@ return jQuery; * @param {Function} iteratee The function invoked per iteration. * @returns {Array|Object|string} Returns `collection`. */ - function baseEach(collection, iteratee) { - var length = collection ? collection.length : 0; - if (!isLength(length)) { - return baseForOwn(collection, iteratee); - } - var index = -1, - iterable = toObject(collection); - - while (++index < length) { - if (iteratee(iterable[index], index, iterable) === false) { - break; - } - } - return collection; - } + var baseEach = createBaseEach(baseForOwn); /** * The base implementation of `_.forEachRight` without support for callback @@ -34149,23 +36139,11 @@ return jQuery; * @param {Function} iteratee The function invoked per iteration. * @returns {Array|Object|string} Returns `collection`. */ - function baseEachRight(collection, iteratee) { - var length = collection ? collection.length : 0; - if (!isLength(length)) { - return baseForOwnRight(collection, iteratee); - } - var iterable = toObject(collection); - while (length--) { - if (iteratee(iterable[length], length, iterable) === false) { - break; - } - } - return collection; - } + var baseEachRight = createBaseEach(baseForOwnRight, true); /** * The base implementation of `_.every` without support for callback - * shorthands or `this` binding. + * shorthands and `this` binding. * * @private * @param {Array|Object|string} collection The collection to iterate over. @@ -34214,7 +36192,7 @@ return jQuery; /** * The base implementation of `_.filter` without support for callback - * shorthands or `this` binding. + * shorthands and `this` binding. * * @private * @param {Array|Object|string} collection The collection to iterate over. @@ -34263,11 +36241,10 @@ return jQuery; * @param {Array} array The array to flatten. * @param {boolean} isDeep Specify a deep flatten. * @param {boolean} isStrict Restrict flattening to arrays and `arguments` objects. - * @param {number} fromIndex The index to start from. * @returns {Array} Returns the new flattened array. */ - function baseFlatten(array, isDeep, isStrict, fromIndex) { - var index = fromIndex - 1, + function baseFlatten(array, isDeep, isStrict) { + var index = -1, length = array.length, resIndex = -1, result = []; @@ -34278,7 +36255,7 @@ return jQuery; if (isObjectLike(value) && isLength(value.length) && (isArray(value) || isArguments(value))) { if (isDeep) { // Recursively flatten arrays (susceptible to call stack limits). - value = baseFlatten(value, isDeep, isStrict, 0); + value = baseFlatten(value, isDeep, isStrict); } var valIndex = -1, valLength = value.length; @@ -34306,20 +36283,7 @@ return jQuery; * @param {Function} keysFunc The function to get the keys of `object`. * @returns {Object} Returns `object`. */ - function baseFor(object, iteratee, keysFunc) { - var index = -1, - iterable = toObject(object), - props = keysFunc(object), - length = props.length; - - while (++index < length) { - var key = props[index]; - if (iteratee(iterable[key], key, iterable) === false) { - break; - } - } - return object; - } + var baseFor = createBaseFor(); /** * This function is like `baseFor` except that it iterates over properties @@ -34331,19 +36295,7 @@ return jQuery; * @param {Function} keysFunc The function to get the keys of `object`. * @returns {Object} Returns `object`. */ - function baseForRight(object, iteratee, keysFunc) { - var iterable = toObject(object), - props = keysFunc(object), - length = props.length; - - while (length--) { - var key = props[length]; - if (iteratee(iterable[key], key, iterable) === false) { - break; - } - } - return object; - } + var baseForRight = createBaseFor(true); /** * The base implementation of `_.forIn` without support for callback @@ -34409,30 +36361,6 @@ return jQuery; } /** - * The base implementation of `_.invoke` which requires additional arguments - * to be provided as an array of arguments rather than individually. - * - * @private - * @param {Array|Object|string} collection The collection to iterate over. - * @param {Function|string} methodName The name of the method to invoke or - * the function invoked per iteration. - * @param {Array} [args] The arguments to invoke the method with. - * @returns {Array} Returns the array of results. - */ - function baseInvoke(collection, methodName, args) { - var index = -1, - isFunc = typeof methodName == 'function', - length = collection ? collection.length : 0, - result = isLength(length) ? Array(length) : []; - - baseEach(collection, function(value) { - var func = isFunc ? methodName : (value != null && value[methodName]); - result[++index] = func ? func.apply(value, args) : undefined; - }); - return result; - } - - /** * The base implementation of `_.isEqual` without support for `this` binding * `customizer` functions. * @@ -34440,12 +36368,12 @@ return jQuery; * @param {*} value The value to compare. * @param {*} other The other value to compare. * @param {Function} [customizer] The function to customize comparing values. - * @param {boolean} [isWhere] Specify performing partial comparisons. + * @param {boolean} [isLoose] Specify performing partial comparisons. * @param {Array} [stackA] Tracks traversed `value` objects. * @param {Array} [stackB] Tracks traversed `other` objects. * @returns {boolean} Returns `true` if the values are equivalent, else `false`. */ - function baseIsEqual(value, other, customizer, isWhere, stackA, stackB) { + function baseIsEqual(value, other, customizer, isLoose, stackA, stackB) { // Exit early for identical values. if (value === other) { // Treat `+0` vs. `-0` as not equal. @@ -34460,7 +36388,7 @@ return jQuery; // Return `false` unless both values are `NaN`. return value !== value && other !== other; } - return baseIsEqualDeep(value, other, baseIsEqual, customizer, isWhere, stackA, stackB); + return baseIsEqualDeep(value, other, baseIsEqual, customizer, isLoose, stackA, stackB); } /** @@ -34473,12 +36401,12 @@ return jQuery; * @param {Object} other The other object to compare. * @param {Function} equalFunc The function to determine equivalents of values. * @param {Function} [customizer] The function to customize comparing objects. - * @param {boolean} [isWhere] Specify performing partial comparisons. + * @param {boolean} [isLoose] Specify performing partial comparisons. * @param {Array} [stackA=[]] Tracks traversed `value` objects. * @param {Array} [stackB=[]] Tracks traversed `other` objects. * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. */ - function baseIsEqualDeep(object, other, equalFunc, customizer, isWhere, stackA, stackB) { + function baseIsEqualDeep(object, other, equalFunc, customizer, isLoose, stackA, stackB) { var objIsArr = isArray(object), othIsArr = isArray(other), objTag = arrayTag, @@ -34500,21 +36428,27 @@ return jQuery; othIsArr = isTypedArray(other); } } - var objIsObj = objTag == objectTag, - othIsObj = othTag == objectTag, + var objIsObj = (objTag == objectTag || (isLoose && objTag == funcTag)), + othIsObj = (othTag == objectTag || (isLoose && othTag == funcTag)), isSameTag = objTag == othTag; if (isSameTag && !(objIsArr || objIsObj)) { return equalByTag(object, other, objTag); } - var valWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'), - othWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__'); + if (isLoose) { + if (!isSameTag && !(objIsObj && othIsObj)) { + return false; + } + } else { + var valWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'), + othWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__'); - if (valWrapped || othWrapped) { - return equalFunc(valWrapped ? object.value() : object, othWrapped ? other.value() : other, customizer, isWhere, stackA, stackB); - } - if (!isSameTag) { - return false; + if (valWrapped || othWrapped) { + return equalFunc(valWrapped ? object.value() : object, othWrapped ? other.value() : other, customizer, isLoose, stackA, stackB); + } + if (!isSameTag) { + return false; + } } // Assume cyclic values are equal. // For more information on detecting circular references see https://es5.github.io/#JO. @@ -34531,7 +36465,7 @@ return jQuery; stackA.push(object); stackB.push(other); - var result = (objIsArr ? equalArrays : equalObjects)(object, other, equalFunc, customizer, isWhere, stackA, stackB); + var result = (objIsArr ? equalArrays : equalObjects)(object, other, equalFunc, customizer, isLoose, stackA, stackB); stackA.pop(); stackB.pop(); @@ -34541,7 +36475,7 @@ return jQuery; /** * The base implementation of `_.isMatch` without support for callback - * shorthands or `this` binding. + * shorthands and `this` binding. * * @private * @param {Object} object The object to inspect. @@ -34552,30 +36486,27 @@ return jQuery; * @returns {boolean} Returns `true` if `object` is a match, else `false`. */ function baseIsMatch(object, props, values, strictCompareFlags, customizer) { - var length = props.length; - if (object == null) { - return !length; - } var index = -1, + length = props.length, noCustomizer = !customizer; while (++index < length) { if ((noCustomizer && strictCompareFlags[index]) ? values[index] !== object[props[index]] - : !hasOwnProperty.call(object, props[index]) + : !(props[index] in object) ) { return false; } } index = -1; while (++index < length) { - var key = props[index]; + var key = props[index], + objValue = object[key], + srcValue = values[index]; + if (noCustomizer && strictCompareFlags[index]) { - var result = hasOwnProperty.call(object, key); + var result = typeof objValue != 'undefined' || (key in object); } else { - var objValue = object[key], - srcValue = values[index]; - result = customizer ? customizer(objValue, srcValue, key) : undefined; if (typeof result == 'undefined') { result = baseIsEqual(srcValue, objValue, customizer, true); @@ -34590,7 +36521,7 @@ return jQuery; /** * The base implementation of `_.map` without support for callback shorthands - * or `this` binding. + * and `this` binding. * * @private * @param {Array|Object|string} collection The collection to iterate over. @@ -34616,13 +36547,17 @@ return jQuery; var props = keys(source), length = props.length; + if (!length) { + return constant(true); + } if (length == 1) { var key = props[0], value = source[key]; if (isStrictComparable(value)) { return function(object) { - return object != null && object[key] === value && hasOwnProperty.call(object, key); + return object != null && object[key] === value && + (typeof value != 'undefined' || (key in toObject(object))); }; } } @@ -34635,7 +36570,7 @@ return jQuery; strictCompareFlags[length] = isStrictComparable(value); } return function(object) { - return baseIsMatch(object, props, values, strictCompareFlags); + return object != null && baseIsMatch(toObject(object), props, values, strictCompareFlags); }; } @@ -34651,7 +36586,8 @@ return jQuery; function baseMatchesProperty(key, value) { if (isStrictComparable(value)) { return function(object) { - return object != null && object[key] === value; + return object != null && object[key] === value && + (typeof value != 'undefined' || (key in toObject(object))); }; } return function(object) { @@ -34731,7 +36667,7 @@ return jQuery; if (isLength(srcValue.length) && (isArray(srcValue) || isTypedArray(srcValue))) { result = isArray(value) ? value - : (value ? arrayCopy(value) : []); + : ((value && value.length) ? arrayCopy(value) : []); } else if (isPlainObject(srcValue) || isArguments(srcValue)) { result = isArguments(value) @@ -34769,30 +36705,6 @@ return jQuery; } /** - * The base implementation of `_.pullAt` without support for individual - * index arguments. - * - * @private - * @param {Array} array The array to modify. - * @param {number[]} indexes The indexes of elements to remove. - * @returns {Array} Returns the new array of removed elements. - */ - function basePullAt(array, indexes) { - var length = indexes.length, - result = baseAt(array, indexes); - - indexes.sort(baseCompareAscending); - while (length--) { - var index = parseFloat(indexes[length]); - if (index != previous && isIndex(index)) { - var previous = index; - splice.call(array, index, 1); - } - } - return result; - } - - /** * The base implementation of `_.random` without support for argument juggling * and returning floating-point numbers. * @@ -34807,7 +36719,7 @@ return jQuery; /** * The base implementation of `_.reduce` and `_.reduceRight` without support - * for callback shorthands or `this` binding, which iterates over `collection` + * for callback shorthands and `this` binding, which iterates over `collection` * using the provided `eachFunc`. * * @private @@ -34874,7 +36786,7 @@ return jQuery; /** * The base implementation of `_.some` without support for callback shorthands - * or `this` binding. + * and `this` binding. * * @private * @param {Array|Object|string} collection The collection to iterate over. @@ -34942,6 +36854,23 @@ return jQuery; } /** + * The base implementation of `_.sum` without support for callback shorthands + * and `this` binding. + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {number} Returns the sum. + */ + function baseSum(collection, iteratee) { + var result = 0; + baseEach(collection, function(value, index, collection) { + result += +iteratee(value, index, collection) || 0; + }); + return result; + } + + /** * The base implementation of `_.uniq` without support for callback shorthands * and `this` binding. * @@ -35015,6 +36944,27 @@ return jQuery; } /** + * The base implementation of `_.dropRightWhile`, `_.dropWhile`, `_.takeRightWhile`, + * and `_.takeWhile` without support for callback shorthands and `this` binding. + * + * @private + * @param {Array} array The array to query. + * @param {Function} predicate The function invoked per iteration. + * @param {boolean} [isDrop] Specify dropping elements instead of taking them. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Array} Returns the slice of `array`. + */ + function baseWhile(array, predicate, isDrop, fromRight) { + var length = array.length, + index = fromRight ? length : -1; + + while ((fromRight ? index-- : ++index < length) && predicate(array[index], index, array)) {} + return isDrop + ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length)) + : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index)); + } + + /** * The base implementation of `wrapperValue` which returns the result of * performing a sequence of actions on the unwrapped `value`, where each * successive action is supplied the return value of the previous. @@ -35022,7 +36972,7 @@ return jQuery; * @private * @param {*} value The unwrapped value. * @param {Array} actions Actions to peform to resolve the unwrapped value. - * @returns {*} Returns the resolved unwrapped value. + * @returns {*} Returns the resolved value. */ function baseWrapperValue(value, actions) { var result = value; @@ -35049,8 +36999,7 @@ return jQuery; * @private * @param {Array} array The sorted array to inspect. * @param {*} value The value to evaluate. - * @param {boolean} [retHighest] Specify returning the highest, instead - * of the lowest, index at which a value should be inserted into `array`. + * @param {boolean} [retHighest] Specify returning the highest qualified index. * @returns {number} Returns the index at which `value` should be inserted * into `array`. */ @@ -35083,8 +37032,7 @@ return jQuery; * @param {Array} array The sorted array to inspect. * @param {*} value The value to evaluate. * @param {Function} iteratee The function invoked per iteration. - * @param {boolean} [retHighest] Specify returning the highest, instead - * of the lowest, index at which a value should be inserted into `array`. + * @param {boolean} [retHighest] Specify returning the highest qualified index. * @returns {number} Returns the index at which `value` should be inserted * into `array`. */ @@ -35250,6 +37198,9 @@ return jQuery; * object composed from the results of running each element in the collection * through an iteratee. * + * **Note:** This function is used to create `_.countBy`, `_.groupBy`, `_.indexBy`, + * and `_.partition`. + * * @private * @param {Function} setter The function to set keys and values of the accumulator object. * @param {Function} [initializer] The function to initialize the accumulator object. @@ -35281,6 +37232,8 @@ return jQuery; * Creates a function that assigns properties of source object(s) to a given * destination object. * + * **Note:** This function is used to create `_.assign`, `_.defaults`, and `_.merge`. + * * @private * @param {Function} assigner The function to assign values. * @returns {Function} Returns the new assigner function. @@ -35321,6 +37274,56 @@ return jQuery; } /** + * Creates a `baseEach` or `baseEachRight` function. + * + * @private + * @param {Function} eachFunc The function to iterate over a collection. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseEach(eachFunc, fromRight) { + return function(collection, iteratee) { + var length = collection ? collection.length : 0; + if (!isLength(length)) { + return eachFunc(collection, iteratee); + } + var index = fromRight ? length : -1, + iterable = toObject(collection); + + while ((fromRight ? index-- : ++index < length)) { + if (iteratee(iterable[index], index, iterable) === false) { + break; + } + } + return collection; + }; + } + + /** + * Creates a base function for `_.forIn` or `_.forInRight`. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseFor(fromRight) { + return function(object, iteratee, keysFunc) { + var iterable = toObject(object), + props = keysFunc(object), + length = props.length, + index = fromRight ? length : -1; + + while ((fromRight ? index-- : ++index < length)) { + var key = props[index]; + if (iteratee(iterable[key], key, iterable) === false) { + break; + } + } + return object; + }; + } + + /** * Creates a function that wraps `func` and invokes it with the `this` * binding of `thisArg`. * @@ -35351,41 +37354,6 @@ return jQuery; }; /** - * Creates a function to compose other functions into a single function. - * - * @private - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Function} Returns the new composer function. - */ - function createComposer(fromRight) { - return function() { - var length = arguments.length, - index = length, - fromIndex = fromRight ? (length - 1) : 0; - - if (!length) { - return function() { return arguments[0]; }; - } - var funcs = Array(length); - while (index--) { - funcs[index] = arguments[index]; - if (typeof funcs[index] != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - } - return function() { - var index = fromIndex, - result = funcs[index].apply(this, arguments); - - while ((fromRight ? index-- : ++index < length)) { - result = funcs[index].call(this, result); - } - return result; - }; - }; - } - - /** * Creates a function that produces compound words out of the words in a * given string. * @@ -35427,7 +37395,26 @@ return jQuery; } /** - * Creates a function that gets the extremum value of a collection. + * Creates a `_.curry` or `_.curryRight` function. + * + * @private + * @param {boolean} flag The curry bit flag. + * @returns {Function} Returns the new curry function. + */ + function createCurry(flag) { + function curryFunc(func, arity, guard) { + if (guard && isIterateeCall(func, arity, guard)) { + arity = null; + } + var result = createWrapper(func, flag, null, null, null, null, null, arity); + result.placeholder = curryFunc.placeholder; + return result; + } + return curryFunc; + } + + /** + * Creates a `_.max` or `_.min` function. * * @private * @param {Function} arrayFunc The function to get the extremum value from an array. @@ -35460,6 +37447,204 @@ return jQuery; } /** + * Creates a `_.find` or `_.findLast` function. + * + * @private + * @param {Function} eachFunc The function to iterate over a collection. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new find function. + */ + function createFind(eachFunc, fromRight) { + return function(collection, predicate, thisArg) { + predicate = getCallback(predicate, thisArg, 3); + if (isArray(collection)) { + var index = baseFindIndex(collection, predicate, fromRight); + return index > -1 ? collection[index] : undefined; + } + return baseFind(collection, predicate, eachFunc); + } + } + + /** + * Creates a `_.findIndex` or `_.findLastIndex` function. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new find function. + */ + function createFindIndex(fromRight) { + return function(array, predicate, thisArg) { + if (!(array && array.length)) { + return -1; + } + predicate = getCallback(predicate, thisArg, 3); + return baseFindIndex(array, predicate, fromRight); + }; + } + + /** + * Creates a `_.findKey` or `_.findLastKey` function. + * + * @private + * @param {Function} objectFunc The function to iterate over an object. + * @returns {Function} Returns the new find function. + */ + function createFindKey(objectFunc) { + return function(object, predicate, thisArg) { + predicate = getCallback(predicate, thisArg, 3); + return baseFind(object, predicate, objectFunc, true); + }; + } + + /** + * Creates a `_.flow` or `_.flowRight` function. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new flow function. + */ + function createFlow(fromRight) { + return function() { + var length = arguments.length; + if (!length) { + return function() { return arguments[0]; }; + } + var wrapper, + index = fromRight ? length : -1, + leftIndex = 0, + funcs = Array(length); + + while ((fromRight ? index-- : ++index < length)) { + var func = funcs[leftIndex++] = arguments[index]; + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + var funcName = wrapper ? '' : getFuncName(func); + wrapper = funcName == 'wrapper' ? new LodashWrapper([]) : wrapper; + } + index = wrapper ? -1 : length; + while (++index < length) { + func = funcs[index]; + funcName = getFuncName(func); + + var data = funcName == 'wrapper' ? getData(func) : null; + if (data && isLaziable(data[0])) { + wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]); + } else { + wrapper = (func.length == 1 && isLaziable(func)) ? wrapper[funcName]() : wrapper.thru(func); + } + } + return function() { + var args = arguments; + if (wrapper && args.length == 1 && isArray(args[0])) { + return wrapper.plant(args[0]).value(); + } + var index = 0, + result = funcs[index].apply(this, args); + + while (++index < length) { + result = funcs[index].call(this, result); + } + return result; + }; + }; + } + + /** + * Creates a function for `_.forEach` or `_.forEachRight`. + * + * @private + * @param {Function} arrayFunc The function to iterate over an array. + * @param {Function} eachFunc The function to iterate over a collection. + * @returns {Function} Returns the new each function. + */ + function createForEach(arrayFunc, eachFunc) { + return function(collection, iteratee, thisArg) { + return (typeof iteratee == 'function' && typeof thisArg == 'undefined' && isArray(collection)) + ? arrayFunc(collection, iteratee) + : eachFunc(collection, bindCallback(iteratee, thisArg, 3)); + }; + } + + /** + * Creates a function for `_.forIn` or `_.forInRight`. + * + * @private + * @param {Function} objectFunc The function to iterate over an object. + * @returns {Function} Returns the new each function. + */ + function createForIn(objectFunc) { + return function(object, iteratee, thisArg) { + if (typeof iteratee != 'function' || typeof thisArg != 'undefined') { + iteratee = bindCallback(iteratee, thisArg, 3); + } + return objectFunc(object, iteratee, keysIn); + }; + } + + /** + * Creates a function for `_.forOwn` or `_.forOwnRight`. + * + * @private + * @param {Function} objectFunc The function to iterate over an object. + * @returns {Function} Returns the new each function. + */ + function createForOwn(objectFunc) { + return function(object, iteratee, thisArg) { + if (typeof iteratee != 'function' || typeof thisArg != 'undefined') { + iteratee = bindCallback(iteratee, thisArg, 3); + } + return objectFunc(object, iteratee); + }; + } + + /** + * Creates a function for `_.padLeft` or `_.padRight`. + * + * @private + * @param {boolean} [fromRight] Specify padding from the right. + * @returns {Function} Returns the new pad function. + */ + function createPadDir(fromRight) { + return function(string, length, chars) { + string = baseToString(string); + return string && ((fromRight ? string : '') + createPadding(string, length, chars) + (fromRight ? '' : string)); + }; + } + + /** + * Creates a `_.partial` or `_.partialRight` function. + * + * @private + * @param {boolean} flag The partial bit flag. + * @returns {Function} Returns the new partial function. + */ + function createPartial(flag) { + var partialFunc = restParam(function(func, partials) { + var holders = replaceHolders(partials, partialFunc.placeholder); + return createWrapper(func, flag, null, partials, holders); + }); + return partialFunc; + } + + /** + * Creates a function for `_.reduce` or `_.reduceRight`. + * + * @private + * @param {Function} arrayFunc The function to iterate over an array. + * @param {Function} eachFunc The function to iterate over a collection. + * @returns {Function} Returns the new each function. + */ + function createReduce(arrayFunc, eachFunc) { + return function(collection, iteratee, accumulator, thisArg) { + var initFromArray = arguments.length < 3; + return (typeof iteratee == 'function' && typeof thisArg == 'undefined' && isArray(collection)) + ? arrayFunc(collection, iteratee, accumulator, initFromArray) + : baseReduce(collection, getCallback(iteratee, thisArg, 4), accumulator, initFromArray, eachFunc); + }; + } + + /** * Creates a function that wraps `func` and invokes it with optional `this` * binding of, partial application, and currying. * @@ -35522,7 +37707,12 @@ return jQuery; if (!isCurryBound) { bitmask &= ~(BIND_FLAG | BIND_KEY_FLAG); } - var result = createHybridWrapper(func, bitmask, thisArg, newPartials, newsHolders, newPartialsRight, newHoldersRight, newArgPos, ary, newArity); + var newData = [func, bitmask, thisArg, newPartials, newsHolders, newPartialsRight, newHoldersRight, newArgPos, ary, newArity], + result = createHybridWrapper.apply(undefined, newData); + + if (isLaziable(func)) { + setData(result, newData); + } result.placeholder = placeholder; return result; } @@ -35544,9 +37734,8 @@ return jQuery; } /** - * Creates the pad required for `string` based on the given padding length. - * The `chars` string may be truncated if the number of padding characters - * exceeds the padding length. + * Creates the padding required for `string` based on the given `length`. + * The `chars` string is truncated if the number of characters exceeds `length`. * * @private * @param {string} string The string to create padding for. @@ -35554,7 +37743,7 @@ return jQuery; * @param {string} [chars=' '] The string used as padding. * @returns {string} Returns the pad for `string`. */ - function createPad(string, length, chars) { + function createPadding(string, length, chars) { var strLength = string.length; length = +length; @@ -35604,6 +37793,22 @@ return jQuery; } /** + * Creates a `_.sortedIndex` or `_.sortedLastIndex` function. + * + * @private + * @param {boolean} [retHighest] Specify returning the highest qualified index. + * @returns {Function} Returns the new index function. + */ + function createSortedIndex(retHighest) { + return function(array, value, iteratee, thisArg) { + var func = getCallback(iteratee); + return (func === baseCallback && iteratee == null) + ? binaryIndex(array, value, retHighest) + : binaryIndexBy(array, value, func(iteratee, thisArg, 1), retHighest); + }; + } + + /** * Creates a function that either curries or invokes `func` with optional * `this` binding and partially applied arguments. * @@ -35645,10 +37850,10 @@ return jQuery; partials = holders = null; } - var data = !isBindKey && getData(func), + var data = isBindKey ? null : getData(func), newData = [func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity]; - if (data && data !== true) { + if (data) { mergeData(newData, data); bitmask = newData[1]; arity = newData[9]; @@ -35677,18 +37882,18 @@ return jQuery; * @param {Array} other The other array to compare. * @param {Function} equalFunc The function to determine equivalents of values. * @param {Function} [customizer] The function to customize comparing arrays. - * @param {boolean} [isWhere] Specify performing partial comparisons. + * @param {boolean} [isLoose] Specify performing partial comparisons. * @param {Array} [stackA] Tracks traversed `value` objects. * @param {Array} [stackB] Tracks traversed `other` objects. * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`. */ - function equalArrays(array, other, equalFunc, customizer, isWhere, stackA, stackB) { + function equalArrays(array, other, equalFunc, customizer, isLoose, stackA, stackB) { var index = -1, arrLength = array.length, othLength = other.length, result = true; - if (arrLength != othLength && !(isWhere && othLength > arrLength)) { + if (arrLength != othLength && !(isLoose && othLength > arrLength)) { return false; } // Deep compare the contents, ignoring non-numeric properties. @@ -35698,23 +37903,23 @@ return jQuery; result = undefined; if (customizer) { - result = isWhere + result = isLoose ? customizer(othValue, arrValue, index) : customizer(arrValue, othValue, index); } if (typeof result == 'undefined') { // Recursively compare arrays (susceptible to call stack limits). - if (isWhere) { + if (isLoose) { var othIndex = othLength; while (othIndex--) { othValue = other[othIndex]; - result = (arrValue && arrValue === othValue) || equalFunc(arrValue, othValue, customizer, isWhere, stackA, stackB); + result = (arrValue && arrValue === othValue) || equalFunc(arrValue, othValue, customizer, isLoose, stackA, stackB); if (result) { break; } } } else { - result = (arrValue && arrValue === othValue) || equalFunc(arrValue, othValue, customizer, isWhere, stackA, stackB); + result = (arrValue && arrValue === othValue) || equalFunc(arrValue, othValue, customizer, isLoose, stackA, stackB); } } } @@ -35770,26 +37975,26 @@ return jQuery; * @param {Object} other The other object to compare. * @param {Function} equalFunc The function to determine equivalents of values. * @param {Function} [customizer] The function to customize comparing values. - * @param {boolean} [isWhere] Specify performing partial comparisons. + * @param {boolean} [isLoose] Specify performing partial comparisons. * @param {Array} [stackA] Tracks traversed `value` objects. * @param {Array} [stackB] Tracks traversed `other` objects. * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. */ - function equalObjects(object, other, equalFunc, customizer, isWhere, stackA, stackB) { + function equalObjects(object, other, equalFunc, customizer, isLoose, stackA, stackB) { var objProps = keys(object), objLength = objProps.length, othProps = keys(other), othLength = othProps.length; - if (objLength != othLength && !isWhere) { + if (objLength != othLength && !isLoose) { return false; } - var hasCtor, + var skipCtor = isLoose, index = -1; while (++index < objLength) { var key = objProps[index], - result = hasOwnProperty.call(other, key); + result = isLoose ? key in other : hasOwnProperty.call(other, key); if (result) { var objValue = object[key], @@ -35797,21 +38002,21 @@ return jQuery; result = undefined; if (customizer) { - result = isWhere + result = isLoose ? customizer(othValue, objValue, key) : customizer(objValue, othValue, key); } if (typeof result == 'undefined') { // Recursively compare objects (susceptible to call stack limits). - result = (objValue && objValue === othValue) || equalFunc(objValue, othValue, customizer, isWhere, stackA, stackB); + result = (objValue && objValue === othValue) || equalFunc(objValue, othValue, customizer, isLoose, stackA, stackB); } } if (!result) { return false; } - hasCtor || (hasCtor = key == 'constructor'); + skipCtor || (skipCtor = key == 'constructor'); } - if (!hasCtor) { + if (!skipCtor) { var objCtor = object.constructor, othCtor = other.constructor; @@ -35829,7 +38034,7 @@ return jQuery; /** * Gets the extremum value of `collection` invoking `iteratee` for each value * in `collection` to generate the criterion by which the value is ranked. - * The `iteratee` is invoked with three arguments; (value, index, collection). + * The `iteratee` is invoked with three arguments: (value, index, collection). * * @private * @param {Array|Object|string} collection The collection to iterate over. @@ -35881,6 +38086,37 @@ return jQuery; }; /** + * Gets the name of `func`. + * + * @private + * @param {Function} func The function to query. + * @returns {string} Returns the function name. + */ + var getFuncName = (function() { + if (!support.funcNames) { + return constant(''); + } + if (constant.name == 'constant') { + return baseProperty('name'); + } + return function(func) { + var result = func.name, + array = realNames[result], + length = array ? array.length : 0; + + while (length--) { + var data = array[length], + otherFunc = data.func; + + if (otherFunc == null || otherFunc == func) { + return data.name; + } + } + return result; + }; + }()); + + /** * Gets the appropriate "indexOf" function. If the `_.indexOf` method is * customized this function returns the custom method, otherwise it returns * the `baseIndexOf` function. If arguments are provided the chosen function @@ -35998,31 +38234,6 @@ return jQuery; } /** - * Checks if `func` is eligible for `this` binding. - * - * @private - * @param {Function} func The function to check. - * @returns {boolean} Returns `true` if `func` is eligible, else `false`. - */ - function isBindable(func) { - var support = lodash.support, - result = !(support.funcNames ? func.name : support.funcDecomp); - - if (!result) { - var source = fnToString.call(func); - if (!support.funcNames) { - result = !reFuncName.test(source); - } - if (!result) { - // Check if `func` references the `this` keyword and store the result. - result = reThis.test(source) || isNative(func); - baseSetData(func, result); - } - } - return result; - } - - /** * Checks if `value` is a valid array-like index. * * @private @@ -36064,11 +38275,21 @@ return jQuery; } /** + * Checks if `func` has a lazy counterpart. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` has a lazy counterpart, else `false`. + */ + function isLaziable(func) { + var funcName = getFuncName(func); + return !!funcName && func === lodash[funcName] && funcName in LazyWrapper.prototype; + } + + /** * Checks if `value` is a valid array-like length. * - * **Note:** This function is based on ES `ToLength`. See the - * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength) - * for more details. + * **Note:** This function is based on [`ToLength`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength). * * @private * @param {*} value The value to check. @@ -36108,22 +38329,13 @@ return jQuery; function mergeData(data, source) { var bitmask = data[1], srcBitmask = source[1], - newBitmask = bitmask | srcBitmask; + newBitmask = bitmask | srcBitmask, + isCommon = newBitmask < ARY_FLAG; - var arityFlags = ARY_FLAG | REARG_FLAG, - bindFlags = BIND_FLAG | BIND_KEY_FLAG, - comboFlags = arityFlags | bindFlags | CURRY_BOUND_FLAG | CURRY_RIGHT_FLAG; - - var isAry = bitmask & ARY_FLAG && !(srcBitmask & ARY_FLAG), - isRearg = bitmask & REARG_FLAG && !(srcBitmask & REARG_FLAG), - argPos = (isRearg ? data : source)[7], - ary = (isAry ? data : source)[8]; - - var isCommon = !(bitmask >= REARG_FLAG && srcBitmask > bindFlags) && - !(bitmask > bindFlags && srcBitmask >= REARG_FLAG); - - var isCombo = (newBitmask >= arityFlags && newBitmask <= comboFlags) && - (bitmask < REARG_FLAG || ((isRearg || isAry) && argPos.length <= ary)); + var isCombo = + (srcBitmask == ARY_FLAG && bitmask == CURRY_FLAG) || + (srcBitmask == ARY_FLAG && bitmask == REARG_FLAG && data[7].length <= source[8]) || + (srcBitmask == (ARY_FLAG | REARG_FLAG) && bitmask == CURRY_FLAG); // Exit early if metadata can't be merged. if (!(isCommon || isCombo)) { @@ -36442,10 +38654,9 @@ return jQuery; * Creates an array excluding all values of the provided arrays using * `SameValueZero` for equality comparisons. * - * **Note:** `SameValueZero` comparisons are like strict equality comparisons, - * e.g. `===`, except that `NaN` matches `NaN`. See the - * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) - * for more details. + * **Note:** [`SameValueZero`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) + * comparisons are like strict equality comparisons, e.g. `===`, except that + * `NaN` matches `NaN`. * * @static * @memberOf _ @@ -36458,19 +38669,11 @@ return jQuery; * _.difference([1, 2, 3], [4, 2]); * // => [1, 3] */ - function difference() { - var args = arguments, - index = -1, - length = args.length; - - while (++index < length) { - var value = args[index]; - if (isArray(value) || isArguments(value)) { - break; - } - } - return baseDifference(value, baseFlatten(args, false, true, ++index)); - } + var difference = restParam(function(array, values) { + return (isArray(array) || isArguments(array)) + ? baseDifference(array, baseFlatten(values, false, true)) + : []; + }); /** * Creates a slice of `array` with `n` elements dropped from the beginning. @@ -36546,7 +38749,7 @@ return jQuery; /** * Creates a slice of `array` excluding elements dropped from the end. * Elements are dropped until `predicate` returns falsey. The predicate is - * bound to `thisArg` and invoked with three arguments; (value, index, array). + * bound to `thisArg` and invoked with three arguments: (value, index, array). * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -36593,19 +38796,15 @@ return jQuery; * // => ['barney', 'fred', 'pebbles'] */ function dropRightWhile(array, predicate, thisArg) { - var length = array ? array.length : 0; - if (!length) { - return []; - } - predicate = getCallback(predicate, thisArg, 3); - while (length-- && predicate(array[length], length, array)) {} - return baseSlice(array, 0, length + 1); + return (array && array.length) + ? baseWhile(array, getCallback(predicate, thisArg, 3), true, true) + : []; } /** * Creates a slice of `array` excluding elements dropped from the beginning. * Elements are dropped until `predicate` returns falsey. The predicate is - * bound to `thisArg` and invoked with three arguments; (value, index, array). + * bound to `thisArg` and invoked with three arguments: (value, index, array). * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -36652,14 +38851,9 @@ return jQuery; * // => ['barney', 'fred', 'pebbles'] */ function dropWhile(array, predicate, thisArg) { - var length = array ? array.length : 0; - if (!length) { - return []; - } - var index = -1; - predicate = getCallback(predicate, thisArg, 3); - while (++index < length && predicate(array[index], index, array)) {} - return baseSlice(array, index); + return (array && array.length) + ? baseWhile(array, getCallback(predicate, thisArg, 3), true) + : []; } /** @@ -36676,6 +38870,19 @@ return jQuery; * @param {number} [start=0] The start position. * @param {number} [end=array.length] The end position. * @returns {Array} Returns `array`. + * @example + * + * var array = [1, 2, 3]; + * + * _.fill(array, 'a'); + * console.log(array); + * // => ['a', 'a', 'a'] + * + * _.fill(Array(3), 2); + * // => [2, 2, 2] + * + * _.fill([4, 6, 8], '*', 1, 2); + * // => [4, '*', 8] */ function fill(array, value, start, end) { var length = array ? array.length : 0; @@ -36691,7 +38898,7 @@ return jQuery; /** * This method is like `_.find` except that it returns the index of the first - * element `predicate` returns truthy for, instead of the element itself. + * element `predicate` returns truthy for instead of the element itself. * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -36737,18 +38944,7 @@ return jQuery; * _.findIndex(users, 'active'); * // => 2 */ - function findIndex(array, predicate, thisArg) { - var index = -1, - length = array ? array.length : 0; - - predicate = getCallback(predicate, thisArg, 3); - while (++index < length) { - if (predicate(array[index], index, array)) { - return index; - } - } - return -1; - } + var findIndex = createFindIndex(); /** * This method is like `_.findIndex` except that it iterates over elements @@ -36798,16 +38994,7 @@ return jQuery; * _.findLastIndex(users, 'active'); * // => 0 */ - function findLastIndex(array, predicate, thisArg) { - var length = array ? array.length : 0; - predicate = getCallback(predicate, thisArg, 3); - while (length--) { - if (predicate(array[length], length, array)) { - return length; - } - } - return -1; - } + var findLastIndex = createFindIndex(true); /** * Gets the first element of `array`. @@ -36844,18 +39031,18 @@ return jQuery; * @example * * _.flatten([1, [2, 3, [4]]]); - * // => [1, 2, 3, [4]]; + * // => [1, 2, 3, [4]] * * // using `isDeep` * _.flatten([1, [2, 3, [4]]], true); - * // => [1, 2, 3, 4]; + * // => [1, 2, 3, 4] */ function flatten(array, isDeep, guard) { var length = array ? array.length : 0; if (guard && isIterateeCall(array, isDeep, guard)) { isDeep = false; } - return length ? baseFlatten(array, isDeep, false, 0) : []; + return length ? baseFlatten(array, isDeep) : []; } /** @@ -36869,11 +39056,11 @@ return jQuery; * @example * * _.flattenDeep([1, [2, 3, [4]]]); - * // => [1, 2, 3, 4]; + * // => [1, 2, 3, 4] */ function flattenDeep(array) { var length = array ? array.length : 0; - return length ? baseFlatten(array, true, false, 0) : []; + return length ? baseFlatten(array, true) : []; } /** @@ -36882,10 +39069,9 @@ return jQuery; * it is used as the offset from the end of `array`. If `array` is sorted * providing `true` for `fromIndex` performs a faster binary search. * - * **Note:** `SameValueZero` comparisons are like strict equality comparisons, - * e.g. `===`, except that `NaN` matches `NaN`. See the - * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) - * for more details. + * **Note:** [`SameValueZero`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) + * comparisons are like strict equality comparisons, e.g. `===`, except that + * `NaN` matches `NaN`. * * @static * @memberOf _ @@ -36948,10 +39134,9 @@ return jQuery; * Creates an array of unique values in all provided arrays using `SameValueZero` * for equality comparisons. * - * **Note:** `SameValueZero` comparisons are like strict equality comparisons, - * e.g. `===`, except that `NaN` matches `NaN`. See the - * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) - * for more details. + * **Note:** [`SameValueZero`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) + * comparisons are like strict equality comparisons, e.g. `===`, except that + * `NaN` matches `NaN`. * * @static * @memberOf _ @@ -37079,10 +39264,10 @@ return jQuery; * comparisons. * * **Notes:** - * - Unlike `_.without`, this method mutates `array`. - * - `SameValueZero` comparisons are like strict equality comparisons, e.g. `===`, - * except that `NaN` matches `NaN`. See the [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) - * for more details. + * - Unlike `_.without`, this method mutates `array` + * - [`SameValueZero`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) + * comparisons are like strict equality comparisons, e.g. `===`, except + * that `NaN` matches `NaN` * * @static * @memberOf _ @@ -37145,14 +39330,28 @@ return jQuery; * console.log(evens); * // => [10, 20] */ - function pullAt(array) { - return basePullAt(array || [], baseFlatten(arguments, false, false, 1)); - } + var pullAt = restParam(function(array, indexes) { + array || (array = []); + indexes = baseFlatten(indexes); + + var length = indexes.length, + result = baseAt(array, indexes); + + indexes.sort(baseCompareAscending); + while (length--) { + var index = parseFloat(indexes[length]); + if (index != previous && isIndex(index)) { + var previous = index; + splice.call(array, index, 1); + } + } + return result; + }); /** * Removes all elements from `array` that `predicate` returns truthy for * and returns an array of the removed elements. The predicate is bound to - * `thisArg` and invoked with three arguments; (value, index, array). + * `thisArg` and invoked with three arguments: (value, index, array). * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -37256,14 +39455,14 @@ return jQuery; * to compute their sort ranking. The iteratee is bound to `thisArg` and * invoked with one argument; (value). * - * If a property name is provided for `predicate` the created `_.property` + * If a property name is provided for `iteratee` the created `_.property` * style callback returns the property value of the given element. * * If a value is also provided for `thisArg` the created `_.matchesProperty` * style callback returns `true` for elements that have a matching property * value, else `false`. * - * If an object is provided for `predicate` the created `_.matches` style + * If an object is provided for `iteratee` the created `_.matches` style * callback returns `true` for elements that have the properties of the given * object, else `false`. * @@ -37297,12 +39496,7 @@ return jQuery; * _.sortedIndex([{ 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); * // => 1 */ - function sortedIndex(array, value, iteratee, thisArg) { - var func = getCallback(iteratee); - return (func === baseCallback && iteratee == null) - ? binaryIndex(array, value) - : binaryIndexBy(array, value, func(iteratee, thisArg, 1)); - } + var sortedIndex = createSortedIndex(); /** * This method is like `_.sortedIndex` except that it returns the highest @@ -37324,12 +39518,7 @@ return jQuery; * _.sortedLastIndex([4, 4, 5, 5], 5); * // => 4 */ - function sortedLastIndex(array, value, iteratee, thisArg) { - var func = getCallback(iteratee); - return (func === baseCallback && iteratee == null) - ? binaryIndex(array, value, true) - : binaryIndexBy(array, value, func(iteratee, thisArg, 1), true); - } + var sortedLastIndex = createSortedIndex(true); /** * Creates a slice of `array` with `n` elements taken from the beginning. @@ -37405,7 +39594,7 @@ return jQuery; /** * Creates a slice of `array` with elements taken from the end. Elements are * taken until `predicate` returns falsey. The predicate is bound to `thisArg` - * and invoked with three arguments; (value, index, array). + * and invoked with three arguments: (value, index, array). * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -37452,19 +39641,15 @@ return jQuery; * // => [] */ function takeRightWhile(array, predicate, thisArg) { - var length = array ? array.length : 0; - if (!length) { - return []; - } - predicate = getCallback(predicate, thisArg, 3); - while (length-- && predicate(array[length], length, array)) {} - return baseSlice(array, length + 1); + return (array && array.length) + ? baseWhile(array, getCallback(predicate, thisArg, 3), false, true) + : []; } /** * Creates a slice of `array` with elements taken from the beginning. Elements * are taken until `predicate` returns falsey. The predicate is bound to - * `thisArg` and invoked with three arguments; (value, index, array). + * `thisArg` and invoked with three arguments: (value, index, array). * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -37511,24 +39696,18 @@ return jQuery; * // => [] */ function takeWhile(array, predicate, thisArg) { - var length = array ? array.length : 0; - if (!length) { - return []; - } - var index = -1; - predicate = getCallback(predicate, thisArg, 3); - while (++index < length && predicate(array[index], index, array)) {} - return baseSlice(array, 0, index); + return (array && array.length) + ? baseWhile(array, getCallback(predicate, thisArg, 3)) + : []; } /** * Creates an array of unique values, in order, of the provided arrays using * `SameValueZero` for equality comparisons. * - * **Note:** `SameValueZero` comparisons are like strict equality comparisons, - * e.g. `===`, except that `NaN` matches `NaN`. See the - * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) - * for more details. + * **Note:** [`SameValueZero`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) + * comparisons are like strict equality comparisons, e.g. `===`, except that + * `NaN` matches `NaN`. * * @static * @memberOf _ @@ -37540,9 +39719,9 @@ return jQuery; * _.union([1, 2], [4, 2], [2, 1]); * // => [1, 2, 4] */ - function union() { - return baseUniq(baseFlatten(arguments, false, true, 0)); - } + var union = restParam(function(arrays) { + return baseUniq(baseFlatten(arrays, false, true)); + }); /** * Creates a duplicate-value-free version of an array using `SameValueZero` @@ -37550,23 +39729,22 @@ return jQuery; * search algorithm for sorted arrays. If an iteratee function is provided it * is invoked for each value in the array to generate the criterion by which * uniqueness is computed. The `iteratee` is bound to `thisArg` and invoked - * with three arguments; (value, index, array). + * with three arguments: (value, index, array). * - * If a property name is provided for `predicate` the created `_.property` + * If a property name is provided for `iteratee` the created `_.property` * style callback returns the property value of the given element. * * If a value is also provided for `thisArg` the created `_.matchesProperty` * style callback returns `true` for elements that have a matching property * value, else `false`. * - * If an object is provided for `predicate` the created `_.matches` style + * If an object is provided for `iteratee` the created `_.matches` style * callback returns `true` for elements that have the properties of the given * object, else `false`. * - * **Note:** `SameValueZero` comparisons are like strict equality comparisons, - * e.g. `===`, except that `NaN` matches `NaN`. See the - * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) - * for more details. + * **Note:** [`SameValueZero`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) + * comparisons are like strict equality comparisons, e.g. `===`, except that + * `NaN` matches `NaN`. * * @static * @memberOf _ @@ -37648,10 +39826,9 @@ return jQuery; * Creates an array excluding all provided values using `SameValueZero` for * equality comparisons. * - * **Note:** `SameValueZero` comparisons are like strict equality comparisons, - * e.g. `===`, except that `NaN` matches `NaN`. See the - * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) - * for more details. + * **Note:** [`SameValueZero`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) + * comparisons are like strict equality comparisons, e.g. `===`, except that + * `NaN` matches `NaN`. * * @static * @memberOf _ @@ -37664,14 +39841,15 @@ return jQuery; * _.without([1, 2, 1, 3], 1, 2); * // => [3] */ - function without(array) { - return baseDifference(array, baseSlice(arguments, 1)); - } + var without = restParam(function(array, values) { + return (isArray(array) || isArguments(array)) + ? baseDifference(array, values) + : []; + }); /** - * Creates an array that is the symmetric difference of the provided arrays. - * See [Wikipedia](https://en.wikipedia.org/wiki/Symmetric_difference) for - * more details. + * Creates an array that is the [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference) + * of the provided arrays. * * @static * @memberOf _ @@ -37713,20 +39891,13 @@ return jQuery; * _.zip(['fred', 'barney'], [30, 40], [true, false]); * // => [['fred', 30, true], ['barney', 40, false]] */ - function zip() { - var length = arguments.length, - array = Array(length); - - while (length--) { - array[length] = arguments[length]; - } - return unzip(array); - } + var zip = restParam(unzip); /** - * Creates an object composed from arrays of property names and values. Provide - * either a single two dimensional array, e.g. `[[key1, value1], [key2, value2]]` - * or two arrays, one of property names and one of corresponding values. + * The inverse of `_.pairs`; this method returns an object composed from arrays + * of property names and values. Provide either a single two dimensional array, + * e.g. `[[key1, value1], [key2, value2]]` or two arrays, one of property names + * and one of corresponding values. * * @static * @memberOf _ @@ -37737,6 +39908,9 @@ return jQuery; * @returns {Object} Returns the new object. * @example * + * _.zipObject([['fred', 30], ['barney', 40]]); + * // => { 'fred': 30, 'barney': 40 } + * * _.zipObject(['fred', 'barney'], [30, 40]); * // => { 'fred': 30, 'barney': 40 } */ @@ -37833,13 +40007,14 @@ return jQuery; * @returns {*} Returns the result of `interceptor`. * @example * - * _([1, 2, 3]) - * .last() + * _(' abc ') + * .chain() + * .trim() * .thru(function(value) { * return [value]; * }) * .value(); - * // => [3] + * // => ['abc'] */ function thru(value, interceptor, thisArg) { return interceptor.call(thisArg, value); @@ -38029,32 +40204,32 @@ return jQuery; * _.at(['a', 'b', 'c'], [0, 2]); * // => ['a', 'c'] * - * _.at(['fred', 'barney', 'pebbles'], 0, 2); - * // => ['fred', 'pebbles'] + * _.at(['barney', 'fred', 'pebbles'], 0, 2); + * // => ['barney', 'pebbles'] */ - function at(collection) { + var at = restParam(function(collection, props) { var length = collection ? collection.length : 0; if (isLength(length)) { collection = toIterable(collection); } - return baseAt(collection, baseFlatten(arguments, false, false, 1)); - } + return baseAt(collection, baseFlatten(props)); + }); /** * Creates an object composed of keys generated from the results of running * each element of `collection` through `iteratee`. The corresponding value * of each key is the number of times the key was returned by `iteratee`. - * The `iteratee` is bound to `thisArg` and invoked with three arguments; + * The `iteratee` is bound to `thisArg` and invoked with three arguments: * (value, index|key, collection). * - * If a property name is provided for `predicate` the created `_.property` + * If a property name is provided for `iteratee` the created `_.property` * style callback returns the property value of the given element. * * If a value is also provided for `thisArg` the created `_.matchesProperty` * style callback returns `true` for elements that have a matching property * value, else `false`. * - * If an object is provided for `predicate` the created `_.matches` style + * If an object is provided for `iteratee` the created `_.matches` style * callback returns `true` for elements that have the properties of the given * object, else `false`. * @@ -38087,7 +40262,7 @@ return jQuery; /** * Checks if `predicate` returns truthy for **all** elements of `collection`. - * The predicate is bound to `thisArg` and invoked with three arguments; + * The predicate is bound to `thisArg` and invoked with three arguments: * (value, index|key, collection). * * If a property name is provided for `predicate` the created `_.property` @@ -38135,6 +40310,9 @@ return jQuery; */ function every(collection, predicate, thisArg) { var func = isArray(collection) ? arrayEvery : baseEvery; + if (thisArg && isIterateeCall(collection, predicate, thisArg)) { + predicate = null; + } if (typeof predicate != 'function' || typeof thisArg != 'undefined') { predicate = getCallback(predicate, thisArg, 3); } @@ -38144,7 +40322,7 @@ return jQuery; /** * Iterates over elements of `collection`, returning an array of all elements * `predicate` returns truthy for. The predicate is bound to `thisArg` and - * invoked with three arguments; (value, index|key, collection). + * invoked with three arguments: (value, index|key, collection). * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -38199,7 +40377,7 @@ return jQuery; /** * Iterates over elements of `collection`, returning the first element * `predicate` returns truthy for. The predicate is bound to `thisArg` and - * invoked with three arguments; (value, index|key, collection). + * invoked with three arguments: (value, index|key, collection). * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -38246,14 +40424,7 @@ return jQuery; * _.result(_.find(users, 'active'), 'user'); * // => 'barney' */ - function find(collection, predicate, thisArg) { - if (isArray(collection)) { - var index = findIndex(collection, predicate, thisArg); - return index > -1 ? collection[index] : undefined; - } - predicate = getCallback(predicate, thisArg, 3); - return baseFind(collection, predicate, baseEach); - } + var find = createFind(baseEach); /** * This method is like `_.find` except that it iterates over elements of @@ -38274,10 +40445,7 @@ return jQuery; * }); * // => 3 */ - function findLast(collection, predicate, thisArg) { - predicate = getCallback(predicate, thisArg, 3); - return baseFind(collection, predicate, baseEachRight); - } + var findLast = createFind(baseEachRight, true); /** * Performs a deep comparison between each element in `collection` and the @@ -38314,7 +40482,7 @@ return jQuery; /** * Iterates over elements of `collection` invoking `iteratee` for each element. - * The `iteratee` is bound to `thisArg` and invoked with three arguments; + * The `iteratee` is bound to `thisArg` and invoked with three arguments: * (value, index|key, collection). Iterator functions may exit iteration early * by explicitly returning `false`. * @@ -38342,11 +40510,7 @@ return jQuery; * }); * // => logs each value-key pair and returns the object (iteration order is not guaranteed) */ - function forEach(collection, iteratee, thisArg) { - return (typeof iteratee == 'function' && typeof thisArg == 'undefined' && isArray(collection)) - ? arrayEach(collection, iteratee) - : baseEach(collection, bindCallback(iteratee, thisArg, 3)); - } + var forEach = createForEach(arrayEach, baseEach); /** * This method is like `_.forEach` except that it iterates over elements of @@ -38364,30 +40528,26 @@ return jQuery; * * _([1, 2]).forEachRight(function(n) { * console.log(n); - * }).join(','); + * }).value(); * // => logs each value from right to left and returns the array */ - function forEachRight(collection, iteratee, thisArg) { - return (typeof iteratee == 'function' && typeof thisArg == 'undefined' && isArray(collection)) - ? arrayEachRight(collection, iteratee) - : baseEachRight(collection, bindCallback(iteratee, thisArg, 3)); - } + var forEachRight = createForEach(arrayEachRight, baseEachRight); /** * Creates an object composed of keys generated from the results of running * each element of `collection` through `iteratee`. The corresponding value * of each key is an array of the elements responsible for generating the key. - * The `iteratee` is bound to `thisArg` and invoked with three arguments; + * The `iteratee` is bound to `thisArg` and invoked with three arguments: * (value, index|key, collection). * - * If a property name is provided for `predicate` the created `_.property` + * If a property name is provided for `iteratee` the created `_.property` * style callback returns the property value of the given element. * * If a value is also provided for `thisArg` the created `_.matchesProperty` * style callback returns `true` for elements that have a matching property * value, else `false`. * - * If an object is provided for `predicate` the created `_.matches` style + * If an object is provided for `iteratee` the created `_.matches` style * callback returns `true` for elements that have the properties of the given * object, else `false`. * @@ -38428,10 +40588,9 @@ return jQuery; * comparisons. If `fromIndex` is negative, it is used as the offset from * the end of `collection`. * - * **Note:** `SameValueZero` comparisons are like strict equality comparisons, - * e.g. `===`, except that `NaN` matches `NaN`. See the - * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) - * for more details. + * **Note:** [`SameValueZero`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-samevaluezero) + * comparisons are like strict equality comparisons, e.g. `===`, except that + * `NaN` matches `NaN`. * * @static * @memberOf _ @@ -38440,6 +40599,7 @@ return jQuery; * @param {Array|Object|string} collection The collection to search. * @param {*} target The value to search for. * @param {number} [fromIndex=0] The index to search from. + * @param- {Object} [guard] Enables use as a callback for functions like `_.reduce`. * @returns {boolean} Returns `true` if a matching element is found, else `false`. * @example * @@ -38455,7 +40615,7 @@ return jQuery; * _.includes('pebbles', 'eb'); * // => true */ - function includes(collection, target, fromIndex) { + function includes(collection, target, fromIndex, guard) { var length = collection ? collection.length : 0; if (!isLength(length)) { collection = values(collection); @@ -38464,10 +40624,10 @@ return jQuery; if (!length) { return false; } - if (typeof fromIndex == 'number') { - fromIndex = fromIndex < 0 ? nativeMax(length + fromIndex, 0) : (fromIndex || 0); - } else { + if (typeof fromIndex != 'number' || (guard && isIterateeCall(target, fromIndex, guard))) { fromIndex = 0; + } else { + fromIndex = fromIndex < 0 ? nativeMax(length + fromIndex, 0) : (fromIndex || 0); } return (typeof collection == 'string' || !isArray(collection) && isString(collection)) ? (fromIndex < length && collection.indexOf(target, fromIndex) > -1) @@ -38478,17 +40638,17 @@ return jQuery; * Creates an object composed of keys generated from the results of running * each element of `collection` through `iteratee`. The corresponding value * of each key is the last element responsible for generating the key. The - * iteratee function is bound to `thisArg` and invoked with three arguments; + * iteratee function is bound to `thisArg` and invoked with three arguments: * (value, index|key, collection). * - * If a property name is provided for `predicate` the created `_.property` + * If a property name is provided for `iteratee` the created `_.property` * style callback returns the property value of the given element. * * If a value is also provided for `thisArg` the created `_.matchesProperty` * style callback returns `true` for elements that have a matching property * value, else `false`. * - * If an object is provided for `predicate` the created `_.matches` style + * If an object is provided for `iteratee` the created `_.matches` style * callback returns `true` for elements that have the properties of the given * object, else `false`. * @@ -38546,23 +40706,32 @@ return jQuery; * _.invoke([123, 456], String.prototype.split, ''); * // => [['1', '2', '3'], ['4', '5', '6']] */ - function invoke(collection, methodName) { - return baseInvoke(collection, methodName, baseSlice(arguments, 2)); - } + var invoke = restParam(function(collection, methodName, args) { + var index = -1, + isFunc = typeof methodName == 'function', + length = collection ? collection.length : 0, + result = isLength(length) ? Array(length) : []; + + baseEach(collection, function(value) { + var func = isFunc ? methodName : (value != null && value[methodName]); + result[++index] = func ? func.apply(value, args) : undefined; + }); + return result; + }); /** * Creates an array of values by running each element in `collection` through * `iteratee`. The `iteratee` is bound to `thisArg` and invoked with three - * arguments; (value, index|key, collection). + * arguments: (value, index|key, collection). * - * If a property name is provided for `predicate` the created `_.property` + * If a property name is provided for `iteratee` the created `_.property` * style callback returns the property value of the given element. * * If a value is also provided for `thisArg` the created `_.matchesProperty` * style callback returns `true` for elements that have a matching property * value, else `false`. * - * If an object is provided for `predicate` the created `_.matches` style + * If an object is provided for `iteratee` the created `_.matches` style * callback returns `true` for elements that have the properties of the given * object, else `false`. * @@ -38571,9 +40740,9 @@ return jQuery; * * The guarded methods are: * `ary`, `callback`, `chunk`, `clone`, `create`, `curry`, `curryRight`, `drop`, - * `dropRight`, `fill`, `flatten`, `invert`, `max`, `min`, `parseInt`, `slice`, - * `sortBy`, `take`, `takeRight`, `template`, `trim`, `trimLeft`, `trimRight`, - * `trunc`, `random`, `range`, `sample`, `uniq`, and `words` + * `dropRight`, `every`, `fill`, `flatten`, `invert`, `max`, `min`, `parseInt`, + * `slice`, `sortBy`, `take`, `takeRight`, `template`, `trim`, `trimLeft`, + * `trimRight`, `trunc`, `random`, `range`, `sample`, `some`, `uniq`, and `words` * * @static * @memberOf _ @@ -38616,7 +40785,7 @@ return jQuery; * Creates an array of elements split into two groups, the first of which * contains elements `predicate` returns truthy for, while the second of which * contains elements `predicate` returns falsey for. The predicate is bound - * to `thisArg` and invoked with three arguments; (value, index|key, collection). + * to `thisArg` and invoked with three arguments: (value, index|key, collection). * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -38707,14 +40876,14 @@ return jQuery; * each element in `collection` through `iteratee`, where each successive * invocation is supplied the return value of the previous. If `accumulator` * is not provided the first element of `collection` is used as the initial - * value. The `iteratee` is bound to `thisArg`and invoked with four arguments; + * value. The `iteratee` is bound to `thisArg` and invoked with four arguments: * (accumulator, value, index|key, collection). * * Many lodash methods are guarded to work as interatees for methods like * `_.reduce`, `_.reduceRight`, and `_.transform`. * * The guarded methods are: - * `assign`, `defaults`, `merge`, and `sortAllBy` + * `assign`, `defaults`, `includes`, `merge`, `sortByAll`, and `sortByOrder` * * @static * @memberOf _ @@ -38738,10 +40907,7 @@ return jQuery; * }, {}); * // => { 'a': 3, 'b': 6 } (iteration order is not guaranteed) */ - function reduce(collection, iteratee, accumulator, thisArg) { - var func = isArray(collection) ? arrayReduce : baseReduce; - return func(collection, getCallback(iteratee, thisArg, 4), accumulator, arguments.length < 3, baseEach); - } + var reduce = createReduce(arrayReduce, baseEach); /** * This method is like `_.reduce` except that it iterates over elements of @@ -38765,10 +40931,7 @@ return jQuery; * }, []); * // => [4, 5, 2, 3, 0, 1] */ - function reduceRight(collection, iteratee, accumulator, thisArg) { - var func = isArray(collection) ? arrayReduceRight : baseReduce; - return func(collection, getCallback(iteratee, thisArg, 4), accumulator, arguments.length < 3, baseEachRight); - } + var reduceRight = createReduce(arrayReduceRight, baseEachRight); /** * The opposite of `_.filter`; this method returns the elements of `collection` @@ -38855,9 +41018,8 @@ return jQuery; } /** - * Creates an array of shuffled values, using a version of the Fisher-Yates - * shuffle. See [Wikipedia](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle) - * for more details. + * Creates an array of shuffled values, using a version of the + * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle). * * @static * @memberOf _ @@ -38915,7 +41077,7 @@ return jQuery; * Checks if `predicate` returns truthy for **any** element of `collection`. * The function returns as soon as it finds a passing value and does not iterate * over the entire collection. The predicate is bound to `thisArg` and invoked - * with three arguments; (value, index|key, collection). + * with three arguments: (value, index|key, collection). * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -38962,6 +41124,9 @@ return jQuery; */ function some(collection, predicate, thisArg) { var func = isArray(collection) ? arraySome : baseSome; + if (thisArg && isIterateeCall(collection, predicate, thisArg)) { + predicate = null; + } if (typeof predicate != 'function' || typeof thisArg != 'undefined') { predicate = getCallback(predicate, thisArg, 3); } @@ -38972,17 +41137,17 @@ return jQuery; * Creates an array of elements, sorted in ascending order by the results of * running each element in a collection through `iteratee`. This method performs * a stable sort, that is, it preserves the original sort order of equal elements. - * The `iteratee` is bound to `thisArg` and invoked with three arguments; + * The `iteratee` is bound to `thisArg` and invoked with three arguments: * (value, index|key, collection). * - * If a property name is provided for `predicate` the created `_.property` + * If a property name is provided for `iteratee` the created `_.property` * style callback returns the property value of the given element. * * If a value is also provided for `thisArg` the created `_.matchesProperty` * style callback returns `true` for elements that have a matching property * value, else `false`. * - * If an object is provided for `predicate` the created `_.matches` style + * If an object is provided for `iteratee` the created `_.matches` style * callback returns `true` for elements that have the properties of the given * object, else `false`. * @@ -39058,17 +41223,24 @@ return jQuery; * _.map(_.sortByAll(users, ['user', 'age']), _.values); * // => [['barney', 26], ['barney', 36], ['fred', 30], ['fred', 40]] */ - function sortByAll(collection) { + function sortByAll() { + var args = arguments, + collection = args[0], + guard = args[3], + index = 0, + length = args.length - 1; + if (collection == null) { return []; } - var args = arguments, - guard = args[3]; - + var props = Array(length); + while (index < length) { + props[index] = args[++index]; + } if (guard && isIterateeCall(args[1], args[2], guard)) { - args = [collection, args[1]]; + props = args[1]; } - return baseSortByOrder(collection, baseFlatten(args, false, false, 1), []); + return baseSortByOrder(collection, baseFlatten(props), []); } /** @@ -39287,7 +41459,7 @@ return jQuery; * @category Function * @param {Function} func The function to bind. * @param {*} thisArg The `this` binding of `func`. - * @param {...*} [args] The arguments to be partially applied. + * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new bound function. * @example * @@ -39306,16 +41478,14 @@ return jQuery; * bound('hi'); * // => 'hi fred!' */ - function bind(func, thisArg) { + var bind = restParam(function(func, thisArg, partials) { var bitmask = BIND_FLAG; - if (arguments.length > 2) { - var partials = baseSlice(arguments, 2), - holders = replaceHolders(partials, bind.placeholder); - + if (partials.length) { + var holders = replaceHolders(partials, bind.placeholder); bitmask |= PARTIAL_FLAG; } return createWrapper(func, bitmask, thisArg, partials, holders); - } + }); /** * Binds methods of an object to the object itself, overwriting the existing @@ -39345,13 +41515,18 @@ return jQuery; * jQuery('#docs').on('click', view.onClick); * // => logs 'clicked docs' when the element is clicked */ - function bindAll(object) { - return baseBindAll(object, - arguments.length > 1 - ? baseFlatten(arguments, false, false, 1) - : functions(object) - ); - } + var bindAll = restParam(function(object, methodNames) { + methodNames = methodNames.length ? baseFlatten(methodNames) : functions(object); + + var index = -1, + length = methodNames.length; + + while (++index < length) { + var key = methodNames[index]; + object[key] = createWrapper(object[key], BIND_FLAG, object); + } + return object; + }); /** * Creates a function that invokes the method at `object[key]` and prepends @@ -39370,7 +41545,7 @@ return jQuery; * @category Function * @param {Object} object The object the method belongs to. * @param {string} key The key of the method. - * @param {...*} [args] The arguments to be partially applied. + * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new bound function. * @example * @@ -39397,16 +41572,14 @@ return jQuery; * bound('hi'); * // => 'hiya fred!' */ - function bindKey(object, key) { + var bindKey = restParam(function(object, key, partials) { var bitmask = BIND_FLAG | BIND_KEY_FLAG; - if (arguments.length > 2) { - var partials = baseSlice(arguments, 2), - holders = replaceHolders(partials, bindKey.placeholder); - + if (partials.length) { + var holders = replaceHolders(partials, bindKey.placeholder); bitmask |= PARTIAL_FLAG; } return createWrapper(key, bitmask, object, partials, holders); - } + }); /** * Creates a function that accepts one or more arguments of `func` that when @@ -39448,14 +41621,7 @@ return jQuery; * curried(1)(_, 3)(2); * // => [1, 2, 3] */ - function curry(func, arity, guard) { - if (guard && isIterateeCall(func, arity, guard)) { - arity = null; - } - var result = createWrapper(func, CURRY_FLAG, null, null, null, null, null, arity); - result.placeholder = curry.placeholder; - return result; - } + var curry = createCurry(CURRY_FLAG); /** * This method is like `_.curry` except that arguments are applied to `func` @@ -39494,14 +41660,7 @@ return jQuery; * curried(3)(1, _)(2); * // => [1, 2, 3] */ - function curryRight(func, arity, guard) { - if (guard && isIterateeCall(func, arity, guard)) { - arity = null; - } - var result = createWrapper(func, CURRY_RIGHT_FLAG, null, null, null, null, null, arity); - result.placeholder = curryRight.placeholder; - return result; - } + var curryRight = createCurry(CURRY_RIGHT_FLAG); /** * Creates a function that delays invoking `func` until after `wait` milliseconds @@ -39696,9 +41855,9 @@ return jQuery; * }, 'deferred'); * // logs 'deferred' after one or more milliseconds */ - function defer(func) { - return baseDelay(func, 1, arguments, 1); - } + var defer = restParam(function(func, args) { + return baseDelay(func, 1, args); + }); /** * Invokes `func` after `wait` milliseconds. Any additional arguments are @@ -39718,9 +41877,9 @@ return jQuery; * }, 1000, 'later'); * // => logs 'later' after one second */ - function delay(func, wait) { - return baseDelay(func, wait, arguments, 2); - } + var delay = restParam(function(func, wait, args) { + return baseDelay(func, wait, args); + }); /** * Creates a function that returns the result of invoking the provided @@ -39742,7 +41901,7 @@ return jQuery; * addSquare(1, 2); * // => 9 */ - var flow = createComposer(); + var flow = createFlow(); /** * This method is like `_.flow` except that it creates a function that @@ -39764,7 +41923,7 @@ return jQuery; * addSquare(1, 2); * // => 9 */ - var flowRight = createComposer(true); + var flowRight = createFlow(true); /** * Creates a function that memoizes the result of `func`. If `resolver` is @@ -39776,10 +41935,8 @@ return jQuery; * * **Note:** The cache is exposed as the `cache` property on the memoized * function. Its creation may be customized by replacing the `_.memoize.Cache` - * constructor with one whose instances implement the ES `Map` method interface - * of `get`, `has`, and `set`. See the - * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-properties-of-the-map-prototype-object) - * for more details. + * constructor with one whose instances implement the [`Map`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-properties-of-the-map-prototype-object) + * method interface of `get`, `has`, and `set`. * * @static * @memberOf _ @@ -39870,7 +42027,7 @@ return jQuery; /** * Creates a function that is restricted to invoking `func` once. Repeat calls * to the function return the value of the first call. The `func` is invoked - * with the `this` binding of the created function. + * with the `this` binding and arguments of the created function. * * @static * @memberOf _ @@ -39903,7 +42060,7 @@ return jQuery; * @memberOf _ * @category Function * @param {Function} func The function to partially apply arguments to. - * @param {...*} [args] The arguments to be partially applied. + * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new partially applied function. * @example * @@ -39920,12 +42077,7 @@ return jQuery; * greetFred('hi'); * // => 'hi fred' */ - function partial(func) { - var partials = baseSlice(arguments, 1), - holders = replaceHolders(partials, partial.placeholder); - - return createWrapper(func, PARTIAL_FLAG, null, partials, holders); - } + var partial = createPartial(PARTIAL_FLAG); /** * This method is like `_.partial` except that partially applied arguments @@ -39941,7 +42093,7 @@ return jQuery; * @memberOf _ * @category Function * @param {Function} func The function to partially apply arguments to. - * @param {...*} [args] The arguments to be partially applied. + * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new partially applied function. * @example * @@ -39958,12 +42110,7 @@ return jQuery; * sayHelloTo('fred'); * // => 'hello fred' */ - function partialRight(func) { - var partials = baseSlice(arguments, 1), - holders = replaceHolders(partials, partialRight.placeholder); - - return createWrapper(func, PARTIAL_RIGHT_FLAG, null, partials, holders); - } + var partialRight = createPartial(PARTIAL_RIGHT_FLAG); /** * Creates a function that invokes `func` with arguments arranged according @@ -39993,29 +42140,80 @@ return jQuery; * }, [1, 2, 3]); * // => [3, 6, 9] */ - function rearg(func) { - var indexes = baseFlatten(arguments, false, false, 1); - return createWrapper(func, REARG_FLAG, null, null, null, indexes); - } + var rearg = restParam(function(func, indexes) { + return createWrapper(func, REARG_FLAG, null, null, null, baseFlatten(indexes)); + }); /** * Creates a function that invokes `func` with the `this` binding of the - * created function and the array of arguments provided to the created - * function much like [Function#apply](http://es5.github.io/#x15.3.4.3). + * created function and arguments from `start` and beyond provided as an array. + * + * **Note:** This method is based on the [rest parameter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters). + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @returns {Function} Returns the new function. + * @example + * + * var say = _.restParam(function(what, names) { + * return what + ' ' + _.initial(names).join(', ') + + * (_.size(names) > 1 ? ', & ' : '') + _.last(names); + * }); + * + * say('hello', 'fred', 'barney', 'pebbles'); + * // => 'hello fred, barney, & pebbles' + */ + function restParam(func, start) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + start = nativeMax(typeof start == 'undefined' ? (func.length - 1) : (+start || 0), 0); + return function() { + var args = arguments, + index = -1, + length = nativeMax(args.length - start, 0), + rest = Array(length); + + while (++index < length) { + rest[index] = args[start + index]; + } + switch (start) { + case 0: return func.call(this, rest); + case 1: return func.call(this, args[0], rest); + case 2: return func.call(this, args[0], args[1], rest); + } + var otherArgs = Array(start + 1); + index = -1; + while (++index < start) { + otherArgs[index] = args[index]; + } + otherArgs[start] = rest; + return func.apply(this, otherArgs); + }; + } + + /** + * Creates a function that invokes `func` with the `this` binding of the created + * function and an array of arguments much like [`Function#apply`](https://es5.github.io/#x15.3.4.3). + * + * **Note:** This method is based on the [spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator). * * @static * @memberOf _ * @category Function * @param {Function} func The function to spread arguments over. - * @returns {*} Returns the new function. + * @returns {Function} Returns the new function. * @example * - * var spread = _.spread(function(who, what) { + * var say = _.spread(function(who, what) { * return who + ' says ' + what; * }); * - * spread(['Fred', 'hello']); - * // => 'Fred says hello' + * say(['fred', 'hello']); + * // => 'fred says hello' * * // with a Promise * var numbers = Promise.all([ @@ -40130,12 +42328,12 @@ return jQuery; * cloning is handled by the method instead. The `customizer` is bound to * `thisArg` and invoked with two argument; (value [, index|key, object]). * - * **Note:** This method is loosely based on the structured clone algorithm. + * **Note:** This method is loosely based on the + * [structured clone algorithm](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm). * The enumerable properties of `arguments` objects and objects created by * constructors other than `Object` are cloned to plain `Object` objects. An * empty object is returned for uncloneable values such as functions, DOM nodes, - * Maps, Sets, and WeakMaps. See the [HTML5 specification](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm) - * for more details. + * Maps, Sets, and WeakMaps. * * @static * @memberOf _ @@ -40193,12 +42391,12 @@ return jQuery; * is handled by the method instead. The `customizer` is bound to `thisArg` * and invoked with two argument; (value [, index|key, object]). * - * **Note:** This method is loosely based on the structured clone algorithm. + * **Note:** This method is loosely based on the + * [structured clone algorithm](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm). * The enumerable properties of `arguments` objects and objects created by * constructors other than `Object` are cloned to plain `Object` objects. An * empty object is returned for uncloneable values such as functions, DOM nodes, - * Maps, Sets, and WeakMaps. See the [HTML5 specification](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm) - * for more details. + * Maps, Sets, and WeakMaps. * * @static * @memberOf _ @@ -40255,7 +42453,7 @@ return jQuery; */ function isArguments(value) { var length = isObjectLike(value) ? value.length : undefined; - return (isLength(length) && objToString.call(value) == argsTag) || false; + return isLength(length) && objToString.call(value) == argsTag; } /** @@ -40275,7 +42473,7 @@ return jQuery; * // => false */ var isArray = nativeIsArray || function(value) { - return (isObjectLike(value) && isLength(value.length) && objToString.call(value) == arrayTag) || false; + return isObjectLike(value) && isLength(value.length) && objToString.call(value) == arrayTag; }; /** @@ -40295,7 +42493,7 @@ return jQuery; * // => false */ function isBoolean(value) { - return (value === true || value === false || isObjectLike(value) && objToString.call(value) == boolTag) || false; + return value === true || value === false || (isObjectLike(value) && objToString.call(value) == boolTag); } /** @@ -40315,7 +42513,7 @@ return jQuery; * // => false */ function isDate(value) { - return (isObjectLike(value) && objToString.call(value) == dateTag) || false; + return isObjectLike(value) && objToString.call(value) == dateTag; } /** @@ -40335,13 +42533,13 @@ return jQuery; * // => false */ function isElement(value) { - return (value && value.nodeType === 1 && isObjectLike(value) && - (objToString.call(value).indexOf('Element') > -1)) || false; + return !!value && value.nodeType === 1 && isObjectLike(value) && + (objToString.call(value).indexOf('Element') > -1); } // Fallback for environments without DOM support. if (!support.dom) { isElement = function(value) { - return (value && value.nodeType === 1 && isObjectLike(value) && !isPlainObject(value)) || false; + return !!value && value.nodeType === 1 && isObjectLike(value) && !isPlainObject(value); }; } @@ -40389,7 +42587,7 @@ return jQuery; * equivalent. If `customizer` is provided it is invoked to compare values. * If `customizer` returns `undefined` comparisons are handled by the method * instead. The `customizer` is bound to `thisArg` and invoked with three - * arguments; (value, other [, index|key]). + * arguments: (value, other [, index|key]). * * **Note:** This method supports comparing arrays, booleans, `Date` objects, * numbers, `Object` objects, regexes, and strings. Objects are compared by @@ -40454,15 +42652,13 @@ return jQuery; * // => false */ function isError(value) { - return (isObjectLike(value) && typeof value.message == 'string' && objToString.call(value) == errorTag) || false; + return isObjectLike(value) && typeof value.message == 'string' && objToString.call(value) == errorTag; } /** * Checks if `value` is a finite primitive number. * - * **Note:** This method is based on ES `Number.isFinite`. See the - * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-number.isfinite) - * for more details. + * **Note:** This method is based on [`Number.isFinite`](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-number.isfinite). * * @static * @memberOf _ @@ -40514,11 +42710,9 @@ return jQuery; }; /** - * Checks if `value` is the language type of `Object`. + * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`. * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) * - * **Note:** See the [ES5 spec](https://es5.github.io/#x8) for more details. - * * @static * @memberOf _ * @category Lang @@ -40539,7 +42733,7 @@ return jQuery; // Avoid a V8 JIT bug in Chrome 19-20. // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. var type = typeof value; - return type == 'function' || (value && type == 'object') || false; + return type == 'function' || (!!value && type == 'object'); } /** @@ -40547,7 +42741,7 @@ return jQuery; * `object` contains equivalent property values. If `customizer` is provided * it is invoked to compare values. If `customizer` returns `undefined` * comparisons are handled by the method instead. The `customizer` is bound - * to `thisArg` and invoked with three arguments; (value, other, index|key). + * to `thisArg` and invoked with three arguments: (value, other, index|key). * * **Note:** This method supports comparing properties of arrays, booleans, * `Date` objects, numbers, `Object` objects, regexes, and strings. Functions @@ -40585,13 +42779,19 @@ return jQuery; var props = keys(source), length = props.length; + if (!length) { + return true; + } + if (object == null) { + return false; + } customizer = typeof customizer == 'function' && bindCallback(customizer, thisArg, 3); if (!customizer && length == 1) { var key = props[0], value = source[key]; if (isStrictComparable(value)) { - return object != null && value === object[key] && hasOwnProperty.call(object, key); + return value === object[key] && (typeof value != 'undefined' || (key in toObject(object))); } } var values = Array(length), @@ -40601,15 +42801,14 @@ return jQuery; value = values[length] = source[props[length]]; strictCompareFlags[length] = isStrictComparable(value); } - return baseIsMatch(object, props, values, strictCompareFlags, customizer); + return baseIsMatch(toObject(object), props, values, strictCompareFlags, customizer); } /** * Checks if `value` is `NaN`. * - * **Note:** This method is not the same as native `isNaN` which returns `true` - * for `undefined` and other non-numeric values. See the [ES5 spec](https://es5.github.io/#x15.1.2.4) - * for more details. + * **Note:** This method is not the same as [`isNaN`](https://es5.github.io/#x15.1.2.4) + * which returns `true` for `undefined` and other non-numeric values. * * @static * @memberOf _ @@ -40659,7 +42858,7 @@ return jQuery; if (objToString.call(value) == funcTag) { return reNative.test(fnToString.call(value)); } - return (isObjectLike(value) && reHostCtor.test(value)) || false; + return isObjectLike(value) && reHostCtor.test(value); } /** @@ -40705,7 +42904,7 @@ return jQuery; * // => false */ function isNumber(value) { - return typeof value == 'number' || (isObjectLike(value) && objToString.call(value) == numberTag) || false; + return typeof value == 'number' || (isObjectLike(value) && objToString.call(value) == numberTag); } /** @@ -40787,7 +42986,7 @@ return jQuery; * // => false */ function isString(value) { - return typeof value == 'string' || (isObjectLike(value) && objToString.call(value) == stringTag) || false; + return typeof value == 'string' || (isObjectLike(value) && objToString.call(value) == stringTag); } /** @@ -40807,7 +43006,7 @@ return jQuery; * // => false */ function isTypedArray(value) { - return (isObjectLike(value) && isLength(value.length) && typedArrayTags[objToString.call(value)]) || false; + return isObjectLike(value) && isLength(value.length) && !!typedArrayTags[objToString.call(value)]; } /** @@ -40889,7 +43088,7 @@ return jQuery; * Assigns own enumerable properties of source object(s) to the destination * object. Subsequent sources overwrite property assignments of previous sources. * If `customizer` is provided it is invoked to produce the assigned values. - * The `customizer` is bound to `thisArg` and invoked with five arguments; + * The `customizer` is bound to `thisArg` and invoked with five arguments: * (objectValue, sourceValue, key, object, source). * * @static @@ -40974,18 +43173,18 @@ return jQuery; * _.defaults({ 'user': 'barney' }, { 'age': 36 }, { 'user': 'fred' }); * // => { 'user': 'barney', 'age': 36 } */ - function defaults(object) { + var defaults = restParam(function(args) { + var object = args[0]; if (object == null) { return object; } - var args = arrayCopy(arguments); args.push(assignDefaults); return assign.apply(undefined, args); - } + }); /** - * This method is like `_.findIndex` except that it returns the key of the - * first element `predicate` returns truthy for, instead of the element itself. + * This method is like `_.find` except that it returns the key of the first + * element `predicate` returns truthy for instead of the element itself. * * If a property name is provided for `predicate` the created `_.property` * style callback returns the property value of the given element. @@ -41031,10 +43230,7 @@ return jQuery; * _.findKey(users, 'active'); * // => 'barney' */ - function findKey(object, predicate, thisArg) { - predicate = getCallback(predicate, thisArg, 3); - return baseFind(object, predicate, baseForOwn, true); - } + var findKey = createFindKey(baseForOwn); /** * This method is like `_.findKey` except that it iterates over elements of @@ -41084,15 +43280,12 @@ return jQuery; * _.findLastKey(users, 'active'); * // => 'pebbles' */ - function findLastKey(object, predicate, thisArg) { - predicate = getCallback(predicate, thisArg, 3); - return baseFind(object, predicate, baseForOwnRight, true); - } + var findLastKey = createFindKey(baseForOwnRight); /** * Iterates over own and inherited enumerable properties of an object invoking * `iteratee` for each property. The `iteratee` is bound to `thisArg` and invoked - * with three arguments; (value, key, object). Iterator functions may exit + * with three arguments: (value, key, object). Iterator functions may exit * iteration early by explicitly returning `false`. * * @static @@ -41116,12 +43309,7 @@ return jQuery; * }); * // => logs 'a', 'b', and 'c' (iteration order is not guaranteed) */ - function forIn(object, iteratee, thisArg) { - if (typeof iteratee != 'function' || typeof thisArg != 'undefined') { - iteratee = bindCallback(iteratee, thisArg, 3); - } - return baseFor(object, iteratee, keysIn); - } + var forIn = createForIn(baseFor); /** * This method is like `_.forIn` except that it iterates over properties of @@ -41148,15 +43336,12 @@ return jQuery; * }); * // => logs 'c', 'b', and 'a' assuming `_.forIn ` logs 'a', 'b', and 'c' */ - function forInRight(object, iteratee, thisArg) { - iteratee = bindCallback(iteratee, thisArg, 3); - return baseForRight(object, iteratee, keysIn); - } + var forInRight = createForIn(baseForRight); /** * Iterates over own enumerable properties of an object invoking `iteratee` * for each property. The `iteratee` is bound to `thisArg` and invoked with - * three arguments; (value, key, object). Iterator functions may exit iteration + * three arguments: (value, key, object). Iterator functions may exit iteration * early by explicitly returning `false`. * * @static @@ -41180,12 +43365,7 @@ return jQuery; * }); * // => logs 'a' and 'b' (iteration order is not guaranteed) */ - function forOwn(object, iteratee, thisArg) { - if (typeof iteratee != 'function' || typeof thisArg != 'undefined') { - iteratee = bindCallback(iteratee, thisArg, 3); - } - return baseForOwn(object, iteratee); - } + var forOwn = createForOwn(baseForOwn); /** * This method is like `_.forOwn` except that it iterates over properties of @@ -41212,10 +43392,7 @@ return jQuery; * }); * // => logs 'b' and 'a' assuming `_.forOwn` logs 'a' and 'b' */ - function forOwnRight(object, iteratee, thisArg) { - iteratee = bindCallback(iteratee, thisArg, 3); - return baseForRight(object, iteratee, keys); - } + var forOwnRight = createForOwn(baseForOwnRight); /** * Creates an array of function property names from all enumerable properties, @@ -41400,7 +43577,7 @@ return jQuery; /** * Creates an object with the same keys as `object` and values generated by * running each own enumerable property of `object` through `iteratee`. The - * iteratee function is bound to `thisArg` and invoked with three arguments; + * iteratee function is bound to `thisArg` and invoked with three arguments: * (value, key, object). * * If a property name is provided for `iteratee` the created `_.property` @@ -41455,7 +43632,7 @@ return jQuery; * provided it is invoked to produce the merged values of the destination and * source properties. If `customizer` returns `undefined` merging is handled * by the method instead. The `customizer` is bound to `thisArg` and invoked - * with five arguments; (objectValue, sourceValue, key, object, source). + * with five arguments: (objectValue, sourceValue, key, object, source). * * @static * @memberOf _ @@ -41504,7 +43681,7 @@ return jQuery; * Property names may be specified as individual arguments or as arrays of * property names. If `predicate` is provided it is invoked for each property * of `object` omitting the properties `predicate` returns truthy for. The - * predicate is bound to `thisArg` and invoked with three arguments; + * predicate is bound to `thisArg` and invoked with three arguments: * (value, key, object). * * @static @@ -41526,19 +43703,19 @@ return jQuery; * _.omit(object, _.isNumber); * // => { 'user': 'fred' } */ - function omit(object, predicate, thisArg) { + var omit = restParam(function(object, props) { if (object == null) { return {}; } - if (typeof predicate != 'function') { - var props = arrayMap(baseFlatten(arguments, false, false, 1), String); + if (typeof props[0] != 'function') { + var props = arrayMap(baseFlatten(props), String); return pickByArray(object, baseDifference(keysIn(object), props)); } - predicate = bindCallback(predicate, thisArg, 3); + var predicate = bindCallback(props[0], props[1], 3); return pickByCallback(object, function(value, key, object) { return !predicate(value, key, object); }); - } + }); /** * Creates a two dimensional array of the key-value pairs for `object`, @@ -41572,7 +43749,7 @@ return jQuery; * names may be specified as individual arguments or as arrays of property * names. If `predicate` is provided it is invoked for each property of `object` * picking the properties `predicate` returns truthy for. The predicate is - * bound to `thisArg` and invoked with three arguments; (value, key, object). + * bound to `thisArg` and invoked with three arguments: (value, key, object). * * @static * @memberOf _ @@ -41593,14 +43770,14 @@ return jQuery; * _.pick(object, _.isString); * // => { 'user': 'fred' } */ - function pick(object, predicate, thisArg) { + var pick = restParam(function(object, props) { if (object == null) { return {}; } - return typeof predicate == 'function' - ? pickByCallback(object, bindCallback(predicate, thisArg, 3)) - : pickByArray(object, baseFlatten(arguments, false, false, 1)); - } + return typeof props[0] == 'function' + ? pickByCallback(object, bindCallback(props[0], props[1], 3)) + : pickByArray(object, baseFlatten(props)); + }); /** * Resolves the value of property `key` on `object`. If the value of `key` is @@ -41645,7 +43822,7 @@ return jQuery; * `accumulator` object which is the result of running each of its own enumerable * properties through `iteratee`, with each invocation potentially mutating * the `accumulator` object. The `iteratee` is bound to `thisArg` and invoked - * with four arguments; (accumulator, value, key, object). Iterator functions + * with four arguments: (accumulator, value, key, object). Iterator functions * may exit iteration early by explicitly returning `false`. * * @static @@ -41856,8 +44033,7 @@ return jQuery; /*------------------------------------------------------------------------*/ /** - * Converts `string` to camel case. - * See [Wikipedia](https://en.wikipedia.org/wiki/CamelCase) for more details. + * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase). * * @static * @memberOf _ @@ -41899,9 +44075,8 @@ return jQuery; } /** - * Deburrs `string` by converting latin-1 supplementary letters to basic latin letters. - * See [Wikipedia](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) - * for more details. + * Deburrs `string` by converting [latin-1 supplementary letters](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) + * to basic latin letters and removing [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). * * @static * @memberOf _ @@ -41915,7 +44090,7 @@ return jQuery; */ function deburr(string) { string = baseToString(string); - return string && string.replace(reLatin1, deburrLetter); + return string && string.replace(reLatin1, deburrLetter).replace(reComboMarks, ''); } /** @@ -41970,9 +44145,8 @@ return jQuery; * [#108](https://html5sec.org/#108), and [#133](https://html5sec.org/#133) of * the [HTML5 Security Cheatsheet](https://html5sec.org/) for more details. * - * When working with HTML you should always quote attribute values to reduce - * XSS vectors. See [Ryan Grove's article](http://wonko.com/post/html-escaping) - * for more details. + * When working with HTML you should always [quote attribute values](http://wonko.com/post/html-escaping) + * to reduce XSS vectors. * * @static * @memberOf _ @@ -41993,8 +44167,8 @@ return jQuery; } /** - * Escapes the `RegExp` special characters "\", "^", "$", ".", "|", "?", "*", - * "+", "(", ")", "[", "]", "{" and "}" in `string`. + * Escapes the `RegExp` special characters "\", "/", "^", "$", ".", "|", "?", + * "*", "+", "(", ")", "[", "]", "{" and "}" in `string`. * * @static * @memberOf _ @@ -42004,7 +44178,7 @@ return jQuery; * @example * * _.escapeRegExp('[lodash](https://lodash.com/)'); - * // => '\[lodash\]\(https://lodash\.com/\)' + * // => '\[lodash\]\(https:\/\/lodash\.com\/\)' */ function escapeRegExp(string) { string = baseToString(string); @@ -42014,9 +44188,7 @@ return jQuery; } /** - * Converts `string` to kebab case. - * See [Wikipedia](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles) for - * more details. + * Converts `string` to [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles). * * @static * @memberOf _ @@ -42039,9 +44211,8 @@ return jQuery; }); /** - * Pads `string` on the left and right sides if it is shorter then the given - * padding length. The `chars` string may be truncated if the number of padding - * characters can't be evenly divided by the padding length. + * Pads `string` on the left and right sides if it is shorter than `length`. + * Padding characters are truncated if they can't be evenly divided by `length`. * * @static * @memberOf _ @@ -42073,14 +44244,13 @@ return jQuery; leftLength = floor(mid), rightLength = ceil(mid); - chars = createPad('', rightLength, chars); + chars = createPadding('', rightLength, chars); return chars.slice(0, leftLength) + string + chars; } /** - * Pads `string` on the left side if it is shorter then the given padding - * length. The `chars` string may be truncated if the number of padding - * characters exceeds the padding length. + * Pads `string` on the left side if it is shorter than `length`. Padding + * characters are truncated if they exceed `length`. * * @static * @memberOf _ @@ -42100,15 +44270,11 @@ return jQuery; * _.padLeft('abc', 3); * // => 'abc' */ - function padLeft(string, length, chars) { - string = baseToString(string); - return string && (createPad(string, length, chars) + string); - } + var padLeft = createPadDir(); /** - * Pads `string` on the right side if it is shorter then the given padding - * length. The `chars` string may be truncated if the number of padding - * characters exceeds the padding length. + * Pads `string` on the right side if it is shorter than `length`. Padding + * characters are truncated if they exceed `length`. * * @static * @memberOf _ @@ -42128,18 +44294,15 @@ return jQuery; * _.padRight('abc', 3); * // => 'abc' */ - function padRight(string, length, chars) { - string = baseToString(string); - return string && (string + createPad(string, length, chars)); - } + var padRight = createPadDir(true); /** * Converts `string` to an integer of the specified radix. If `radix` is * `undefined` or `0`, a `radix` of `10` is used unless `value` is a hexadecimal, * in which case a `radix` of `16` is used. * - * **Note:** This method aligns with the ES5 implementation of `parseInt`. - * See the [ES5 spec](https://es5.github.io/#E) for more details. + * **Note:** This method aligns with the [ES5 implementation](https://es5.github.io/#E) + * of `parseInt`. * * @static * @memberOf _ @@ -42219,8 +44382,7 @@ return jQuery; } /** - * Converts `string` to snake case. - * See [Wikipedia](https://en.wikipedia.org/wiki/Snake_case) for more details. + * Converts `string` to [snake case](https://en.wikipedia.org/wiki/Snake_case). * * @static * @memberOf _ @@ -42243,9 +44405,7 @@ return jQuery; }); /** - * Converts `string` to start case. - * See [Wikipedia](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage) - * for more details. + * Converts `string` to [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage). * * @static * @memberOf _ @@ -42304,9 +44464,9 @@ return jQuery; * properties may be accessed as free variables in the template. If a setting * object is provided it takes precedence over `_.templateSettings` values. * - * **Note:** In the development build `_.template` utilizes sourceURLs for easier debugging. - * See the [HTML5 Rocks article on sourcemaps](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) - * for more details. + * **Note:** In the development build `_.template` utilizes + * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) + * for easier debugging. * * For more information on precompiling templates see * [lodash's custom builds documentation](https://lodash.com/custom-builds). @@ -42518,7 +44678,7 @@ return jQuery; * // => 'abc' * * _.map([' foo ', ' bar '], _.trim); - * // => ['foo', 'bar] + * // => ['foo', 'bar'] */ function trim(string, chars, guard) { var value = string; @@ -42626,7 +44786,7 @@ return jQuery; * 'length': 24, * 'separator': /,? +/ * }); - * //=> 'hi-diddly-ho there...' + * // => 'hi-diddly-ho there...' * * _.trunc('hi-diddly-ho there, neighborino', { * 'omission': ' [...]' @@ -42745,7 +44905,7 @@ return jQuery; * @static * @memberOf _ * @category Utility - * @param {*} func The function to attempt. + * @param {Function} func The function to attempt. * @returns {*} Returns the `func` result or error object. * @example * @@ -42758,20 +44918,13 @@ return jQuery; * elements = []; * } */ - function attempt() { - var func = arguments[0], - length = arguments.length, - args = Array(length ? (length - 1) : 0); - - while (--length > 0) { - args[length - 1] = arguments[length]; - } + var attempt = restParam(function(func, args) { try { return func.apply(undefined, args); } catch(e) { return isError(e) ? e : new Error(e); } - } + }); /** * Creates a function that invokes `func` with the `this` binding of `thisArg` @@ -42908,12 +45061,11 @@ return jQuery; * * var users = [ * { 'user': 'barney' }, - * { 'user': 'fred' }, - * { 'user': 'pebbles' } + * { 'user': 'fred' } * ]; * * _.find(users, _.matchesProperty('user', 'fred')); - * // => { 'user': 'fred', 'age': 40 } + * // => { 'user': 'fred' } */ function matchesProperty(key, value) { return baseMatchesProperty(key + '', baseClone(value, true)); @@ -42924,6 +45076,9 @@ return jQuery; * destination object. If `object` is a function then methods are added to * its prototype as well. * + * **Note:** Use `_.runInContext` to create a pristine `lodash` function + * for mixins to avoid conflicts caused by modifying the original. + * * @static * @memberOf _ * @category Utility @@ -42941,7 +45096,7 @@ return jQuery; * }); * } * - * // use `_.runInContext` to avoid potential conflicts (esp. in Node.js) + * // use `_.runInContext` to avoid conflicts (esp. in Node.js) * var _ = require('lodash').runInContext(); * * _.mixin({ 'vowels': vowels }); @@ -42991,12 +45146,10 @@ return jQuery; return function() { var chainAll = this.__chain__; if (chain || chainAll) { - var result = object(this.__wrapped__); - (result.__actions__ = arrayCopy(this.__actions__)).push({ - 'func': func, - 'args': arguments, - 'thisArg': object - }); + var result = object(this.__wrapped__), + actions = result.__actions__ = arrayCopy(this.__actions__); + + actions.push({ 'func': func, 'args': arguments, 'thisArg': object }); result.__chain__ = chainAll; return result; } @@ -43063,7 +45216,7 @@ return jQuery; * var getName = _.property('user'); * * _.map(users, getName); - * // => ['fred', barney'] + * // => ['fred', 'barney'] * * _.pluck(_.sortBy(users, getName), 'user'); * // => ['barney', 'fred'] @@ -43073,7 +45226,7 @@ return jQuery; } /** - * The inverse of `_.property`; this method creates a function which returns + * The opposite of `_.property`; this method creates a function which returns * the property value of a given key on `object`. * * @static @@ -43251,16 +45404,16 @@ return jQuery; * `-Infinity` is returned. If an iteratee function is provided it is invoked * for each value in `collection` to generate the criterion by which the value * is ranked. The `iteratee` is bound to `thisArg` and invoked with three - * arguments; (value, index, collection). + * arguments: (value, index, collection). * - * If a property name is provided for `predicate` the created `_.property` + * If a property name is provided for `iteratee` the created `_.property` * style callback returns the property value of the given element. * * If a value is also provided for `thisArg` the created `_.matchesProperty` * style callback returns `true` for elements that have a matching property * value, else `false`. * - * If an object is provided for `predicate` the created `_.matches` style + * If an object is provided for `iteratee` the created `_.matches` style * callback returns `true` for elements that have the properties of the given * object, else `false`. * @@ -43287,11 +45440,11 @@ return jQuery; * _.max(users, function(chr) { * return chr.age; * }); - * // => { 'user': 'fred', 'age': 40 }; + * // => { 'user': 'fred', 'age': 40 } * * // using the `_.property` callback shorthand * _.max(users, 'age'); - * // => { 'user': 'fred', 'age': 40 }; + * // => { 'user': 'fred', 'age': 40 } */ var max = createExtremum(arrayMax); @@ -43300,16 +45453,16 @@ return jQuery; * `Infinity` is returned. If an iteratee function is provided it is invoked * for each value in `collection` to generate the criterion by which the value * is ranked. The `iteratee` is bound to `thisArg` and invoked with three - * arguments; (value, index, collection). + * arguments: (value, index, collection). * - * If a property name is provided for `predicate` the created `_.property` + * If a property name is provided for `iteratee` the created `_.property` * style callback returns the property value of the given element. * * If a value is also provided for `thisArg` the created `_.matchesProperty` * style callback returns `true` for elements that have a matching property * value, else `false`. * - * If an object is provided for `predicate` the created `_.matches` style + * If an object is provided for `iteratee` the created `_.matches` style * callback returns `true` for elements that have the properties of the given * object, else `false`. * @@ -43336,11 +45489,11 @@ return jQuery; * _.min(users, function(chr) { * return chr.age; * }); - * // => { 'user': 'barney', 'age': 36 }; + * // => { 'user': 'barney', 'age': 36 } * * // using the `_.property` callback shorthand * _.min(users, 'age'); - * // => { 'user': 'barney', 'age': 36 }; + * // => { 'user': 'barney', 'age': 36 } */ var min = createExtremum(arrayMin, true); @@ -43351,26 +45504,45 @@ return jQuery; * @memberOf _ * @category Math * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. * @returns {number} Returns the sum. * @example * - * _.sum([4, 6, 2]); - * // => 12 + * _.sum([4, 6]); + * // => 10 + * + * _.sum({ 'a': 4, 'b': 6 }); + * // => 10 * - * _.sum({ 'a': 4, 'b': 6, 'c': 2 }); - * // => 12 + * var objects = [ + * { 'n': 4 }, + * { 'n': 6 } + * ]; + * + * _.sum(objects, function(object) { + * return object.n; + * }); + * // => 10 + * + * // using the `_.property` callback shorthand + * _.sum(objects, 'n'); + * // => 10 */ - function sum(collection) { - if (!isArray(collection)) { - collection = toIterable(collection); + function sum(collection, iteratee, thisArg) { + if (thisArg && isIterateeCall(collection, iteratee, thisArg)) { + iteratee = null; } - var length = collection.length, - result = 0; + var func = getCallback(), + noIteratee = iteratee == null; - while (length--) { - result += +collection[length] || 0; + if (!(func === baseCallback && noIteratee)) { + noIteratee = false; + iteratee = func(iteratee, thisArg, 3); } - return result; + return noIteratee + ? arraySum(isArray(collection) ? collection : toIterable(collection)) + : baseSum(collection, iteratee); } /*------------------------------------------------------------------------*/ @@ -43469,6 +45641,7 @@ return jQuery; lodash.reject = reject; lodash.remove = remove; lodash.rest = rest; + lodash.restParam = restParam; lodash.shuffle = shuffle; lodash.slice = slice; lodash.sortBy = sortBy; @@ -43760,8 +45933,11 @@ return jQuery; // Add `LazyWrapper` methods to `lodash.prototype`. baseForOwn(LazyWrapper.prototype, function(func, methodName) { - var lodashFunc = lodash[methodName], - checkIteratee = /^(?:filter|map|reject)|While$/.test(methodName), + var lodashFunc = lodash[methodName]; + if (!lodashFunc) { + return; + } + var checkIteratee = /^(?:filter|map|reject)|While$/.test(methodName), retUnwrapped = /^(?:first|last)$/.test(methodName); lodash.prototype[methodName] = function() { @@ -43820,6 +45996,19 @@ return jQuery; }; }); + // Map minified function names to their real names. + baseForOwn(LazyWrapper.prototype, function(func, methodName) { + var lodashFunc = lodash[methodName]; + if (lodashFunc) { + var key = lodashFunc.name, + names = realNames[key] || (realNames[key] = []); + + names.push({ 'name': methodName, 'func': lodashFunc }); + } + }); + + realNames[createHybridWrapper(null, BIND_KEY_FLAG).name] = [{ 'name': 'wrapper', 'func': null }]; + // Add functions to the lazy wrapper. LazyWrapper.prototype.clone = lazyClone; LazyWrapper.prototype.reverse = lazyReverse; @@ -43893,13 +46082,13 @@ exports.HashLocation = require("./locations/HashLocation"); exports.HistoryLocation = require("./locations/HistoryLocation"); exports.RefreshLocation = require("./locations/RefreshLocation"); exports.StaticLocation = require("./locations/StaticLocation"); +exports.TestLocation = require("./locations/TestLocation"); exports.ImitateBrowserBehavior = require("./behaviors/ImitateBrowserBehavior"); exports.ScrollToTopBehavior = require("./behaviors/ScrollToTopBehavior"); exports.History = require("./History"); exports.Navigation = require("./Navigation"); -exports.RouteHandlerMixin = require("./RouteHandlerMixin"); exports.State = require("./State"); exports.createRoute = require("./Route").createRoute; @@ -43909,10 +46098,10 @@ exports.createRedirect = require("./Route").createRedirect; exports.createRoutesFromReactChildren = require("./createRoutesFromReactChildren"); exports.create = require("./createRouter"); exports.run = require("./runRouter"); -},{"./History":6,"./Navigation":8,"./Route":13,"./RouteHandlerMixin":14,"./State":16,"./behaviors/ImitateBrowserBehavior":20,"./behaviors/ScrollToTopBehavior":21,"./components/DefaultRoute":22,"./components/Link":23,"./components/NotFoundRoute":24,"./components/Redirect":25,"./components/Route":26,"./components/RouteHandler":27,"./createRouter":28,"./createRoutesFromReactChildren":29,"./locations/HashLocation":32,"./locations/HistoryLocation":33,"./locations/RefreshLocation":34,"./locations/StaticLocation":35,"./runRouter":36}],"react/addons":[function(require,module,exports){ +},{"./History":10,"./Navigation":12,"./Route":16,"./State":18,"./behaviors/ImitateBrowserBehavior":21,"./behaviors/ScrollToTopBehavior":22,"./components/DefaultRoute":24,"./components/Link":25,"./components/NotFoundRoute":26,"./components/Redirect":27,"./components/Route":28,"./components/RouteHandler":29,"./createRouter":30,"./createRoutesFromReactChildren":31,"./locations/HashLocation":34,"./locations/HistoryLocation":35,"./locations/RefreshLocation":36,"./locations/StaticLocation":37,"./locations/TestLocation":38,"./runRouter":39}],"react/addons":[function(require,module,exports){ module.exports = require('./lib/ReactWithAddons'); -},{"./lib/ReactWithAddons":133}],"react":[function(require,module,exports){ +},{"./lib/ReactWithAddons":141}],"react":[function(require,module,exports){ module.exports = require('./lib/React'); -},{"./lib/React":72}]},{},["jquery","lodash","react","react-router","flux","react/addons"]); +},{"./lib/React":71}]},{},["flux","jquery","lodash","react","react-router","react/addons"]); |