<commit_before>// ==UserScript==
// @name        SE chat timelines indicators
// @namespace   Kyll-things
// @author      Kyll
// @description Adds coloured lines between messages to indicate age
// @include     *://chat.stackoverflow.com/rooms/*
// @exclude     *://chat.stackoverflow.com/rooms/info/*
// @include     *://chat.meta.stackexchange.com/rooms/*
// @exclude     *://chat.meta.stackexchange.com/rooms/info/*
// @include     *://chat.stackexchange.com/rooms/*
// @exclude      *://chat.stackexchange.com/rooms/info/*
// @version     1.2
// @eat         waffle
// ==/UserScript==

// Add some Plop
window.Plop = window.Plop || {}
Plopify = (...args) => Object.assign(window.Plop, ...args)

const storeKey = 'plop-lines'

// Add some magic
HTMLCollection.prototype[Symbol.iterator] =
  HTMLCollection.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]
NodeList.prototype[Symbol.iterator] =
  NodeList.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]

// Add some style
const linesStyleNode = document.createElement('style')
document.head.appendChild(linesStyleNode)

// Add timestamps on all existing messages
// This requires the DOM to be loaded.
// Try until it found some messages on which to add timestamps
// If anyone has some kind of cleaner way to do this (event?), I'm taker
const allTimestampsInterval = setInterval(function tryAddAllTimestamps() {
  const allMessages = document.querySelectorAll('*[id^="message-"]')

  for(let message of allMessages) {
    let timestamp = $.data(message, 'info') && $.data(message, 'info').time
    message.setAttribute('timestamp', timestamp)
  }

  if(allMessages.length > 0) {
    clearInterval(allTimestampsInterval)
  }
}, 2000)

CHAT.addEventHandlerHook(({event_type, time_stamp, message_id}) => {
  const caughtEvents = [
    1, // new message
    2, // edit
    10 // deleted message
  ]
  if(caughtEvents.includes(event_type)) {
    // We need the HTML element to exist, it's set synchronously
    // Thus, queue the query
    setTimeout(() => {
      const element = document.getElementById(`message-${message_id}`)
      if (element) {
        element.setAttribute('timestamp', time_stamp)
      }
    }, 0)
  }
})

// Sophisticated.
const makeLine = (colour, durationSeconds, className) =>
  ({ colour, durationSeconds, className })

// Sequence ordered by duration in ascending order
// Example:
// [
//   { colour: 'red', durationSeconds: 120, className: 'two-minutes'}
// ]
const lines = []

const defaultLines = [
  {
    colour: 'red',
    durationSeconds: 120,
    className: 'two-minutes'
  },
  {
    colour: 'green',
    durationSeconds: 300,
    className: 'five-minutes'
  },
  {
    colour: 'blue',
    durationSeconds: 600,
    className: 'ten-minutes'
  }
]

Object.defineProperties(lines, {
  addLine : {
    value: (colour, durationSeconds, className, doUpdate = true) => {
      const newLine = makeLine(colour, durationSeconds, className)
          , indexToInsert = lines.findIndex(e => e.durationSeconds > durationSeconds)

      if(indexToInsert === -1) {
        lines.push(newLine)
      }
      else {
        lines.splice(
          indexToInsert,
          0,
          newLine
        )
      }
      if(doUpdate) {
        lines.updateCSS()
      }
    }
  },
  addMultipleLines: {
    value: (...linesSequence) => {
      linesSequence.forEach(line => lines.addLine(
        line.colour, line.durationSeconds, line.className, false
      ))
      lines.updateCSS()
    }
  },
  removeLine: {
    value: (index, doUpdate = true) => {
      lines.splice(index, 1)
      if(doUpdate) {
        lines.updateCSS()
      }
    }
  },
  removeMultipleLines: {
    value: (...indexes) => {
      indexes.forEach(index => lines.removeLine(index), false)
      lines.updateCSS()
    }
  },
  maxLookupAmount: {
    value: 42
  },
  CSSNode: {
    value: linesStyleNode
  },
  generateCSS: {
    value: () => lines.reduce((summedCSS, line) =>
      `${summedCSS}
      .message.${line.className} {
        border-bottom-style: solid!important;
        border-bottom-color: ${line.colour}!important;
          border-bottom-width: thin!important;
      }`, '')
  },
  updateCSS: {
    value: () => lines.CSSNode.innerHTML = lines.generateCSS()
  },
  save: {
    value: () => localStorage.setItem('plop-lines', JSON.stringify([...lines]))
  },
  reset: {
    value: () => {
      localStorage.removeItem(storeKey)
      lines.removeMultipleLines(
        ...lines.keys()
      )
      lines.addMultipleLines(...defaultLines)
    }
  }
})

// TODO: That's overstepping the responsibility of this script.
// Need to hoist the "help" thingies in its own script and require it
Plop.help = () => console.log('Maybe you meant `Plop.halp()`?')
Plop.halp = () => console.log(
`Plop!
You can view the lines properties by typing Plop.lines in this console.
You can change any property of said lines with your debug tools or a command.
The colours must be CSS colours, such as "purple" or "0xFAFAFA"
Example:
    Plop.lines[0].colour = 'purple'
    Plop.lines[3].durationSeconds = 1200 // 20 minutes

To update deh CSS and make the changes appear:
    Plop.lines.updateCSS()

To add a new line, use the Plop.lines.addLine function.
First argument is the colour of the line.
Second argument is the duration in seconds it represents.
Third argument is the CSS class name it should use.
Example:
    Plop.lines.addLine('yellow', 180 /* three minutes */, 'three-minutes')

To remove a line, use the Plop.lines.removeLine function.
Only argument is the index (can be negative as reverse index).
Example:
    Plop.lines.removeLine(0)  // Remove first line
    Plop.lines.removeLine(-1) // Remove last line

Once you've done your changes, save them so that they persist across reloads:
    Plop.lines.save()

If you screwed up:
    Plop.reset()

Have fun!
 - Kyll
`
)

const storedLines = JSON.parse(localStorage.getItem(storeKey))

if(storedLines) {
  lines.addMultipleLines(...storedLines)
}
else {
  // Default lines
  lines.addMultipleLines({
    colour: 'red',
    durationSeconds: 120,
    className: 'two-minutes'
  },
  {
    colour: 'green',
    durationSeconds: 300,
    className: 'five-minutes'
  },
  {
    colour: 'blue',
    durationSeconds: 600,
    className: 'ten-minutes'
  })
}

Plopify({ lines })

setInterval(function manageTimeLines() {
  const removeAllLines = () => {
    // TODO: Implement Timeouts instead of raw removals
    const allClasses = lines.map(line => line.className)
    // Ninja it to array because element collections suck
    allClasses
      .map(className => document.getElementsByClassName(className))
      .forEach(elementsCollection =>
        [...elementsCollection].forEach(element =>
          element.classList.remove(...allClasses)
        )
      )
  }

  const addLines = () => {
    const allMessages = document.querySelectorAll('.message')
        , maxLookupAmount = Math.min(lines.maxLookupAmount, allMessages.length)
        , now = Date.now()

        , thresholds = [...lines]

    for(let i = allMessages.length - 1; i > allMessages.length - maxLookupAmount; i--) {
      const currentLine = thresholds[0]
          , currentMessage = allMessages[i]
          , messageTimestamp = Number.parseInt(
            currentMessage.getAttribute('timestamp')
          ) * 1000

      if(!currentLine) {
        // No more lines to add, quit
        break
      }

      if(
        messageTimestamp < (now - (currentLine.durationSeconds * 1000))
      ) {
        currentMessage.classList.add(currentLine.className)

        // We're done with this line, remove it
        thresholds.shift()
        // A message can have multiple lines attached, so redo this round
        i++
      }
    }
  }

  removeAllLines()

  addLines()
}, 420)
<commit_msg>Added padding to underlined messages<commit_after>// ==UserScript==
// @name        SE chat timelines indicators
// @namespace   Kyll-things
// @author      Kyll
// @description Adds coloured lines between messages to indicate age
// @include     *://chat.stackoverflow.com/rooms/*
// @exclude     *://chat.stackoverflow.com/rooms/info/*
// @include     *://chat.meta.stackexchange.com/rooms/*
// @exclude     *://chat.meta.stackexchange.com/rooms/info/*
// @include     *://chat.stackexchange.com/rooms/*
// @exclude      *://chat.stackexchange.com/rooms/info/*
// @version     1.2
// @eat         waffle
// ==/UserScript==

// Add some Plop
window.Plop = window.Plop || {}
Plopify = (...args) => Object.assign(window.Plop, ...args)

const storeKey = 'plop-lines'

// Add some magic
HTMLCollection.prototype[Symbol.iterator] =
  HTMLCollection.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]
NodeList.prototype[Symbol.iterator] =
  NodeList.prototype[Symbol.iterator] || Array.prototype[Symbol.iterator]

// Add some style
const linesStyleNode = document.createElement('style')
document.head.appendChild(linesStyleNode)

// Add timestamps on all existing messages
// This requires the DOM to be loaded.
// Try until it found some messages on which to add timestamps
// If anyone has some kind of cleaner way to do this (event?), I'm taker
const allTimestampsInterval = setInterval(function tryAddAllTimestamps() {
  const allMessages = document.querySelectorAll('*[id^="message-"]')

  for(let message of allMessages) {
    let timestamp = $.data(message, 'info') && $.data(message, 'info').time
    message.setAttribute('timestamp', timestamp)
  }

  if(allMessages.length > 0) {
    clearInterval(allTimestampsInterval)
  }
}, 2000)

CHAT.addEventHandlerHook(({event_type, time_stamp, message_id}) => {
  const caughtEvents = [
    1, // new message
    2, // edit
    10 // deleted message
  ]
  if(caughtEvents.includes(event_type)) {
    // We need the HTML element to exist, it's set synchronously
    // Thus, queue the query
    setTimeout(() => {
      const element = document.getElementById(`message-${message_id}`)
      if (element) {
        element.setAttribute('timestamp', time_stamp)
      }
    }, 0)
  }
})

// Sophisticated.
const makeLine = (colour, durationSeconds, className) =>
  ({ colour, durationSeconds, className })

// Sequence ordered by duration in ascending order
// Example:
// [
//   { colour: 'red', durationSeconds: 120, className: 'two-minutes'}
// ]
const lines = []

const defaultLines = [
  {
    colour: 'red',
    durationSeconds: 120,
    className: 'two-minutes'
  },
  {
    colour: 'green',
    durationSeconds: 300,
    className: 'five-minutes'
  },
  {
    colour: 'blue',
    durationSeconds: 600,
    className: 'ten-minutes'
  }
]

