kjs_events.cpp   [plain text]


/*
 *  This file is part of the KDE libraries
 *  Copyright (C) 2001 Peter Kelly (pmk@post.com)
 *  Copyright (C) 2003, 2004, 2005, 2006 Apple Computer, Inc.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "config.h"
#include "kjs_events.h"

#include "Clipboard.h"
#include "ClipboardEvent.h"
#include "Document.h"
#include "Event.h"
#include "EventNames.h"
#include "Frame.h"
#include "HTMLImageElement.h"
#include "HTMLNames.h"
#include "JSEvent.h"
#include "JSKeyboardEvent.h"
#include "JSMouseEvent.h"
#include "JSMutationEvent.h"
#include "JSOverflowEvent.h"
#include "JSWheelEvent.h"
#include "KeyboardEvent.h"
#include "MouseEvent.h"
#include "MutationEvent.h"
#include "OverflowEvent.h"
#include "UIEvent.h"
#include "WheelEvent.h"
#include "kjs_proxy.h"
#include "kjs_window.h"

#include "kjs_events.lut.h"

using namespace WebCore;
using namespace EventNames;
using namespace HTMLNames;

namespace KJS {

JSAbstractEventListener::JSAbstractEventListener(bool _html)
    : html(_html)
{
}

void JSAbstractEventListener::handleEvent(Event* ele, bool isWindowEvent)
{
#ifdef KJS_DEBUGGER
    if (KJSDebugWin::instance() && KJSDebugWin::instance()->inSession())
        return;
#endif

    Event *event = ele;

    JSObject* listener = listenerObj();
    if (!listener)
        return;

    Window* window = windowObj();
    Frame *frame = window->frame();
    if (!frame)
        return;
    KJSProxy* proxy = frame->jScript();
    if (!proxy)
        return;

    JSLock lock;
  
    ScriptInterpreter* interpreter = proxy->interpreter();
    ExecState* exec = interpreter->globalExec();
  
    JSValue* handleEventFuncValue = listener->get(exec, "handleEvent");
    JSObject* handleEventFunc = 0;
    if (handleEventFuncValue->isObject()) {      
        handleEventFunc = static_cast<JSObject*>(handleEventFuncValue);
        if (!handleEventFunc->implementsCall())
            handleEventFunc = 0;
    }
  
    if (handleEventFunc || listener->implementsCall()) {
        ref();
      
        List args;
        args.append(toJS(exec, event));
      
        // Set the event we're handling in the Window object
        window->setCurrentEvent(event);
        // ... and in the interpreter
        interpreter->setCurrentEvent(event);
      
        JSValue* retval;
        if (handleEventFunc) {
            interpreter->startTimeoutCheck();
            retval = handleEventFunc->call(exec, listener, args);
        } else {
            JSObject* thisObj;
            if (isWindowEvent)
                thisObj = window;
            else
                thisObj = static_cast<JSObject*>(toJS(exec, event->currentTarget()));
            interpreter->startTimeoutCheck();
            retval = listener->call(exec, thisObj, args);
        }
        interpreter->stopTimeoutCheck();

        window->setCurrentEvent(0);
        interpreter->setCurrentEvent(0);

        if (exec->hadException()) {
            JSObject* exception = exec->exception()->toObject(exec);
            String message = exception->get(exec, messagePropertyName)->toString(exec);
            int lineNumber = exception->get(exec, "line")->toInt32(exec);
            String sourceURL = exception->get(exec, "sourceURL")->toString(exec);
            if (Interpreter::shouldPrintExceptions())
                printf("(event handler):%s\n", message.deprecatedString().utf8().data());
            frame->addMessageToConsole(message, lineNumber, sourceURL);
            exec->clearException();
        } else {
            if (!retval->isUndefinedOrNull() && event->storesResultAsString())
                event->storeResult(retval->toString(exec));
            if (html) {
                bool retvalbool;
                if (retval->getBoolean(retvalbool) && !retvalbool)
                    event->preventDefault();
            }
        }

        Document::updateDocumentsRendering();
        deref();
    }
}

bool JSAbstractEventListener::isHTMLEventListener() const
{
    return html;
}

// -------------------------------------------------------------------------

JSUnprotectedEventListener::JSUnprotectedEventListener(JSObject* _listener, Window* _win, bool _html)
  : JSAbstractEventListener(_html)
  , listener(_listener)
  , win(_win)
{
    if (_listener) {
        Window::UnprotectedListenersMap& listeners = _html
            ? _win->jsUnprotectedHTMLEventListeners : _win->jsUnprotectedEventListeners;
        listeners.set(_listener, this);
    }
}

JSUnprotectedEventListener::~JSUnprotectedEventListener()
{
    if (listener && win) {
        Window::UnprotectedListenersMap& listeners = isHTMLEventListener()
            ? win->jsUnprotectedHTMLEventListeners : win->jsUnprotectedEventListeners;
        listeners.remove(listener);
    }
}

JSObject* JSUnprotectedEventListener::listenerObj() const
{ 
    return listener; 
}

Window* JSUnprotectedEventListener::windowObj() const
{
    return win;
}

void JSUnprotectedEventListener::clearWindowObj()
{
    win = 0;
}

void JSUnprotectedEventListener::mark()
{
    if (listener && !listener->marked())
        listener->mark();
}

#ifndef NDEBUG
#ifndef LOG_CHANNEL_PREFIX
#define LOG_CHANNEL_PREFIX Log
#endif
WTFLogChannel LogWebCoreEventListenerLeaks = { 0x00000000, "", WTFLogChannelOn };

struct EventListenerCounter {
    static unsigned count;
    ~EventListenerCounter()
    {
        if (count)
            LOG(WebCoreEventListenerLeaks, "LEAK: %u EventListeners\n", count);
    }
};
unsigned EventListenerCounter::count = 0;
static EventListenerCounter eventListenerCounter;
#endif

// -------------------------------------------------------------------------

JSEventListener::JSEventListener(JSObject* _listener, Window* _win, bool _html)
    : JSAbstractEventListener(_html)
    , listener(_listener)
    , win(_win)
{
    if (_listener) {
        Window::ListenersMap& listeners = _html
            ? _win->jsHTMLEventListeners : _win->jsEventListeners;
        listeners.set(_listener, this);
    }
#ifndef NDEBUG
    ++eventListenerCounter.count;
#endif            
}

JSEventListener::~JSEventListener()
{
    if (listener && win) {
        Window::ListenersMap& listeners = isHTMLEventListener()
            ? win->jsHTMLEventListeners : win->jsEventListeners;
        listeners.remove(listener);
    }
#ifndef NDEBUG
    --eventListenerCounter.count;
#endif
}

JSObject* JSEventListener::listenerObj() const
{ 
    return listener; 
}

Window* JSEventListener::windowObj() const
{
    return win;
}

void JSEventListener::clearWindowObj()
{
    win = 0;
}

// -------------------------------------------------------------------------

JSLazyEventListener::JSLazyEventListener(const String& functionName, const String& code, Window* win, Node* node, int lineno)
  : JSEventListener(0, win, true)
  , m_functionName(functionName)
  , code(code)
  , parsed(false)
  , lineNumber(lineno)
  , originalNode(node)
{
    // We don't retain the original node because we assume it
    // will stay alive as long as this handler object is around
    // and we need to avoid a reference cycle. If JS transfers
    // this handler to another node, parseCode will be called and
    // then originalNode is no longer needed.
}

JSObject* JSLazyEventListener::listenerObj() const
{
    parseCode();
    return listener;
}

JSValue* JSLazyEventListener::eventParameterName() const
{
    static ProtectedPtr<JSValue> eventString = jsString("event");
    return eventString.get();
}

void JSLazyEventListener::parseCode() const
{
    if (parsed)
        return;
    parsed = true;

    Frame *frame = windowObj()->frame();
    KJSProxy *proxy = 0;
    if (frame)
        proxy = frame->jScript();

    if (proxy) {
        ScriptInterpreter* interpreter = proxy->interpreter();
        ExecState* exec = interpreter->globalExec();

        JSLock lock;
        JSObject* constr = interpreter->builtinFunction();
        List args;

        UString sourceURL(frame->url().url());
        args.append(eventParameterName());
        args.append(jsString(code));
        listener = constr->construct(exec, args, m_functionName, sourceURL, lineNumber); // ### is globalExec ok ?

        if (exec->hadException()) {
            exec->clearException();

            // failed to parse, so let's just make this listener a no-op
            listener = 0;
        } else if (originalNode) {
            // Add the event's home element to the scope
            // (and the document, and the form - see JSHTMLElement::eventHandlerScope)
            ScopeChain scope = listener->scope();

            JSValue* thisObj = toJS(exec, originalNode);
            if (thisObj->isObject()) {
                static_cast<DOMEventTargetNode*>(thisObj)->pushEventHandlerScope(exec, scope);
                listener->setScope(scope);
            }
        }
    }

    // no more need to keep the unparsed code around
    m_functionName = String();
    code = String();

    if (listener) {
        Window::ListenersMap& listeners = isHTMLEventListener()
            ? windowObj()->jsHTMLEventListeners : windowObj()->jsEventListeners;
        listeners.set(listener, const_cast<JSLazyEventListener*>(this));
    }
}

JSValue* getNodeEventListener(EventTargetNode* n, const AtomicString& eventType)
{
    if (JSAbstractEventListener* listener = static_cast<JSAbstractEventListener*>(n->getHTMLEventListener(eventType)))
        if (JSValue* obj = listener->listenerObj())
            return obj;
    return jsNull();
}

// -------------------------------------------------------------------------

const ClassInfo DOMEvent::info = { "Event", 0, &DOMEventTable, 0 };
/*
@begin DOMEventTable 12
  type          DOMEvent::Type          DontDelete|ReadOnly
  target        DOMEvent::Target        DontDelete|ReadOnly
  currentTarget DOMEvent::CurrentTarget DontDelete|ReadOnly
  srcElement    DOMEvent::SrcElement    DontDelete|ReadOnly
  eventPhase    DOMEvent::EventPhase    DontDelete|ReadOnly
  bubbles       DOMEvent::Bubbles       DontDelete|ReadOnly
  cancelable    DOMEvent::Cancelable    DontDelete|ReadOnly
  timeStamp     DOMEvent::TimeStamp     DontDelete|ReadOnly
  returnValue   DOMEvent::ReturnValue   DontDelete
  cancelBubble  DOMEvent::CancelBubble  DontDelete
  dataTransfer  DOMEvent::DataTransfer  DontDelete|ReadOnly
  clipboardData  DOMEvent::ClipboardData  DontDelete|ReadOnly
@end
@begin DOMEventProtoTable 3
  stopPropagation       DOMEvent::StopPropagation       DontDelete|Function 0
  preventDefault        DOMEvent::PreventDefault        DontDelete|Function 0
  initEvent             DOMEvent::InitEvent             DontDelete|Function 3
@end
*/
KJS_IMPLEMENT_PROTOFUNC(DOMEventProtoFunc)
KJS_IMPLEMENT_PROTOTYPE("DOMEvent", DOMEventProto, DOMEventProtoFunc)