Object.defineProperties(lines, {
  addLine : {
    value: (colour, durationSeconds, className, doUpdate = true) => {
      const newLine = makeLine(colour, durationSeconds, className)
          , indexToInsert = lines.findIndex(e => e.durationSeconds > durationSeconds)

      if(indexToInsert === -1) {
        lines.push(newLine)
      }
      else {
        lines.splice(
          indexToInsert,
          0,
          newLine
        )
      }
      if(doUpdate) {
        lines.updateCSS()
      }
    }
  },
  addMultipleLines: {
    value: (...linesSequence) => {
      linesSequence.forEach(line => lines.addLine(
        line.colour, line.durationSeconds, line.className, false
      ))
      lines.updateCSS()
    }
  },
  removeLine: {
    value: (index, doUpdate = true) => {
      lines.splice(index, 1)
      if(doUpdate) {
        lines.updateCSS()
      }
    }
  },
  removeMultipleLines: {
    value: (...indexes) => {
      indexes.forEach(index => lines.removeLine(index), false)
      lines.updateCSS()
    }
  },
  maxLookupAmount: {
    value: 42
  },
  CSSNode: {
    value: linesStyleNode
  },
  generateCSS: {
    value: () => lines.reduce((summedCSS, line) =>
      `${summedCSS}
      .message.${line.className} {
        border-bottom-style: solid!important;
        border-bottom-color: ${line.colour}!important;
        border-bottom-width: thin!important;
        padding-bottom: 1px!important;
      }`, '')
  },
  updateCSS: {
    value: () => lines.CSSNode.innerHTML = lines.generateCSS()
  },
  save: {
    value: () => localStorage.setItem('plop-lines', JSON.stringify([...lines]))
  },
  reset: {
    value: () => {
      localStorage.removeItem(storeKey)
      lines.removeMultipleLines(
        ...lines.keys()
      )
      lines.addMultipleLines(...defaultLines)
    }
  }
})

// TODO: That's overstepping the responsibility of this script.
// Need to hoist the "help" thingies in its own script and require it
Plop.help = () => console.log('Maybe you meant `Plop.halp()`?')
Plop.halp = () => console.log(
`Plop!
You can view the lines properties by typing Plop.lines in this console.
You can change any property of said lines with your debug tools or a command.
The colours must be CSS colours, such as "purple" or "0xFAFAFA"
Example:
    Plop.lines[0].colour = 'purple'
    Plop.lines[3].durationSeconds = 1200 // 20 minutes

To update deh CSS and make the changes appear:
    Plop.lines.updateCSS()

To add a new line, use the Plop.lines.addLine function.
First argument is the colour of the line.
Second argument is the duration in seconds it represents.
Third argument is the CSS class name it should use.
Example:
    Plop.lines.addLine('yellow', 180 /* three minutes */, 'three-minutes')

To remove a line, use the Plop.lines.removeLine function.
Only argument is the index (can be negative as reverse index).
Example:
    Plop.lines.removeLine(0)  // Remove first line
    Plop.lines.removeLine(-1) // Remove last line

Once you've done your changes, save them so that they persist across reloads:
    Plop.lines.save()

If you screwed up:
    Plop.reset()

Have fun!
 - Kyll
`
)

const storedLines = JSON.parse(localStorage.getItem(storeKey))

if(storedLines) {
  lines.addMultipleLines(...storedLines)
}
else {
  // Default lines
  lines.addMultipleLines({
    colour: 'red',
    durationSeconds: 120,
    className: 'two-minutes'
  },
  {
    colour: 'green',
    durationSeconds: 300,
    className: 'five-minutes'
  },
  {
    colour: 'blue',
    durationSeconds: 600,
    className: 'ten-minutes'
  })
}

Plopify({ lines })

setInterval(function manageTimeLines() {
  const removeAllLines = () => {
    // TODO: Implement Timeouts instead of raw removals
    const allClasses = lines.map(line => line.className)
    // Ninja it to array because element collections suck
    allClasses
      .map(className => document.getElementsByClassName(className))
      .forEach(elementsCollection =>
        [...elementsCollection].forEach(element =>
          element.classList.remove(...allClasses)
        )
      )
  }

  const addLines = () => {
    const allMessages = document.querySelectorAll('.message')
        , maxLookupAmount = Math.min(lines.maxLookupAmount, allMessages.length)
        , now = Date.now()

        , thresholds = [...lines]

    for(let i = allMessages.length - 1; i > allMessages.length - maxLookupAmount; i--) {
      const currentLine = thresholds[0]
          , currentMessage = allMessages[i]
          , messageTimestamp = Number.parseInt(
            currentMessage.getAttribute('timestamp')
          ) * 1000

      if(!currentLine) {
        // No more lines to add, quit
        break
      }

      if(
        messageTimestamp < (now - (currentLine.durationSeconds * 1000))
      ) {
        currentMessage.classList.add(currentLine.className)

        // We're done with this line, remove it
        thresholds.shift()
        // A message can have multiple lines attached, so redo this round
        i++
      }
    }
  }

  removeAllLines()

  addLines()
}, 420)
<|endoftext|>


<commit_before>/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

/*
  Copyright (C) 2005 SKYRIX Software AG

  This file is part of OpenGroupware.org.

  OGo 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, or (at your option) any
  later version.

  OGo 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 OGo; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

var contactSelectorAction = 'calendars-contacts';
var AppointmentEditor = {
    attendeesMenu: null
};

function uixEarlierDate(date1, date2) {
    // can this be done in a sane way?
    if (date1.getYear()  < date2.getYear()) return date1;
    if (date1.getYear()  > date2.getYear()) return date2;
    // same year
    if (date1.getMonth() < date2.getMonth()) return date1;
    if (date1.getMonth() > date2.getMonth()) return date2;
    //   // same month
    if (date1.getDate() < date2.getDate()) return date1;
    if (date1.getDate() > date2.getDate()) return date2;
    // same day
    return null;
}

function validateAptEditor() {
    var e, startdate, enddate, tmpdate;

    e = $('summary');
    if (e.value.length == 0) {
        if (!confirm(labels.validate_notitle))
            return false;
    }

    e = $('startTime_date');
    if (e.value.length != 10) {
        alert(labels.validate_invalid_startdate);
        return false;
    }
    startdate = e.calendar.prs_date(e.value);
    if (startdate == null) {
        alert(labels.validate_invalid_startdate);
        return false;
    }

    e = $('endTime_date');
    if (e.value.length != 10) {
        alert(labels.validate_invalid_enddate);
        return false;
    }
    enddate = e.calendar.prs_date(e.value);
    if (enddate == null) {
        alert(labels.validate_invalid_enddate);
        return false;
    }
    tmpdate = uixEarlierDate(startdate, enddate);
    if (tmpdate == enddate) {
        alert(labels.validate_endbeforestart);
        return false;
    }
    else if (tmpdate == null /* means: same date */) {
        // TODO: check time
        var start, end;

        start = parseInt(document.forms[0]['startTime_time_hour'].value);
        end = parseInt(document.forms[0]['endTime_time_hour'].value);

        if (start > end) {
            alert(labels.validate_endbeforestart);
            return false;
        }
        else if (start == end) {
            start = parseInt(document.forms[0]['startTime_time_minute'].value);
            end = parseInt(document.forms[0]['endTime_time_minute'].value);
            if (start > end) {
                alert(labels.validate_endbeforestart);
                return false;
            }
        }
    }

    return true;
}

function onAttendeesMenuPrepareVisibility()
{
    var composeToUndecidedAttendees = $('composeToUndecidedAttendees');
    var attendeesStates = $('attendeesStates').value;

    if (attendeesStates.indexOf("needs-action") < 0)
        composeToUndecidedAttendees.addClassName("disabled");
    else
        composeToUndecidedAttendees.removeClassName("disabled");
}

function onComposeToAllAttendees()
{
    var attendees = $$("DIV#attendeesMenu LI.attendee");
    var addresses = new Array();
    attendees.each(function(item) {
            var address = item.firstChild.nodeValue.trim() + " <" + item.readAttribute("email") + ">";
            addresses.push(address);
        });
    if (window.opener)
        window.opener.openMailTo(addresses.join(","));
}

function onComposeToUndecidedAttendees()
{
    if ($(this).hasClassName("disabled"))
        return;

    var attendees = $$("DIV#attendeesMenu LI.attendee.needs-action");
    var addresses = new Array();
    attendees.each(function(item) {
            var address = item.firstChild.nodeValue.trim() + " <" + item.readAttribute("email") + ">";
            addresses.push(address);
        });
    if (window.opener)
        window.opener.openMailTo(addresses.join(","));
}