DOMEvent::DOMEvent(ExecState *exec, Event *e)
  : m_impl(e), clipboard(0) 
{
    setPrototype(DOMEventProto::self(exec));
}

DOMEvent::~DOMEvent()
{
    ScriptInterpreter::forgetDOMObject(m_impl.get());
}

// pass marks through to JS objects we hold during garbage collection
void DOMEvent::mark()
{
    DOMObject::mark();
    if (clipboard && !clipboard->marked())
        clipboard->mark();
}

bool DOMEvent::getOwnPropertySlot(ExecState *exec, const Identifier& propertyName, PropertySlot& slot)
{
    return getStaticValueSlot<DOMEvent, DOMObject>(exec, &DOMEventTable, this, propertyName, slot);
}

JSValue *DOMEvent::getValueProperty(ExecState *exec, int token) const
{
  Event &event = *m_impl;
  switch (token) {
  case Type:
    return jsString(event.type());
  case Target:
  case SrcElement: /*MSIE extension - "the object that fired the event"*/
    return toJS(exec, event.target());
  case CurrentTarget:
    return toJS(exec, event.currentTarget());
  case EventPhase:
    return jsNumber(event.eventPhase());
  case Bubbles:
    return jsBoolean(event.bubbles());
  case CancelBubble:
    return jsBoolean(event.getCancelBubble());
  case ReturnValue:
    return jsBoolean(!event.defaultPrevented());
  case Cancelable:
    return jsBoolean(event.cancelable());
  case TimeStamp:
    return jsNumber(event.timeStamp());
  case ClipboardData:
  {
    if (event.isClipboardEvent()) {
      ClipboardEvent *impl = static_cast<ClipboardEvent *>(&event);
      if (!clipboard)
        clipboard = new Clipboard(exec, impl->clipboard());
      return clipboard;
    } else
      return jsUndefined();
  }
  case DataTransfer:
  {
    if (event.isDragEvent()) {
      MouseEvent *impl = static_cast<MouseEvent *>(&event);
      if (!clipboard)
        clipboard = new Clipboard(exec, impl->clipboard());
      return clipboard;
    } else
      return jsUndefined();
  }
  default:
    return 0;
  }
}

void DOMEvent::put(ExecState *exec, const Identifier &propertyName,
                      JSValue *value, int attr)
{
    lookupPut<DOMEvent, DOMObject>(exec, propertyName, value, attr, &DOMEventTable, this);
}

void DOMEvent::putValueProperty(ExecState *exec, int token, JSValue *value, int)
{
  Event &event = *m_impl;
  switch (token) {
  case ReturnValue:
    event.setDefaultPrevented(!value->toBoolean(exec));
    break;
  case CancelBubble:
    event.setCancelBubble(value->toBoolean(exec));
    break;
  default:
    break;
  }
}

JSValue *DOMEventProtoFunc::callAsFunction(ExecState *exec, JSObject * thisObj, const List &args)
{
  if (!thisObj->inherits(&DOMEvent::info))
    return throwError(exec, TypeError);
  Event &event = *static_cast<DOMEvent *>( thisObj )->impl();
  switch (id) {
    case DOMEvent::StopPropagation:
      event.stopPropagation();
      return jsUndefined();
    case DOMEvent::PreventDefault:
      event.preventDefault();
      return jsUndefined();
    case DOMEvent::InitEvent:
      event.initEvent(AtomicString(args[0]->toString(exec)), args[1]->toBoolean(exec), args[2]->toBoolean(exec));
      return jsUndefined();
  };
  return jsUndefined();
}