function addContact(tag, fullContactName, contactId, contactName, contactEmail) {
    var uids = $('uixselector-participants-uidList');

    if (contactId)
        {
            var re = new RegExp("(^|,)" + contactId + "($|,)");

            if (!re.test(uids.value))
                {
                    if (uids.value.length > 0)
                        uids.value += ',' + contactId;
                    else
                        uids.value = contactId;

                    var names = $('uixselector-participants-display');
                    names.innerHTML += ('
'
                                        + contactName + '
');
                }
        }

    return false;
}

function saveEvent(sender) {
    if (validateAptEditor()) {
        document.forms['editform'].attendees.value = attendees.toJSON();
        document.forms['editform'].submit();
    }

    return false;
}

function startDayAsShortString() {
    return $('startTime_date').valueAsShortDateString();
}

function endDayAsShortString() {
    return $('endTime_date').valueAsShortDateString();
}

function _getDate(which) {
    var date = window.timeWidgets[which]['date'].valueAsDate();
    date.setHours( window.timeWidgets[which]['hour'].value );
    date.setMinutes( window.timeWidgets[which]['minute'].value );

    return date;
}

function _getShadowDate(which) {
    var date = window.timeWidgets[which]['date'].getAttribute("shadow-value").asDate();
    var intValue = parseInt(window.timeWidgets[which]['hour'].getAttribute("shadow-value"));
    date.setHours(intValue);
    intValue = parseInt(window.timeWidgets[which]['minute'].getAttribute("shadow-value"));
    date.setMinutes(intValue);

    return date;
}

function getStartDate() {
    return this._getDate('start');
}

function getEndDate() {
    return this._getDate('end');
}

function getShadowStartDate() {
    return this._getShadowDate('start');
}

function getShadowEndDate() {
    return this._getShadowDate('end');
}

function _setDate(which, newDate) {
    window.timeWidgets[which]['date'].setValueAsDate(newDate);
    window.timeWidgets[which]['hour'].value = newDate.getHours();
    var minutes = newDate.getMinutes();
    if (minutes % 15)
        minutes += (15 - minutes % 15);
    window.timeWidgets[which]['minute'].value = minutes;
}

function setStartDate(newStartDate) {
    this._setDate('start', newStartDate);
}

function setEndDate(newEndDate) {
    this._setDate('end', newEndDate);
}

function onAdjustTime(event) {
    var endDate = window.getEndDate();
    var startDate = window.getStartDate();

    if ($(this).readAttribute("id").startsWith("start")) {
        // Start date was changed
        var delta = window.getShadowStartDate().valueOf() -
            startDate.valueOf();
        var newEndDate = new Date(endDate.valueOf() - delta);
        window.setEndDate(newEndDate);

        window.timeWidgets['end']['date'].updateShadowValue();
        window.timeWidgets['end']['hour'].updateShadowValue();
        window.timeWidgets['end']['minute'].updateShadowValue();
        window.timeWidgets['start']['date'].updateShadowValue();
        window.timeWidgets['start']['hour'].updateShadowValue();
        window.timeWidgets['start']['minute'].updateShadowValue();
    }
    else {
        // End date was changed
        var delta = endDate.valueOf() - startDate.valueOf();
        if (delta < 0) {
            alert(labels.validate_endbeforestart);
            var oldEndDate = window.getShadowEndDate();
            window.setEndDate(oldEndDate);

            window.timeWidgets['end']['date'].updateShadowValue();
            window.timeWidgets['end']['hour'].updateShadowValue();
            window.timeWidgets['end']['minute'].updateShadowValue();
        }
    }
}

function onAllDayChanged(event) {
    for (var type in window.timeWidgets) {
        window.timeWidgets[type]['hour'].disabled = this.checked;
        window.timeWidgets[type]['minute'].disabled = this.checked;
    }
}

function initTimeWidgets(widgets) {
    this.timeWidgets = widgets;

    if (widgets['start']['date']) {
        widgets['start']['date'].observe("change", this.onAdjustTime, false);
        widgets['start']['hour'].observe("change", this.onAdjustTime, false);
        widgets['start']['minute'].observe("change", this.onAdjustTime, false);
    }

    if (widgets['end']['date']) {
        widgets['end']['date'].observe("change", this.onAdjustTime, false);
        widgets['end']['hour'].observe("change", this.onAdjustTime, false);
        widgets['end']['minute'].observe("change", this.onAdjustTime, false);
    }

    var allDayLabel = $("allDay");
    if (allDayLabel) {
        var input = $(allDayLabel).childNodesWithTag("input")[0];
        input.observe("change", onAllDayChanged.bindAsEventListener(input));
        if (input.checked) {
            for (var type in widgets) {
                widgets[type]['hour'].disabled = true;
                widgets[type]['minute'].disabled = true;
            }
        }
    }
}

function refreshAttendeesRO () {
    var attendeesMenu = $("attendeesMenu");
    var attendeesLabel = $("attendeesLabel");
    var attendeesDiv = $("attendeesDiv");

    attendeesLabel.setStyle({ display: "block" });
    attendeesDiv.setStyle({display: "block"});

    // Register "click" event on each attendee's email
    var attendees = attendeesMenu.getElementsByTagName('a');
    $A(attendees).each(function(attendee) {
            $(attendee).observe("click", onMailTo);
        });
}

function refreshAttendees(newAttendees) {
    var attendeesLabel = $("attendeesLabel");
    var attendeesHref = $("attendeesHref");
    var attendeesMenu = $("attendeesMenu");

    if (!attendeesHref)
        return refreshAttendeesRO ();

    if (attendeesMenu)
        attendeesMenu = $("attendeesMenu").down("ul");

    // Remove link of attendees
    for (var i = 0; i < attendeesHref.childNodes.length; i++)
        attendeesHref.removeChild(attendeesHref.childNodes[i]);

    // Remove attendees from menu
    var menuItems = $$("DIV#attendeesMenu LI.attendee");
    if (menuItems && attendeesMenu)
        for (var i = 0; i < menuItems.length; i++)
            attendeesMenu.removeChild(menuItems[i]);

    if (newAttendees)
        attendees = $H(newAttendees.evalJSON(true));

    if (attendees.keys().length > 0) {
        // Update attendees link and show label
        var names = new Array();
        attendees.values().each(function(attendee) {
                attendee = $H(attendee);
                var name = attendee.get('name') || attendee.get('email');
                if (!attendee.get('delegated-to'))
                    names.push(name);

                if (attendeesMenu) {
                    var delegatedTo = attendee.get('delegated-to');
                    if (!attendee.get('delegated-from') || delegatedTo) {
                        var node = document.createElement("li");
                        attendeesMenu.appendChild(node);
                        setupAttendeeNode(node, attendee);
                    }
                    if (delegatedTo) {
                        var delegate = attendees.get(delegatedTo);
                        var node = document.createElement("li");
                        attendeesMenu.appendChild(node);
                        setupAttendeeNode(node, $H(delegate), true);
                    }
                }
            });
        attendeesHref.appendChild(document.createTextNode(names.join(", ")));
        attendeesLabel.setStyle({ display: "block" });
    }
    else {
        // Hide link of attendees
        attendeesLabel.setStyle({ display: "none" });
    }
}

function setupAttendeeNode(aNode, aAttendee, isDelegate) {
    // Construct the display string from common name and/or email address.
    var name = aAttendee.get('name');
    var email = aAttendee.get('email');
//    if (name)
//        name += ' <' + email + '>';
//    else
//        name = email;
    name = name || email;

    $(aNode).writeAttribute("email", email);
    $(aNode).addClassName("attendee");
    $(aNode).addClassName(aAttendee.get('partstat'));
    if (isDelegate)
        $(aNode).addClassName("delegate");
    aNode.appendChild(document.createTextNode(name));
    $(aNode).observe("click", onMailTo);
}

function initializeAttendeesHref() {
    var attendeesHref = $("attendeesHref");
    var attendeesLabel = $("attendeesLabel");
    var attendeesNames = $("attendeesNames");

    if (attendeesHref && !attendeesHref.hasClassName ("nomenu"))
        attendeesHref.observe("click", onAttendeesHrefClick, false);
    refreshAttendees();
}

function onAttendeesHrefClick(event) {
    popupToolbarMenu(this, 'attendeesMenu');
    preventDefault(event);
    return false;
}

function onMailTo(event) {
    var target = getTarget(event);
    var address = target.firstChild.nodeValue.trim() + " <" + target.readAttribute("email") + ">";
    openMailTo(address);
    Event.stop(event);
    return false;
}

function getMenus() {
    AppointmentEditor.attendeesMenu = new Array(onPopupAttendeesWindow,
                                                "-",
                                                onComposeToAllAttendees,
                                                onComposeToUndecidedAttendees,
                                                "-",
                                                null);

    var attendeesMenu = $('attendeesMenu');
    if (attendeesMenu)
        attendeesMenu.prepareVisibility = onAttendeesMenuPrepareVisibility;

    return { "attendeesMenu": AppointmentEditor.attendeesMenu };
}

function onAppointmentEditorLoad() {
    if (readOnly == false) {
        assignCalendar('startTime_date');
        assignCalendar('endTime_date');

        var widgets = {'start': {'date': $("startTime_date"),
                                 'hour': $("startTime_time_hour"),
                                 'minute': $("startTime_time_minute")},
                       'end': {'date': $("endTime_date"),
                               'hour': $("endTime_time_hour"),
                               'minute': $("endTime_time_minute")}};
        initTimeWidgets(widgets);
    }

    // Extend JSON representation of attendees
    attendees = $H(attendees);

    initializeAttendeesHref();
}

document.observe("dom:loaded", onAppointmentEditorLoad);
<commit_msg>Fixed js error (menu on attendees click in appt editor was missing)<commit_after>/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

/*
  Copyright (C) 2005 SKYRIX Software AG

  This file is part of OpenGroupware.org.

  OGo 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, or (at your option) any
  later version.

  OGo 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 OGo; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

var contactSelectorAction = 'calendars-contacts';
var AppointmentEditor = {
    attendeesMenu: null
};

function uixEarlierDate(date1, date2) {
    // can this be done in a sane way?
    if (date1.getYear()  < date2.getYear()) return date1;
    if (date1.getYear()  > date2.getYear()) return date2;
    // same year
    if (date1.getMonth() < date2.getMonth()) return date1;
    if (date1.getMonth() > date2.getMonth()) return date2;
    //   // same month
    if (date1.getDate() < date2.getDate()) return date1;
    if (date1.getDate() > date2.getDate()) return date2;
    // same day
    return null;
}

function validateAptEditor() {
    var e, startdate, enddate, tmpdate;

    e = $('summary');
    if (e.value.length == 0) {
        if (!confirm(labels.validate_notitle))
            return false;
    }

    e = $('startTime_date');
    if (e.value.length != 10) {
        alert(labels.validate_invalid_startdate);
        return false;
    }
    startdate = e.calendar.prs_date(e.value);
    if (startdate == null) {
        alert(labels.validate_invalid_startdate);
        return false;
    }

    e = $('endTime_date');
    if (e.value.length != 10) {
        alert(labels.validate_invalid_enddate);
        return false;
    }
    enddate = e.calendar.prs_date(e.value);
    if (enddate == null) {
        alert(labels.validate_invalid_enddate);
        return false;
    }
    tmpdate = uixEarlierDate(startdate, enddate);
    if (tmpdate == enddate) {
        alert(labels.validate_endbeforestart);
        return false;
    }
    else if (tmpdate == null /* means: same date */) {
        // TODO: check time
        var start, end;

        start = parseInt(document.forms[0]['startTime_time_hour'].value);
        end = parseInt(document.forms[0]['endTime_time_hour'].value);

        if (start > end) {
            alert(labels.validate_endbeforestart);
            return false;
        }
        else if (start == end) {
            start = parseInt(document.forms[0]['startTime_time_minute'].value);
            end = parseInt(document.forms[0]['endTime_time_minute'].value);
            if (start > end) {
                alert(labels.validate_endbeforestart);
                return false;
            }
        }
    }

    return true;
}

function onAttendeesMenuPrepareVisibility()
{
    var composeToUndecidedAttendees = $('composeToUndecidedAttendees');
    var attendeesStates = $('attendeesMenu').innerHTML;

    if (attendeesStates.indexOf("needs-action") < 0)
        composeToUndecidedAttendees.addClassName("disabled");
    else
        composeToUndecidedAttendees.removeClassName("disabled");
}

function onComposeToAllAttendees()
{
    var attendees = $$("DIV#attendeesMenu LI.attendee");
    var addresses = new Array();
    attendees.each(function(item) {
            var address = item.firstChild.nodeValue.trim() + " <" + item.readAttribute("email") + ">";
            addresses.push(address);
        });
    if (window.opener)
        window.opener.openMailTo(addresses.join(","));
}

function onComposeToUndecidedAttendees()
{
    if ($(this).hasClassName("disabled"))
        return;

    var attendees = $$("DIV#attendeesMenu LI.attendee.needs-action");
    var addresses = new Array();
    attendees.each(function(item) {
            var address = item.firstChild.nodeValue.trim() + " <" + item.readAttribute("email") + ">";
            addresses.push(address);
        });
    if (window.opener)
        window.opener.openMailTo(addresses.join(","));
}