JSValue *toJS(ExecState *exec, Event *e)
{
  if (!e)
    return jsNull();
  ScriptInterpreter* interp = static_cast<ScriptInterpreter *>(exec->dynamicInterpreter());

  JSLock lock;

  DOMObject *ret = interp->getDOMObject(e);
  if (!ret) {
    if (e->isKeyboardEvent())
      ret = new JSKeyboardEvent(exec, static_cast<KeyboardEvent *>(e));
    else if (e->isMouseEvent())
      ret = new JSMouseEvent(exec, static_cast<MouseEvent *>(e));
    else if (e->isWheelEvent())
      ret = new JSWheelEvent(exec, static_cast<WheelEvent *>(e));
    else if (e->isUIEvent())
      ret = new JSUIEvent(exec, static_cast<UIEvent *>(e));
    else if (e->isMutationEvent())
      ret = new JSMutationEvent(exec, static_cast<MutationEvent *>(e));
    else if (e->isOverflowEvent())
      ret = new JSOverflowEvent(exec, static_cast<OverflowEvent*>(e));
    else
      ret = new JSEvent(exec, e);

    interp->putDOMObject(e, ret);
  }

  return ret;
}

Event *toEvent(JSValue *val)
{
    if (!val || !val->isObject(&DOMEvent::info))
        return 0;
    return static_cast<DOMEvent *>(val)->impl();
}

// -------------------------------------------------------------------------

const ClassInfo Clipboard::info = { "Clipboard", 0, &ClipboardTable, 0 };

/* Source for ClipboardTable. Use "make hashtables" to regenerate.
@begin ClipboardTable 3
  dropEffect    Clipboard::DropEffect   DontDelete
  effectAllowed Clipboard::EffectAllowed        DontDelete
  types         Clipboard::Types        DontDelete|ReadOnly
@end
@begin ClipboardProtoTable 4
  clearData     Clipboard::ClearData    DontDelete|Function 0
  getData       Clipboard::GetData      DontDelete|Function 1
  setData       Clipboard::SetData      DontDelete|Function 2
  setDragImage  Clipboard::SetDragImage DontDelete|Function 3
@end
*/

KJS_DEFINE_PROTOTYPE(ClipboardProto)
KJS_IMPLEMENT_PROTOFUNC(ClipboardProtoFunc)
KJS_IMPLEMENT_PROTOTYPE("Clipboard", ClipboardProto, ClipboardProtoFunc)