function addContact(tag, fullContactName, contactId, contactName, contactEmail) {
    var uids = $('uixselector-participants-uidList');

    if (contactId)
        {
            var re = new RegExp("(^|,)" + contactId + "($|,)");

            if (!re.test(uids.value))
                {
                    if (uids.value.length > 0)
                        uids.value += ',' + contactId;
                    else
                        uids.value = contactId;

                    var names = $('uixselector-participants-display');
                    names.innerHTML += ('
'
                                        + contactName + '
');
                }
        }

    return false;
}

function saveEvent(sender) {
    if (validateAptEditor()) {
        document.forms['editform'].attendees.value = attendees.toJSON();
        document.forms['editform'].submit();
    }

    return false;
}

function startDayAsShortString() {
    return $('startTime_date').valueAsShortDateString();
}

function endDayAsShortString() {
    return $('endTime_date').valueAsShortDateString();
}

function _getDate(which) {
    var date = window.timeWidgets[which]['date'].valueAsDate();
    date.setHours( window.timeWidgets[which]['hour'].value );
    date.setMinutes( window.timeWidgets[which]['minute'].value );

    return date;
}

function _getShadowDate(which) {
    var date = window.timeWidgets[which]['date'].getAttribute("shadow-value").asDate();
    var intValue = parseInt(window.timeWidgets[which]['hour'].getAttribute("shadow-value"));
    date.setHours(intValue);
    intValue = parseInt(window.timeWidgets[which]['minute'].getAttribute("shadow-value"));
    date.setMinutes(intValue);

    return date;
}

function getStartDate() {
    return this._getDate('start');
}

function getEndDate() {
    return this._getDate('end');
}

function getShadowStartDate() {
    return this._getShadowDate('start');
}

function getShadowEndDate() {
    return this._getShadowDate('end');
}

function _setDate(which, newDate) {
    window.timeWidgets[which]['date'].setValueAsDate(newDate);
    window.timeWidgets[which]['hour'].value = newDate.getHours();
    var minutes = newDate.getMinutes();
    if (minutes % 15)
        minutes += (15 - minutes % 15);
    window.timeWidgets[which]['minute'].value = minutes;
}

function setStartDate(newStartDate) {
    this._setDate('start', newStartDate);
}

function setEndDate(newEndDate) {
    this._setDate('end', newEndDate);
}

function onAdjustTime(event) {
    var endDate = window.getEndDate();
    var startDate = window.getStartDate();

    if ($(this).readAttribute("id").startsWith("start")) {
        // Start date was changed
        var delta = window.getShadowStartDate().valueOf() -
            startDate.valueOf();
        var newEndDate = new Date(endDate.valueOf() - delta);
        window.setEndDate(newEndDate);

        window.timeWidgets['end']['date'].updateShadowValue();
        window.timeWidgets['end']['hour'].updateShadowValue();
        window.timeWidgets['end']['minute'].updateShadowValue();
        window.timeWidgets['start']['date'].updateShadowValue();
        window.timeWidgets['start']['hour'].updateShadowValue();
        window.timeWidgets['start']['minute'].updateShadowValue();
    }
    else {
        // End date was changed
        var delta = endDate.valueOf() - startDate.valueOf();
        if (delta < 0) {
            alert(labels.validate_endbeforestart);
            var oldEndDate = window.getShadowEndDate();
            window.setEndDate(oldEndDate);

            window.timeWidgets['end']['date'].updateShadowValue();
            window.timeWidgets['end']['hour'].updateShadowValue();
            window.timeWidgets['end']['minute'].updateShadowValue();
        }
    }
}

function onAllDayChanged(event) {
    for (var type in window.timeWidgets) {
        window.timeWidgets[type]['hour'].disabled = this.checked;
        window.timeWidgets[type]['minute'].disabled = this.checked;
    }
}

function initTimeWidgets(widgets) {
    this.timeWidgets = widgets;

    if (widgets['start']['date']) {
        widgets['start']['date'].observe("change", this.onAdjustTime, false);
        widgets['start']['hour'].observe("change", this.onAdjustTime, false);
        widgets['start']['minute'].observe("change", this.onAdjustTime, false);
    }

    if (widgets['end']['date']) {
        widgets['end']['date'].observe("change", this.onAdjustTime, false);
        widgets['end']['hour'].observe("change", this.onAdjustTime, false);
        widgets['end']['minute'].observe("change", this.onAdjustTime, false);
    }

    var allDayLabel = $("allDay");
    if (allDayLabel) {
        var input = $(allDayLabel).childNodesWithTag("input")[0];
        input.observe("change", onAllDayChanged.bindAsEventListener(input));
        if (input.checked) {
            for (var type in widgets) {
                widgets[type]['hour'].disabled = true;
                widgets[type]['minute'].disabled = true;
            }
        }
    }
}

function refreshAttendeesRO () {
    var attendeesMenu = $("attendeesMenu");
    var attendeesLabel = $("attendeesLabel");
    var attendeesDiv = $("attendeesDiv");

    attendeesLabel.setStyle({ display: "block" });
    attendeesDiv.setStyle({display: "block"});

    // Register "click" event on each attendee's email
    var attendees = attendeesMenu.getElementsByTagName('a');
    $A(attendees).each(function(attendee) {
            $(attendee).observe("click", onMailTo);
        });
}

function refreshAttendees(newAttendees) {
    var attendeesLabel = $("attendeesLabel");
    var attendeesHref = $("attendeesHref");
    var attendeesMenu = $("attendeesMenu");

    if (!attendeesHref)
        return refreshAttendeesRO ();

    if (attendeesMenu)
        attendeesMenu = $("attendeesMenu").down("ul");

    // Remove link of attendees
    for (var i = 0; i < attendeesHref.childNodes.length; i++)
        attendeesHref.removeChild(attendeesHref.childNodes[i]);

    // Remove attendees from menu
    var menuItems = $$("DIV#attendeesMenu LI.attendee");
    if (menuItems && attendeesMenu)
        for (var i = 0; i < menuItems.length; i++)
            attendeesMenu.removeChild(menuItems[i]);

    if (newAttendees)
        attendees = $H(newAttendees.evalJSON(true));

    if (attendees.keys().length > 0) {
        // Update attendees link and show label
        var names = new Array();
        attendees.values().each(function(attendee) {
                attendee = $H(attendee);
                var name = attendee.get('name') || attendee.get('email');
                if (!attendee.get('delegated-to'))
                    names.push(name);

                if (attendeesMenu) {
                    var delegatedTo = attendee.get('delegated-to');
                    if (!attendee.get('delegated-from') || delegatedTo) {
                        var node = document.createElement("li");
                        attendeesMenu.appendChild(node);
                        setupAttendeeNode(node, attendee);
                    }
                    if (delegatedTo) {
                        var delegate = attendees.get(delegatedTo);
                        var node = document.createElement("li");
                        attendeesMenu.appendChild(node);
                        setupAttendeeNode(node, $H(delegate), true);
                    }
                }
            });
        attendeesHref.appendChild(document.createTextNode(names.join(", ")));
        attendeesLabel.setStyle({ display: "block" });
    }
    else {
        // Hide link of attendees
        attendeesLabel.setStyle({ display: "none" });
    }
}

function setupAttendeeNode(aNode, aAttendee, isDelegate) {
    // Construct the display string from common name and/or email address.
    var name = aAttendee.get('name');
    var email = aAttendee.get('email');
//    if (name)
//        name += ' <' + email + '>';
//    else
//        name = email;
    name = name || email;

    $(aNode).writeAttribute("email", email);
    $(aNode).addClassName("attendee");
    $(aNode).addClassName(aAttendee.get('partstat'));
    if (isDelegate)
        $(aNode).addClassName("delegate");
    aNode.appendChild(document.createTextNode(name));
    $(aNode).observe("click", onMailTo);
}

function initializeAttendeesHref() {
    var attendeesHref = $("attendeesHref");
    var attendeesLabel = $("attendeesLabel");
    var attendeesNames = $("attendeesNames");

    if (attendeesHref && !attendeesHref.hasClassName ("nomenu"))
        attendeesHref.observe("click", onAttendeesHrefClick, false);
    refreshAttendees();
}

function onAttendeesHrefClick(event) {
    popupToolbarMenu(this, 'attendeesMenu');
    preventDefault(event);
    return false;
}

function onMailTo(event) {
    var target = getTarget(event);
    var address = target.firstChild.nodeValue.trim() + " <" + target.readAttribute("email") + ">";
    openMailTo(address);
    Event.stop(event);
    return false;
}

function getMenus() {
    AppointmentEditor.attendeesMenu = new Array(onPopupAttendeesWindow,
                                                "-",
                                                onComposeToAllAttendees,
                                                onComposeToUndecidedAttendees,
                                                "-",
                                                null);

    var attendeesMenu = $('attendeesMenu');
    if (attendeesMenu)
        attendeesMenu.prepareVisibility = onAttendeesMenuPrepareVisibility;

    return { "attendeesMenu": AppointmentEditor.attendeesMenu };
}

function onAppointmentEditorLoad() {
    if (readOnly == false) {
        assignCalendar('startTime_date');
        assignCalendar('endTime_date');

        var widgets = {'start': {'date': $("startTime_date"),
                                 'hour': $("startTime_time_hour"),
                                 'minute': $("startTime_time_minute")},
                       'end': {'date': $("endTime_date"),
                               'hour': $("endTime_time_hour"),
                               'minute': $("endTime_time_minute")}};
        initTimeWidgets(widgets);
    }

    // Extend JSON representation of attendees
    attendees = $H(attendees);

    initializeAttendeesHref();
}

document.observe("dom:loaded", onAppointmentEditorLoad);
<|endoftext|>


<commit_before>// Copyright 2012 Software Freedom Conservancy. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Defines utilities for exchanging messages between the
 * sandboxed SafariDriver injected script and its corresponding content page.
 */

goog.provide('safaridriver.inject.page');

goog.require('bot.Error');
goog.require('bot.ErrorCode');
goog.require('bot.dom');
goog.require('goog.array');
goog.require('goog.debug.Logger');
goog.require('goog.dom');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.dom.classes');
goog.require('safaridriver.console');
goog.require('safaridriver.message');
goog.require('webdriver.CommandName');
goog.require('webdriver.error');
goog.require('webdriver.promise');

/**
 * @define {boolean} Whether this script is being used by the extension or has
 *     been injected into the web page.
 */
safaridriver.inject.page.EXTENSION = true;

/**
 * Class name assigned to the SCRIPT element used to insert the compiled form of
 * this script into the web page.
 * @type {string}
 * @const
 * @private
 */
safaridriver.inject.page.SCRIPT_CLASS_NAME_ = 'safari-driver';

/**
 * @type {!goog.debug.Logger}
 * @const
 * @private
 */
safaridriver.inject.page.LOG_ = goog.debug.Logger.getLogger(
    'safaridriver.inject.page.' +
        (safaridriver.inject.page.EXTENSION ? 'extension' : 'webpage'));

/**
 * Initializes this script. When included in the SafariDriver extension, this
 * appends a SCRIPT element to the DOM that loads the page script. When
 * included as the page script, this sends a LOADED message to the injected
 * script, informing it that the page script has been successfully loaded.
 */
safaridriver.inject.page.init = function() {
  var script;
  if (safaridriver.inject.page.EXTENSION) {
    safaridriver.inject.page.LOG_.info('Initializing for extension');

    script = document.createElement('script');
    script.className = safaridriver.inject.page.SCRIPT_CLASS_NAME_;
    script.type = 'text/javascript';
    script.src = safari.extension.baseURI + 'page.js';
    document.documentElement.appendChild(script);
  } else {
    safaridriver.console.init();
    safaridriver.inject.page.LOG_.info('Initializing for page');

    window.addEventListener('message', safaridriver.inject.page.onMessage_,
        true);

    var message = new safaridriver.message.Message(
        safaridriver.message.Type.LOADED);
    safaridriver.inject.page.LOG_.info('Sending ' + message);
    message.send(window);

    script = document.querySelector(
        'script.' + safaridriver.inject.page.SCRIPT_CLASS_NAME_ +
            ':last-child');
    // If we find the script running this script, remove it.
    if (script) {
      goog.dom.removeNode(script);
    }
  }
};

if (!safaridriver.inject.page.EXTENSION) {
  goog.exportSymbol('init', safaridriver.inject.page.init);
}

/**
 * Responds to messages from the injected script.
 * @param {Event} e The message event.
 * @private
 */
safaridriver.inject.page.onMessage_ = function(e) {
  try {
    var message = safaridriver.message.Message.fromEvent(
        (/** @type {!MessageEvent} */e));
  } catch (ex) {
    // Silently ignore parse failure; message may not have origininated from the
    // injected script.
    return;
  }

  // Ignore messages that are from this context. How would we receive our own
  // messages?  Simple - when we post a message to the page, in addition to
  // going to the page, it will be posted back on our own window.
  if (message.getOrigin() === safaridriver.message.ORIGIN) {
    return;
  }

  safaridriver.inject.page.LOG_.info('onMessage: ' + JSON.stringify(message));
  switch (message.getType()) {
    case safaridriver.message.Type.COMMAND:
      safaridriver.inject.page.onCommand_(
          (/** @type {!safaridriver.message.CommandMessage} */message));
      return;

    default:
      return;  // Ignore unknown message type.
  }
};

/**
 * Handles command messages from the injected script.
 * @param {!safaridriver.message.CommandMessage} message The command message.
 * @throws {Error} If the command is not supported by this script.
 * @private
 */
safaridriver.inject.page.onCommand_ = function(message) {
  var command = message.getCommand();
  switch (command.getName()) {
    case webdriver.CommandName.EXECUTE_ASYNC_SCRIPT:
      safaridriver.inject.page.executeAsyncScript_(command);
      break;

    case webdriver.CommandName.EXECUTE_SCRIPT:
      safaridriver.inject.page.executeScript_(command);
      return;

    default:
      throw Error('Unknown command: ' + command.getName());
  }
};

/**
 * Locates an element by XPath.
 * @param {string} xpath The XPath expression.
 * @return {Element} The located element or {@code null}.
 * @private
 */
safaridriver.inject.page.getElementByXpath_ = function(xpath) {
  return document.evaluate(xpath, document, null,
      XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue;
};

/**
 * Computes the canonical XPath locator for an element.
 * @param {!Element} element The element to compute an XPath expression for.
 * @return {string} The element's XPath locator.
 * @private
 */
safaridriver.inject.page.getElementXPath_ = function(element) {
  var path = '';
  for(; element; element = bot.dom.getParentElement(element)) {
    var index = 1;
    for (var sibling = element.previousSibling; sibling;
        sibling = sibling.previousSibling) {
      if (sibling.nodeType == goog.dom.NodeType.ELEMENT &&
          sibling.tagName == element.tagName) {
        index++;
      }
    }
    var tmp = '/' + element.tagName;
    if (index > 1) {
      tmp += '[' + index + ']';
    }
    path = tmp + path;
  }
  return path;
};

/**
 * Key used in an object literal to indicate it is the encoded representation of
 * a DOM element. The corresponding property's value will be a CSS selector for
 * the encoded elmeent.
 * @type {string}
 * @const
 * @private
 */
safaridriver.inject.page.ENCODED_ELEMENT_KEY_ = 'WebElement';

/**
 * Encodes a value so it may be included in a message exchanged between the
 * document and sandboxed injected script. Any DOM element references will
 * be replaced with an object literal whose sole key is t
 * @param {*} value The value to encode.
 * @return {*} The encoded value.
 * @throws {Error} If the value is cannot be encoded (e.g. it is a function, or
 *     an array or object with a cyclical reference).
 */
safaridriver.inject.page.encodeValue = function(value) {
  var type = goog.typeOf(value);
  switch (type) {
    case 'boolean':
    case 'number':
    case 'string':
      return value;

    case 'null':
    case 'undefined':
      return null;

    case 'array':
      return goog.array.map((/** @type {!Array} */value),
          safaridriver.inject.page.encodeValue);

    case 'object':
      if (bot.dom.isElement(value)) {
        // We don't permit elements belong to another document because we
        // wouldn't be able to find them again on the other side when the
        // element was decoded.
        if (value.ownerDocument !== document) {
          throw Error('The element does not belong to this document: ' +
              safaridriver.inject.page.getElementCssSelector_(value));
        }

        var encoded = {};
        encoded[safaridriver.inject.page.ENCODED_ELEMENT_KEY_] =
            safaridriver.inject.page.getElementCssSelector_(value);
        return encoded;
      }
      return goog.object.map((/** @type {!Object} */value),
          safaridriver.inject.page.encodeValue);

    case 'function':
    default:
      throw Error('Invalid value type: ' + type + ' => ' + value);
  }
};

/**
 * Decodes a value. Any object literals whose sole key is
 * {@link safaridriver.inject.page.ENCODED_ELEMENT_KEY_} will be considered an
 * encoded reference to a DOM element. The corresponding value for this key will
 * be used as a CSS selector to locate the element.
 * @param {*} value The value to decode.
 * @return {*} The decoded value.
 * @throws {bot.Error} If an encoded DOM element cannot be located on the page.
 * @throws {Error} If the value is an invalid type, or an array or object with
 *     cyclical references.
 */
safaridriver.inject.page.decodeValue = function(value) {
  var type = goog.typeOf(value);
  switch (type) {
    case 'boolean':
    case 'number':
    case 'string':
      return value;

    case 'null':
    case 'undefined':
      return null;

    case 'array':
      return goog.array.map((/** @type {!Array} */value),
          safaridriver.inject.page.decodeValue);

    case 'object':
      var obj = (/** @type {!Object} */value);
      var keys = Object.keys(obj);
      if (keys.length == 1 &&
          keys[0] === safaridriver.inject.page.ENCODED_ELEMENT_KEY_) {
        var xpath = value[safaridriver.inject.page.ENCODED_ELEMENT_KEY_];
        var element = safaridriver.inject.page.getElementByXpath_(xpath);
        if (!element) {
          throw new bot.Error(bot.ErrorCode.STALE_ELEMENT_REFERENCE,
              'Unable to locate encoded element: ' + xpath);
        }
        return element;
      }
      return goog.object.map(obj, safaridriver.inject.page.decodeValue);

    case 'function':
    default:
      throw Error('Invalid value type: ' + type + ' => ' + value);
  }
};

/**
 * Handles an executeScript command.
 * @param {!safaridriver.Command} command The command to execute.
 * @private
 */
safaridriver.inject.page.executeScript_ = function(command) {
  var response;
  try {
    // TODO: clean-up bot.inject.executeScript so it doesn't pull in so many
    // extra dependencies.
    var fn = new Function(command.getParameter('script'));

    var args = command.getParameter('args');
    args = (/** @type {!Array} */safaridriver.inject.page.decodeValue(args));

    response = fn.apply(window, args);
    console.log('script response', response);
    response = safaridriver.inject.page.encodeValue(response);
    response = {
      'status': bot.ErrorCode.SUCCESS,
      'value': response
    };
  } catch (ex) {
    response = webdriver.error.createResponse(ex);
  }
  var responseMessage = new safaridriver.message.ResponseMessage(
      command.getId(), response);
  safaridriver.inject.page.LOG_.info(
      'Sending executeScript response: ' + responseMessage);
  responseMessage.send(window);
};

/**
 * Handles an executeAsyncScript command.
 * @param {!safaridriver.Command} command The command to execute.
 * @private
 */
safaridriver.inject.page.executeAsyncScript_ = function(command) {
  var response = new webdriver.promise.Deferred();
  // When the response is resolved, we want to wrap it up in a message and
  // send it back to the injected script. This does all that.
  response.
      then(function(value) {
        value = safaridriver.inject.page.encodeValue(value);
        return {
          'status': bot.ErrorCode.SUCCESS,
          'value': value
        };
      }).
      then(null, webdriver.error.createResponse).
      then(function(response) {
        var responseMessage = new safaridriver.message.ResponseMessage(
            command.getId(), response);
        safaridriver.inject.page.LOG_.info(
            'Sending executeAsyncScript response: ' + responseMessage);
        responseMessage.send(window);
      });

  try {
    var script = (/** @type {string} */command.getParameter('script'));
    var scriptFn = new Function(script);

    var args = command.getParameter('args');
    args = (/** @type {!Array} */safaridriver.inject.page.decodeValue(args));
    // The last argument for an async script is the callback that triggers the
    // response.
    args.push(function(value) {
      window.clearTimeout(timeoutId);
      if (response.isPending()) {
        response.resolve(value);
      }
    });

    var startTime = goog.now();
    scriptFn.apply(window, args);

    // Register our timeout *after* the function has been invoked. This will
    // ensure we don't timeout on a function that invokes its callback after a
    // 0-based timeout:
    // var scriptFn = function(callback) {
    //   setTimeout(callback, 0);
    // };
    var timeout = (/** @type {number} */command.getParameter('timeout'));
    var timeoutId = window.setTimeout(function() {
      if (response.isPending()) {
        response.reject(new bot.Error(bot.ErrorCode.SCRIPT_TIMEOUT,
            'Timed out waiting for an asynchronous script result after ' +
                (goog.now() - startTime) +  ' ms'));
      }
    }, Math.max(0, timeout));
  } catch (ex) {
    if (response.isPending()) {
      response.reject(ex);
    }
  }
};
<commit_msg>JasonLeyba: It would help if I ran the tests before committing code...<commit_after>// Copyright 2012 Software Freedom Conservancy. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Defines utilities for exchanging messages between the
 * sandboxed SafariDriver injected script and its corresponding content page.
 */

goog.provide('safaridriver.inject.page');

goog.require('bot.Error');
goog.require('bot.ErrorCode');
goog.require('bot.dom');
goog.require('goog.array');
goog.require('goog.debug.Logger');
goog.require('goog.dom');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.dom.classes');
goog.require('safaridriver.console');
goog.require('safaridriver.message');
goog.require('webdriver.CommandName');
goog.require('webdriver.error');
goog.require('webdriver.promise');

/**
 * @define {boolean} Whether this script is being used by the extension or has
 *     been injected into the web page.
 */
safaridriver.inject.page.EXTENSION = true;

/**
 * Class name assigned to the SCRIPT element used to insert the compiled form of
 * this script into the web page.
 * @type {string}
 * @const
 * @private
 */
safaridriver.inject.page.SCRIPT_CLASS_NAME_ = 'safari-driver';

/**
 * @type {!goog.debug.Logger}
 * @const
 * @private
 */
safaridriver.inject.page.LOG_ = goog.debug.Logger.getLogger(
    'safaridriver.inject.page.' +
        (safaridriver.inject.page.EXTENSION ? 'extension' : 'webpage'));

/**
 * Initializes this script. When included in the SafariDriver extension, this
 * appends a SCRIPT element to the DOM that loads the page script. When
 * included as the page script, this sends a LOADED message to the injected
 * script, informing it that the page script has been successfully loaded.
 */
safaridriver.inject.page.init = function() {
  var script;
  if (safaridriver.inject.page.EXTENSION) {
    safaridriver.inject.page.LOG_.info('Initializing for extension');

    script = document.createElement('script');
    script.className = safaridriver.inject.page.SCRIPT_CLASS_NAME_;
    script.type = 'text/javascript';
    script.src = safari.extension.baseURI + 'page.js';
    document.documentElement.appendChild(script);
  } else {
    safaridriver.console.init();
    safaridriver.inject.page.LOG_.info('Initializing for page');

    window.addEventListener('message', safaridriver.inject.page.onMessage_,
        true);

    var message = new safaridriver.message.Message(
        safaridriver.message.Type.LOADED);
    safaridriver.inject.page.LOG_.info('Sending ' + message);
    message.send(window);

    script = document.querySelector(
        'script.' + safaridriver.inject.page.SCRIPT_CLASS_NAME_ +
            ':last-child');
    // If we find the script running this script, remove it.
    if (script) {
      goog.dom.removeNode(script);
    }
  }
};

if (!safaridriver.inject.page.EXTENSION) {
  goog.exportSymbol('init', safaridriver.inject.page.init);
}

/**
 * Responds to messages from the injected script.
 * @param {Event} e The message event.
 * @private
 */
safaridriver.inject.page.onMessage_ = function(e) {
  try {
    var message = safaridriver.message.Message.fromEvent(
        (/** @type {!MessageEvent} */e));
  } catch (ex) {
    // Silently ignore parse failure; message may not have origininated from the
    // injected script.
    return;
  }

  // Ignore messages that are from this context. How would we receive our own
  // messages?  Simple - when we post a message to the page, in addition to
  // going to the page, it will be posted back on our own window.
  if (message.getOrigin() === safaridriver.message.ORIGIN) {
    return;
  }

  safaridriver.inject.page.LOG_.info('onMessage: ' + JSON.stringify(message));
  switch (message.getType()) {
    case safaridriver.message.Type.COMMAND:
      safaridriver.inject.page.onCommand_(
          (/** @type {!safaridriver.message.CommandMessage} */message));
      return;

    default:
      return;  // Ignore unknown message type.
  }
};

/**
 * Handles command messages from the injected script.
 * @param {!safaridriver.message.CommandMessage} message The command message.
 * @throws {Error} If the command is not supported by this script.
 * @private
 */
safaridriver.inject.page.onCommand_ = function(message) {
  var command = message.getCommand();
  switch (command.getName()) {
    case webdriver.CommandName.EXECUTE_ASYNC_SCRIPT:
      safaridriver.inject.page.executeAsyncScript_(command);
      break;

    case webdriver.CommandName.EXECUTE_SCRIPT:
      safaridriver.inject.page.executeScript_(command);
      return;

    default:
      throw Error('Unknown command: ' + command.getName());
  }
};

/**
 * Locates an element by XPath.
 * @param {string} xpath The XPath expression.
 * @return {Element} The located element or {@code null}.
 * @private
 */
safaridriver.inject.page.getElementByXpath_ = function(xpath) {
  return document.evaluate(xpath, document, null,
      XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue;
};

/**
 * Computes the canonical XPath locator for an element.
 * @param {!Element} element The element to compute an XPath expression for.
 * @return {string} The element's XPath locator.
 * @private
 */
safaridriver.inject.page.getElementXPath_ = function(element) {
  var path = '';
  for (var current = element; current;
       current = bot.dom.getParentElement(current)) {
    var index = 1;
    for (var sibling = current.previousSibling; sibling;
        sibling = sibling.previousSibling) {
      if (sibling.nodeType == goog.dom.NodeType.ELEMENT &&
          sibling.tagName == current.tagName) {
        index++;
      }
    }
    var tmp = '/' + current.tagName;
    if (index > 1) {
      tmp += '[' + index + ']';
    }
    path = tmp + path;
  }
  return path;
};

/**
 * Key used in an object literal to indicate it is the encoded representation of
 * a DOM element. The corresponding property's value will be a CSS selector for
 * the encoded elmeent.
 * @type {string}
 * @const
 * @private
 */
safaridriver.inject.page.ENCODED_ELEMENT_KEY_ = 'WebElement';

/**
 * Encodes a value so it may be included in a message exchanged between the
 * document and sandboxed injected script. Any DOM element references will
 * be replaced with an object literal whose sole key is t
 * @param {*} value The value to encode.
 * @return {*} The encoded value.
 * @throws {Error} If the value is cannot be encoded (e.g. it is a function, or
 *     an array or object with a cyclical reference).
 */
safaridriver.inject.page.encodeValue = function(value) {
  var type = goog.typeOf(value);
  switch (type) {
    case 'boolean':
    case 'number':
    case 'string':
      return value;

    case 'null':
    case 'undefined':
      return null;

    case 'array':
      return goog.array.map((/** @type {!Array} */value),
          safaridriver.inject.page.encodeValue);

    case 'object':
      if (goog.dom.isElement(value)) {
        // We don't permit elements belong to another document because we
        // wouldn't be able to find them again on the other side when the
        // element was decoded.
        if (value.ownerDocument !== document) {
          throw Error('The element does not belong to this document: ' +
              safaridriver.inject.page.getElementXPath_(
                  (/** @type {!Element} */value)));
        }

        var encoded = {};
        encoded[safaridriver.inject.page.ENCODED_ELEMENT_KEY_] =
            safaridriver.inject.page.getElementXPath_(
                (/** @type {!Element} */value));
        return encoded;
      }
      return goog.object.map((/** @type {!Object} */value),
          safaridriver.inject.page.encodeValue);

    case 'function':
    default:
      throw Error('Invalid value type: ' + type + ' => ' + value);
  }
};

/**
 * Decodes a value. Any object literals whose sole key is
 * {@link safaridriver.inject.page.ENCODED_ELEMENT_KEY_} will be considered an
 * encoded reference to a DOM element. The corresponding value for this key will
 * be used as a CSS selector to locate the element.
 * @param {*} value The value to decode.
 * @return {*} The decoded value.
 * @throws {bot.Error} If an encoded DOM element cannot be located on the page.
 * @throws {Error} If the value is an invalid type, or an array or object with
 *     cyclical references.
 */
safaridriver.inject.page.decodeValue = function(value) {
  var type = goog.typeOf(value);
  switch (type) {
    case 'boolean':
    case 'number':
    case 'string':
      return value;

    case 'null':
    case 'undefined':
      return null;

    case 'array':
      return goog.array.map((/** @type {!Array} */value),
          safaridriver.inject.page.decodeValue);

    case 'object':
      var obj = (/** @type {!Object} */value);
      var keys = Object.keys(obj);
      if (keys.length == 1 &&
          keys[0] === safaridriver.inject.page.ENCODED_ELEMENT_KEY_) {
        var xpath = value[safaridriver.inject.page.ENCODED_ELEMENT_KEY_];
        var element = safaridriver.inject.page.getElementByXpath_(xpath);
        if (!element) {
          throw new bot.Error(bot.ErrorCode.STALE_ELEMENT_REFERENCE,
              'Unable to locate encoded element: ' + xpath);
        }
        return element;
      }
      return goog.object.map(obj, safaridriver.inject.page.decodeValue);

    case 'function':
    default:
      throw Error('Invalid value type: ' + type + ' => ' + value);
  }
};

/**
 * Handles an executeScript command.
 * @param {!safaridriver.Command} command The command to execute.
 * @private
 */
safaridriver.inject.page.executeScript_ = function(command) {
  var response;
  try {
    // TODO: clean-up bot.inject.executeScript so it doesn't pull in so many
    // extra dependencies.
    var fn = new Function(command.getParameter('script'));

    var args = command.getParameter('args');
    args = (/** @type {!Array} */safaridriver.inject.page.decodeValue(args));

    response = fn.apply(window, args);
    console.log('script response', response);
    response = safaridriver.inject.page.encodeValue(response);
    response = {
      'status': bot.ErrorCode.SUCCESS,
      'value': response
    };
  } catch (ex) {
    response = webdriver.error.createResponse(ex);
  }
  var responseMessage = new safaridriver.message.ResponseMessage(
      command.getId(), response);
  safaridriver.inject.page.LOG_.info(
      'Sending executeScript response: ' + responseMessage);
  responseMessage.send(window);
};

/**
 * Handles an executeAsyncScript command.
 * @param {!safaridriver.Command} command The command to execute.
 * @private
 */
safaridriver.inject.page.executeAsyncScript_ = function(command) {
  var response = new webdriver.promise.Deferred();
  // When the response is resolved, we want to wrap it up in a message and
  // send it back to the injected script. This does all that.
  response.
      then(function(value) {
        value = safaridriver.inject.page.encodeValue(value);
        return {
          'status': bot.ErrorCode.SUCCESS,
          'value': value
        };
      }).
      then(null, webdriver.error.createResponse).
      then(function(response) {
        var responseMessage = new safaridriver.message.ResponseMessage(
            command.getId(), response);
        safaridriver.inject.page.LOG_.info(
            'Sending executeAsyncScript response: ' + responseMessage);
        responseMessage.send(window);
      });

  try {
    var script = (/** @type {string} */command.getParameter('script'));
    var scriptFn = new Function(script);

    var args = command.getParameter('args');
    args = (/** @type {!Array} */safaridriver.inject.page.decodeValue(args));
    // The last argument for an async script is the callback that triggers the
    // response.
    args.push(function(value) {
      window.clearTimeout(timeoutId);
      if (response.isPending()) {
        response.resolve(value);
      }
    });

    var startTime = goog.now();
    scriptFn.apply(window, args);

    // Register our timeout *after* the function has been invoked. This will
    // ensure we don't timeout on a function that invokes its callback after a
    // 0-based timeout:
    // var scriptFn = function(callback) {
    //   setTimeout(callback, 0);
    // };
    var timeout = (/** @type {number} */command.getParameter('timeout'));
    var timeoutId = window.setTimeout(function() {
      if (response.isPending()) {
        response.reject(new bot.Error(bot.ErrorCode.SCRIPT_TIMEOUT,
            'Timed out waiting for an asynchronous script result after ' +
                (goog.now() - startTime) +  ' ms'));
      }
    }, Math.max(0, timeout));
  } catch (ex) {
    if (response.isPending()) {
      response.reject(ex);
    }
  }
};
<|endoftext|>


<commit_before>/**
 *	@author bh-lay
 *	@github https://github.com/bh-lay/uploader
 *  @updata 2014-4-1 22:59
 *
 */
window.util = window.util || {};
/**
 * until.drag(handle_dom,dom,fn);
 */
(function uploaderClosure(exports){
	var staticID = 0;
	var upCnt_tpl = ['

'].join('');
	var up_tpl = ['
',
		'',
		'<form method="post" action="{action}" enctype="multipart/form-data" name="uploader" target="uploader{ID}">',
			//请选择图片
			'<input name="{fileinputname}" type="file" multiple="multiple" class="uploader_btn" title="\u8BF7\u9009\u62E9\u56FE\u7247"/>',
			'{data}',
		'</form>',
	'
'].join('');

	var input_tpl = '<input type="hidden" name="{name}" value="{value}" />';

	var up_css = [''].join('');

	//创建工作环境
	var global_cnt = $(upCnt_tpl);

	$('body').append(global_cnt);
	$('head').append(up_css);

	//iframe加载事件监听方法
	function iframe_load(iframe,load){
		if (iframe.attachEvent){
			iframe.attachEvent("onload", function(){
				load&&load.call(iframe.contentDocument);
			});
		}else{
			iframe.onload = function(){
				load&&load.call(iframe.contentDocument);
			};
		}
	}
	//创建新的html
	function buildDom(){
		var this_up = this;
		var data_html = '';
		var data = this.data;
		for(var i in data){
			data_html += input_tpl.replace(/{(\w*)}/g,function(result,key){
				if(key == 'name'){
					return i;
				}else if(key == 'value'){
					return data[i]
				}
			});
		}

		var html = up_tpl.replace(/{(\w*)}/g,function(result,key){
			if(key == 'ID'){
				return this_up.ID
			}else if(key == 'action'){
				return this_up.action
			}else if(key == 'fileinputname'){
				return this_up.fileinputname
			}else if(key == "data"){
				return data_html;
			}
		});
		return $(html);
	}
	//获取文件信息
	function getFilesFromInput(input){
		var returns = [];
		if(input.value.length > 0){
			if(input.files){
				for(var i=0,total=input.files.length;i<total;i++){
					returns.push({
						'name' : input.files[i]['name'],
						'size' : input.files[i]['size']
					})
				}
			}else{
				returns.push({
					'name' : input.value.split(/\\/).pop()
				});
			}
		}
		return returns;
	}
	//格式化JSON
	function parseJSON(input){
		//去除部分浏览器自动添加的标签
		input = input.replace(/<[^>]+>|<\/[^>]+>/g,'');
		var output = {};
		//尝试去解析
		try{
			output = eval('(' + input + ')');
		}catch(e){
			output = null;
		}
		return output;
	}
	//构造新的单次上传模块
	function SingleUp(param){
		var this_up = this;
		this.ID = ++staticID;
		this.action = param.action;
		this.data = param['data'] || {};

		//状态 等待上传|上传中
		//    wait | uploading
		this.status = 'wait';
		this.responseParser = param['responseParser'] || null;
		this.fileinputname = param['fileinputname'] || 'photos111';
		//事件集
		this.onHide = param.onHide || null;
		this.onMousedown = param.onMousedown || null;
		this.beforeUpload = param.beforeUpload || null;
		this.onStartUpload = param.onStartUpload || null;
		this.onFail = param.onFail || null;
		this.onSuccess = param.onSuccess || null;

		this.dom = buildDom.call(this);

		var position = param.position;

		this.dom.css({
			'top' : position.top,
			'left' : position.left,
			'width' : position.width,
			'height' : position.height
		});
		var DOM = this.dom;
		global_cnt.append(DOM);
		/**
		 * 这个定时器是为了处理IE7异步创建iframe的BUG
		 * 如果你有好的办法，请  mail:bh_lay@126.com
		 */
		setTimeout(function(){
			this_up.init();
		},100);
	}
	//销毁自己
	SingleUp.prototype.destory = function(){
//			console.log('destory-2');
//			console.log('---------------');
		this.onHide && this.onHide();
		if(this.status == 'wait'){
			this.dom.remove();
		}else{
			this.dom.hide();
		}
	};
	//处理response
	SingleUp.prototype.handleResponse = function(responseTXT){
		var ID = this.ID;
		this.status = 'wait';
		//是否已格式化数据方法
		if(this.responseParser){
			var jsonData = parseJSON(responseTXT);
			if(jsonData == null){
				//服务器数据异常！
				this.onFail && this.onFail(ID,'\u670D\u52A1\u5668\u6570\u636E\u5F02\u5E38\uFF01');
			}else{
				//数据交由开发者格式化
				var parserData = this.responseParser(jsonData);

				if(typeof(parserData) != 'object'){
					this.onFail && this.onFail(ID);
				}else{
					if(parserData['files'] && parserData['files']['length'] > 0){
						this.onSuccess && this.onSuccess(ID,parserData['files'],parserData['extra']);
					}else{
						this.onFail && this.onFail(ID,parserData['extra']);
					}
				}
			}
		}else{
			//需要定义responseParser
			this.onFail && this.onFail(ID,'you need define method : responseParser! ');
		}
		//销毁单次上传模块
		this.destory();
	}
	//初始化单次上传模块
	SingleUp.prototype.init = function(){
		var this_up = this;

		var uploaderDom = this.dom;
		var iframe = this.dom.find('iframe')[0];
		var ID = this.ID;
		//事件处理
		uploaderDom.on('mouseout',function(){
		//	this_up.destory();
			//FIXME ie11 destory bug
			uploaderDom.hide();
		}).on('mousedown',function(){
			this_up.onMousedown && this_up.onMousedown();
		}).on('mousemove',function(e){
			var oW = uploaderDom.width();
			var oL = uploaderDom.offset().left;
			var mL = e.pageX;
			uploaderDom.find('.uploader_btn').css('right',oL+oW-mL-25);
		}).on('change','.uploader_btn',function(){
			var input = this;
			var can_start = true;
			//检测是否已选择文件
			var files = getFilesFromInput(input);
			if(files.length > 0){
				//触发上传前事件
				if(this_up.beforeUpload){
					can_start = this_up.beforeUpload(ID,files);
				}else{
					can_start = true;
				}
				if(can_start){
					uploaderDom.hide();
					//开始上传文件
					$(this).parents('form').submit();
					this_up.status = 'uploading';
					//触发上传事件
					this_up.onStartUpload && this_up.onStartUpload(ID,files);
				}else{
					//重置html
					input.outerHTML = input.outerHTML;
				}
			}
		});
		iframe_load(iframe,function(){
			var responseTXT = this.body.innerHTML;
			this_up.handleResponse(responseTXT);
		});
	};

	////

	function uploader(param){
		var this_up = this;
		//事件堆
		this.events = {};
		this.action = param['action'] || null;
		this.data = param['data'] || {};
		//绑定上传方法的DOM
		this.dom = param['dom'];
		this.fileinputname = param['fileinputname'] || 'photos121';
		//是否可上传
		this.can_upload = true;
		this.responseParser = param['responseParser'] || null;

		//为按钮绑定悬停事件
		var delay;
		this.dom.mouseenter(function(){
	//		clearTimeout(delay);
			var btn = $(this);
	//		delay = setTimeout(function(){
				this_up.createSingleUp(btn);
	//		},80);
		}).mouseleave(function(){
	//		clearTimeout(delay);
		});
	}
	uploader.prototype = {
		'on' : function (eventName,callback){
			//事件堆无该事件，创建一个事件堆
			if(!this.events[eventName]){
				this.events[eventName] = [];
			}
			this.events[eventName].push(callback);
			return this;
		},
		'emit' : function (eventName,args){
			//事件堆无该事件，结束运行
			if(!this.events[eventName]){
				return
			}
			args = args || null;
			for(var i=0,total=this.events[eventName].length;i<total;i++){
				this.events[eventName][i].apply(this.event_global || this , args);
			}
		},
		'createSingleUp' : function(BTN){
			BTN.addClass('hover');
			var offset = BTN.offset();
			var this_up = this;
			var newUp = new SingleUp({
				'action' : this.action,
				'data' : this.data,
				'fileinputname' : this.fileinputname,
				'responseParser' : this.responseParser,
				'position' : {
					'top' : offset.top,
					'left' : offset.left,
					'width' : BTN.outerWidth(),
					'height' : BTN.outerHeight()
				},
				'beforeUpload' : function(ID,files){
					this_up.emit('beforeUpload',[ID,files]);
					if(this_up.can_upload){
						return true;
					}else{
						return false;
					}
				},
				'onStartUpload' : function(ID,files){
					this_up.emit('startUpload',[ID,files]);
				},
				'onSuccess' : function(ID,files,extra){
					this_up.emit('success',[ID,files,extra]);
				},
				'onFail' : function(ID,extra){
					this_up.emit('fail',[ID,extra]);
				},
				'onHide' : function(){
					BTN.removeClass('hover');
					BTN.removeClass('active');
				},
				'onMousedown' : function(){
					BTN.removeClass('hover')
					BTN.addClass('active');
				}
			});
		}
	};
	exports.uploader = uploader;
})(window.util);

//提供CommonJS规范的接口
window.define && define(function(require,exports,module){
	//对外接口
	return window.util.uploader;
});<commit_msg>修改上传失败时事件名错误<commit_after>/**
 *	@author bh-lay
 *	@github https://github.com/bh-lay/uploader
 *  @updata 2014-4-28 15:46
 *
 */
window.util = window.util || {};
/**
 * until.drag(handle_dom,dom,fn);
 */
(function uploaderClosure(exports){
	var staticID = 0;
	var upCnt_tpl = ['

'].join('');
	var up_tpl = ['
',
		'',
		'<form method="post" action="{action}" enctype="multipart/form-data" name="uploader" target="uploader{ID}">',
			//请选择图片
			'<input name="{fileinputname}" type="file" multiple="multiple" class="uploader_btn" title="\u8BF7\u9009\u62E9\u56FE\u7247"/>',
			'{data}',
		'</form>',
	'
'].join('');

	var input_tpl = '<input type="hidden" name="{name}" value="{value}" />';

	var up_css = [''].join('');

	//创建工作环境
	var global_cnt = $(upCnt_tpl);

	$('body').append(global_cnt);
	$('head').append(up_css);

	//iframe加载事件监听方法
	function iframe_load(iframe,load){
		if (iframe.attachEvent){
			iframe.attachEvent("onload", function(){
				load&&load.call(iframe.contentDocument);
			});
		}else{
			iframe.onload = function(){
				load&&load.call(iframe.contentDocument);
			};
		}
	}
	//创建新的html
	function buildDom(){
		var this_up = this;
		var data_html = '';
		var data = this.data;
		for(var i in data){
			data_html += input_tpl.replace(/{(\w*)}/g,function(result,key){
				if(key == 'name'){
					return i;
				}else if(key == 'value'){
					return data[i]
				}
			});
		}

		var html = up_tpl.replace(/{(\w*)}/g,function(result,key){
			if(key == 'ID'){
				return this_up.ID
			}else if(key == 'action'){
				return this_up.action
			}else if(key == 'fileinputname'){
				return this_up.fileinputname
			}else if(key == "data"){
				return data_html;
			}
		});
		return $(html);
	}
	//获取文件信息
	function getFilesFromInput(input){
		var returns = [];
		if(input.value.length > 0){
			if(input.files){
				for(var i=0,total=input.files.length;i<total;i++){
					returns.push({
						'name' : input.files[i]['name'],
						'size' : input.files[i]['size']
					})
				}
			}else{
				returns.push({
					'name' : input.value.split(/\\/).pop()
				});
			}
		}
		return returns;
	}
	//格式化JSON
	function parseJSON(input){
		//去除部分浏览器自动添加的标签
		input = input.replace(/<[^>]+>|<\/[^>]+>/g,'');
		var output = {};
		//尝试去解析
		try{
			output = eval('(' + input + ')');
		}catch(e){
			output = 'fail';
		}
		return output;
	}
	//构造新的单次上传模块
	function SingleUp(param){
		var this_up = this;
		this.ID = ++staticID;
		this.action = param.action;
		this.data = param['data'] || {};

		//状态 等待上传|上传中
		//    wait | uploading
		this.status = 'wait';
		this.responseParser = param['responseParser'] || null;
		this.fileinputname = param['fileinputname'] || 'photos';
		//事件集
		this.onHide = param.onHide || null;
		this.onMousedown = param.onMousedown || null;
		this.beforeUpload = param.beforeUpload || null;
		this.onStartUpload = param.onStartUpload || null;
		this.onFail = param.onFail || null;
		this.onSuccess = param.onSuccess || null;

		this.dom = buildDom.call(this);

		var position = param.position;

		this.dom.css({
			'top' : position.top,
			'left' : position.left,
			'width' : position.width,
			'height' : position.height
		});

		global_cnt.append(this.dom);

		/**
		 * 这个定时器是为了处理IE7异步创建iframe的BUG
		 * 如果你有好的办法，请  mail:bh_lay@126.com
		 */
		setTimeout(function(){
			this_up.init();
		},100);
	}
	//销毁自己
	SingleUp.prototype.destory = function(){
		this.onHide && this.onHide();
		if(this.status == 'wait'){
			this.dom.remove();
		}else{
			this.dom.hide();
		}
	};
	//处理response
	SingleUp.prototype.handleResponse = function(responseTXT){
		var ID = this.ID;
		this.status = 'wait';
		//是否已格式化数据方法
		if(this.responseParser){
			var jsonData = parseJSON(responseTXT);
			if(jsonData == 'fail'){
					//服务器数据异常！
				this.onFail && this.onFail(ID,'\u670D\u52A1\u5668\u6570\u636E\u5F02\u5E38\uFF01');
			}else{
				//数据交由开发者格式化
				var parserData = this.responseParser(jsonData);

				if(typeof(parserData) != 'object'){
					this.onFail && this.onFail(ID);
				}else{
					if(parserData['files'] && parserData['files']['length'] > 0){
						this.onSuccess && this.onSuccess(ID,parserData['files'],parserData['extra']);
					}else{
						this.onFail && this.onFail(ID,parserData['extra']);
					}
				}
			}
		}else{
			this.onFail && this.onFail(ID,'you need define method : responseParser! ');
		}
		//销毁单次上传模块
		this.destory();
	}
	//初始化单次上传模块
	SingleUp.prototype.init = function(){
		var this_up = this;

		var uploaderDom = this.dom;
		var iframe = this.dom.find('iframe')[0];
		var ID = this.ID;
		//事件处理
		uploaderDom.on('mouseleave',function(){
	//		setTimeout(function(){
				this_up.destory();
		//	},100);
		}).on('mousedown',function(){
			this_up.onMousedown && this_up.onMousedown();
		}).on('mousemove',function(e){
			var oW = uploaderDom.width();
			var oL = uploaderDom.offset().left;
			var mL = e.pageX;
			uploaderDom.find('.uploader_btn').css('right',oL+oW-mL-25);
		}).on('change','.uploader_btn',function(){
			var input = this;
			var can_start = true;
			//检测是否已选择文件
			var files = getFilesFromInput(input);
			if(files.length > 0){
				//触发上传前事件
				if(this_up.beforeUpload){
					can_start = this_up.beforeUpload(ID,files);
				}else{
					can_start = true;
				}
				if(can_start){
					uploaderDom.hide();
					//开始上传文件
					$(this).parents('form').submit();
					this_up.status = 'uploading';
					//触发上传事件
					this_up.onStartUpload && this_up.onStartUpload(ID,files);
				}else{
					//重置html
					input.outerHTML = input.outerHTML;
				}
			}
		});
		iframe_load(iframe,function(){
			var responseTXT = this.body.innerHTML;
			this_up.handleResponse(responseTXT);
		});
	};

	////

	function uploader(param){
		var this_up = this;
		//事件堆
		this.events = {};
		this.action = param['action'] || null;
		this.data = param['data'] || {};
		//绑定上传方法的DOM
		this.dom = param['dom'];
		this.fileinputname = param['fileinputname'] || 'photos';
		//是否可上传
		this.can_upload = true;
		this.responseParser = param['responseParser'] || null;

		//为按钮绑定悬停事件
		var delay;
		this.dom.mouseenter(function(){
			clearTimeout(delay);
			var btn = $(this);
			delay = setTimeout(function(){
				this_up.createSingleUp(btn);
			},100);
		}).mouseleave(function(){
			clearTimeout(delay);
		});
	}
	uploader.prototype = {
		'on' : function (eventName,callback){
			//事件堆无该事件，创建一个事件堆
			if(!this.events[eventName]){
				this.events[eventName] = [];
			}
			this.events[eventName].push(callback);
			return this;
		},
		'emit' : function (eventName,args){
			//事件堆无该事件，结束运行
			if(!this.events[eventName]){
				return
			}
			args = args || null;
			for(var i=0,total=this.events[eventName].length;i<total;i++){
				this.events[eventName][i].apply(this.event_global || this , args);
			}
		},
		'createSingleUp' : function(BTN){
			BTN.addClass('hover');
			var offset = BTN.offset();
			var this_up = this;
			var newUp = new SingleUp({
				'action' : this.action,
				'data' : this.data,
				'responseParser' : this.responseParser,
				'position' : {
					'top' : offset.top,
					'left' : offset.left,
					'width' : BTN.outerWidth(),
					'height' : BTN.outerHeight()
				},
				'beforeUpload' : function(ID,files){
					this_up.emit('beforeUpload',[ID,files]);
					if(this_up.can_upload){
						return true;
					}else{
						return false;
					}
				},
				'onStartUpload' : function(ID,files){
					this_up.emit('startUpload',[ID,files]);
				},
				'onSuccess' : function(ID,files,extra){
					this_up.emit('success',[ID,files,extra]);
				},
				'onFail' : function(ID,extra){
					this_up.emit('error',[ID,extra]);
				},
				'onHide' : function(){
					BTN.removeClass('hover');
					BTN.removeClass('active');
				},
				'onMousedown' : function(){
					BTN.removeClass('hover')
					BTN.addClass('active');
				}
			});
		}
	};
	exports.uploader = uploader;
})(window.util);

//提供CommonJS规范的接口
window.define && define(function(require,exports,module){
	//对外接口
	return window.util.uploader;
});<|endoftext|>


<commit_before>CLASS({
  name: 'EMailCitationView',
  package: 'com.google.mail',
  extendsModel: 'DetailView',
  imports: [
    'controller'
  ],
  properties: [
    { name: 'className', defaultValue: 'email-citation' },
    {
      name: 'preferredHeight',
      help: 'Specifying the preferred height of this view for the ScrollView, since an empty row is too small.',
      defaultValue: 82
    }
  ],
  templates: [
    function CSS() {/*
      .email-citation {
        display: flex;
        display: -webkit-flex;
        border-bottom: solid #B5B5B5 1px;
        padding: 10px 14px 10px 6px;
      }

      .email-citation.unread {
        font-weight: bold;
      }

      .email-citation .from {
        display: block;
        font-size: 17px;
        line-height: 24px;
        white-space: nowrap;
        overflow-x:hidden;
        text-overflow: ellipsis;
        flex-grow: 1;
        -webkit-flex-grow: 1;
      }

      .email-citation .timestamp {
        font-size: 12px;
        color: rgb(17, 85, 204);
        white-space: nowrap;
        flex-shrink: 0;
        -webkit-flex-shrink: 0;
      }

      .email-citation .subject {
        display: block;
        font-size: 13px;
        line-height: 17px;
        overflow-x:hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }

      .email-citation .snippet {
        color: rgb(119, 119, 119);
        display: block;
        font-size: 13px;
        height: 20px;
        overflow-x: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }

      .email-citation .monogram-string-view {
        margin: auto 6px auto 0;
      }
    */},
    function toHTML() {/*
      <%
        var id = this.setClass('unread', function() { return self.data && self.data.unread; }, this.id);
        this.on('click', function() { this.controller.open(this.data.id); }, this.id);
      %>

      <div id="<%= id %>" %%cssClassAttr() >
        $$from{model_: 'foam.ui.md.MonogramStringView'}

            $$from{mode: 'read-only', className: 'from', escapeHTML: true}
            $$timestamp{ model_: 'RelativeDateTimeFieldView', mode: 'read-only', className: 'timestamp' }

              $$subject{mode: 'read-only', className: 'subject'}
              $$snippet{mode: 'read-only', className: 'snippet'}

            $$starred{
              model_: 'ImageBooleanView',
              className:  'star',
              trueImage:  'images/ic_star_24dp.png',
              falseImage: 'images/ic_star_outline_24dp.png'
            }

    */}
   ]
});
<commit_msg>Add missing require for MonogramStringView<commit_after>CLASS({
  name: 'EMailCitationView',
  package: 'com.google.mail',
  extendsModel: 'DetailView',
  requires: [
    'foam.ui.md.MonogramStringView'
  ],
  imports: [
    'controller'
  ],
  properties: [
    { name: 'className', defaultValue: 'email-citation' },
    {
      name: 'preferredHeight',
      help: 'Specifying the preferred height of this view for the ScrollView, since an empty row is too small.',
      defaultValue: 82
    }
  ],
  templates: [
    function CSS() {/*
      .email-citation {
        display: flex;
        display: -webkit-flex;
        border-bottom: solid #B5B5B5 1px;
        padding: 10px 14px 10px 6px;
      }

      .email-citation.unread {
        font-weight: bold;
      }

      .email-citation .from {
        display: block;
        font-size: 17px;
        line-height: 24px;
        white-space: nowrap;
        overflow-x:hidden;
        text-overflow: ellipsis;
        flex-grow: 1;
        -webkit-flex-grow: 1;
      }

      .email-citation .timestamp {
        font-size: 12px;
        color: rgb(17, 85, 204);
        white-space: nowrap;
        flex-shrink: 0;
        -webkit-flex-shrink: 0;
      }

      .email-citation .subject {
        display: block;
        font-size: 13px;
        line-height: 17px;
        overflow-x:hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }

      .email-citation .snippet {
        color: rgb(119, 119, 119);
        display: block;
        font-size: 13px;
        height: 20px;
        overflow-x: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }

      .email-citation .monogram-string-view {
        margin: auto 6px auto 0;
      }
    */},
    function toHTML() {/*
      <%
        var id = this.setClass('unread', function() { return self.data && self.data.unread; }, this.id);
        this.on('click', function() { this.controller.open(this.data.id); }, this.id);
      %>

      <div id="<%= id %>" %%cssClassAttr() >
        $$from{model_: 'foam.ui.md.MonogramStringView'}

            $$from{mode: 'read-only', className: 'from', escapeHTML: true}
            $$timestamp{ model_: 'RelativeDateTimeFieldView', mode: 'read-only', className: 'timestamp' }

              $$subject{mode: 'read-only', className: 'subject'}
              $$snippet{mode: 'read-only', className: 'snippet'}

            $$starred{
              model_: 'ImageBooleanView',
              className:  'star',
              trueImage:  'images/ic_star_24dp.png',
              falseImage: 'images/ic_star_outline_24dp.png'
            }

    */}
   ]
});
<|endoftext|>