Clipboard::Clipboard(ExecState *exec, WebCore::Clipboard *cb)
  : clipboard(cb)
{
    setPrototype(ClipboardProto::self(exec));
}

bool Clipboard::getOwnPropertySlot(ExecState *exec, const Identifier& propertyName, PropertySlot& slot)
{
    return getStaticValueSlot<Clipboard, DOMObject>(exec, &ClipboardTable, this, propertyName, slot);
}

JSValue *Clipboard::getValueProperty(ExecState *exec, int token) const
{
    switch (token) {
        case DropEffect:
            assert(clipboard->isForDragging() || clipboard->dropEffect().isNull());
            return jsStringOrUndefined(clipboard->dropEffect());
        case EffectAllowed:
            assert(clipboard->isForDragging() || clipboard->effectAllowed().isNull());
            return jsStringOrUndefined(clipboard->effectAllowed());
        case Types:
        {
            DeprecatedStringList qTypes = clipboard->types();
            if (qTypes.isEmpty())
                return jsNull(); 
            else {
                List list;
                for (DeprecatedStringList::Iterator it = qTypes.begin(); it != qTypes.end(); ++it) {
                    list.append(jsString(UString(*it)));
                }
                return exec->lexicalInterpreter()->builtinArray()->construct(exec, list);
            }
        }
        default:
            return NULL;
    }
}

void Clipboard::put(ExecState *exec, const Identifier &propertyName, JSValue *value, int attr)
{
    lookupPut<Clipboard,DOMObject>(exec, propertyName, value, attr, &ClipboardTable, this );
}

void Clipboard::putValueProperty(ExecState *exec, int token, JSValue *value, int /*attr*/)
{
    switch (token) {
        case DropEffect:
            // can never set this when not for dragging, thus getting always returns NULL string
            if (clipboard->isForDragging())
                clipboard->setDropEffect(value->toString(exec));
            break;
        case EffectAllowed:
            // can never set this when not for dragging, thus getting always returns NULL string
            if (clipboard->isForDragging())
                clipboard->setEffectAllowed(value->toString(exec));
            break;
    }
}

JSValue *ClipboardProtoFunc::callAsFunction(ExecState *exec, JSObject *thisObj, const List &args)
{
    if (!thisObj->inherits(&Clipboard::info))
        return throwError(exec, TypeError);

    Clipboard *cb = static_cast<Clipboard *>(thisObj);
    switch (id) {
        case Clipboard::ClearData:
            if (args.size() == 0) {
                cb->clipboard->clearAllData();
                return jsUndefined();
            } else if (args.size() == 1) {
                cb->clipboard->clearData(args[0]->toString(exec));
                return jsUndefined();
            } else
                return throwError(exec, SyntaxError, "clearData: Invalid number of arguments");
        case Clipboard::GetData:
        {
            if (args.size() == 1) {
                bool success;
                String result = cb->clipboard->getData(args[0]->toString(exec), success);
                if (success)
                    return jsString(result);
                else
                    return jsUndefined();
            } else
                return throwError(exec, SyntaxError, "getData: Invalid number of arguments");
        }
        case Clipboard::SetData:
            if (args.size() == 2)
                return jsBoolean(cb->clipboard->setData(args[0]->toString(exec), args[1]->toString(exec)));
            else
                return throwError(exec, SyntaxError, "setData: Invalid number of arguments");
        case Clipboard::SetDragImage:
        {
            if (!cb->clipboard->isForDragging())
                return jsUndefined();

            if (args.size() != 3)
                return throwError(exec, SyntaxError, "setDragImage: Invalid number of arguments");

            int x = (int)args[1]->toNumber(exec);
            int y = (int)args[2]->toNumber(exec);

            // See if they passed us a node
            Node *node = toNode(args[0]);
            if (!node)
                return throwError(exec, TypeError);
            
            if (!node->isElementNode())
                return throwError(exec, SyntaxError, "setDragImageFromElement: Invalid first argument");

            if (static_cast<Element*>(node)->hasLocalName(imgTag) && 
                !node->inDocument())
                cb->clipboard->setDragImage(static_cast<HTMLImageElement*>(node)->cachedImage(), IntPoint(x, y));
            else
                cb->clipboard->setDragImageElement(node, IntPoint(x, y));                    
                
            return jsUndefined();
        }
    }
    return jsUndefined();
}

}