diff --git a/airtime_mvc/application/controllers/PlaylistController.php b/airtime_mvc/application/controllers/PlaylistController.php index c508f5037..04915c07b 100644 --- a/airtime_mvc/application/controllers/PlaylistController.php +++ b/airtime_mvc/application/controllers/PlaylistController.php @@ -165,15 +165,18 @@ class PlaylistController extends Zend_Controller_Action $ids = $this->_getParam('ids'); $ids = (!is_array($ids)) ? array($ids) : $ids; $afterItem = $this->_getParam('afterItem', null); - //$afterItem = (!is_numeric($afterItem)) ? null : intval($afterItem); + $addType = $this->_getParam('type', 'after'); + + Logging::log("type is ".$addType); try { $pl = $this->getPlaylist(); - $pl->addAudioClips($ids, $afterItem); + $pl->addAudioClips($ids, $afterItem, $addType); } catch (PlaylistNotFoundException $e) { - Logging::log("Playlist {$pl_id} not found"); + Logging::log("Playlist not found"); $this->changePlaylist(null); + $this->createFullResponse(null); } catch (Exception $e) { Logging::log("{$e->getFile()}"); @@ -189,15 +192,15 @@ class PlaylistController extends Zend_Controller_Action $ids = $this->_getParam('ids'); $ids = (!is_array($ids)) ? array($ids) : $ids; $afterItem = $this->_getParam('afterItem', null); - //$afterItem = (!is_numeric($afterItem)) ? null : intval($afterItem); try { $pl = $this->getPlaylist(); $pl->moveAudioClips($ids, $afterItem); } catch (PlaylistNotFoundException $e) { - Logging::log("Playlist {$pl_id} not found"); + Logging::log("Playlist not found"); $this->changePlaylist(null); + $this->createFullResponse(null); } catch (Exception $e) { Logging::log("{$e->getFile()}"); @@ -218,8 +221,9 @@ class PlaylistController extends Zend_Controller_Action $pl->delAudioClips($ids); } catch (PlaylistNotFoundException $e) { - Logging::log("Playlist {$pl_id} not found"); + Logging::log("Playlist not found"); $this->changePlaylist(null); + $this->createFullResponse(null); } catch (Exception $e) { Logging::log("{$e->getFile()}"); @@ -232,23 +236,30 @@ class PlaylistController extends Zend_Controller_Action public function setCueAction() { - $pos = $this->_getParam('pos'); - $pl = $this->getPlaylist(); - if ($pl === false){ - $this->view->playlist_error = true; - return false; - } - + $id = $this->_getParam('id'); $cueIn = $this->_getParam('cueIn', null); $cueOut = $this->_getParam('cueOut', null); - $response = $pl->changeClipLength($pos, $cueIn, $cueOut); + try { + $pl = $this->getPlaylist(); + $response = $pl->changeClipLength($id, $cueIn, $cueOut); - $this->view->response = $response; + $this->view->response = $response; - if(!isset($response["error"])) { - $this->createUpdateResponse($pl); - } + if(!isset($response["error"])) { + $this->createUpdateResponse($pl); + } + } + catch (PlaylistNotFoundException $e) { + Logging::log("Playlist not found"); + $this->changePlaylist(null); + $this->createFullResponse(null); + } + catch (Exception $e) { + Logging::log("{$e->getFile()}"); + Logging::log("{$e->getLine()}"); + Logging::log("{$e->getMessage()}"); + } } public function setFadeAction() diff --git a/airtime_mvc/application/models/Playlist.php b/airtime_mvc/application/models/Playlist.php index 2023da474..f88053ff8 100644 --- a/airtime_mvc/application/models/Playlist.php +++ b/airtime_mvc/application/models/Playlist.php @@ -209,8 +209,10 @@ class Application_Model_Playlist { * an array of audioclips to add to the playlist * @param int|null $p_afterItem * item which to add the new items after in the playlist, null if added to the end. + * @param string (before|after) $addAfter + * whether to add the clips before or after the selected item. */ - public function addAudioClips($p_items, $p_afterItem=NULL) + public function addAudioClips($p_items, $p_afterItem=NULL, $addType = 'after') { $this->con->beginTransaction(); $contentsToUpdate = array(); @@ -221,11 +223,14 @@ class Application_Model_Playlist { Logging::log("Finding playlist content item {$p_afterItem}"); $afterItem = CcPlaylistcontentsQuery::create()->findPK($p_afterItem); - $pos = $afterItem->getDbPosition() + 1; + + $index = $afterItem->getDbPosition(); + Logging::log("index is {$index}"); + $pos = ($addType == 'after') ? $index + 1 : $index; $contentsToUpdate = CcPlaylistcontentsQuery::create() ->filterByDbPlaylistId($this->id) - ->filterByDbPosition($pos-1, Criteria::GREATER_THAN) + ->filterByDbPosition($pos, Criteria::GREATER_EQUAL) ->orderByDbPosition() ->find($this->con); @@ -234,8 +239,9 @@ class Application_Model_Playlist { } else { - $pos = $this->getSize(); - Logging::log("Adding to end of playlist"); + $pos = ($addType == 'after') ? $this->getSize() : 0; + + Logging::log("Adding to playlist"); Logging::log("at position {$pos}"); } @@ -449,22 +455,6 @@ class Application_Model_Playlist { return array("fadeIn"=>$fadeIn, "fadeOut"=>$fadeOut); } - public function getCueInfo($pos) { - - $row = CcPlaylistcontentsQuery::create() - ->joinWith(CcFilesPeer::OM_CLASS) - ->filterByDbPlaylistId($this->id) - ->filterByDbPosition($pos) - ->findOne(); - - $file = $row->getCcFiles(); - $origLength = $file->getDbLength(); - $cueIn = $row->getDBCuein(); - $cueOut = $row->getDbCueout(); - - return array($cueIn, $cueOut, $origLength); - } - /** * Change cueIn/cueOut values for playlist element * @@ -476,27 +466,26 @@ class Application_Model_Playlist { * new value in ss.ssssss or extent format * @return boolean or pear error object */ - public function changeClipLength($pos, $cueIn, $cueOut) + public function changeClipLength($id, $cueIn, $cueOut) { $errArray= array(); $con = Propel::getConnection(CcPlaylistPeer::DATABASE_NAME); - if(is_null($cueIn) && is_null($cueOut)) { + if (is_null($cueIn) && is_null($cueOut)) { $errArray["error"]="Cue in and cue out are null."; return $errArray; } - if(is_null($pos) || $pos < 0 || $pos >= $this->getNextPos()) { - $errArray["error"]="Invalid position."; - return $errArray; - } - $row = CcPlaylistcontentsQuery::create() ->joinWith(CcFilesPeer::OM_CLASS) - ->filterByDbPlaylistId($this->id) - ->filterByDbPosition($pos) + ->filterByPrimaryKey($id) ->findOne(); + if (is_null($row)) { + $errArray["error"]="Playlist item does not exist!."; + return $errArray; + } + $oldCueIn = $row->getDBCuein(); $oldCueOut = $row->getDbCueout(); $fadeIn = $row->getDbFadein(); diff --git a/airtime_mvc/application/views/scripts/playlist/set-cue.phtml b/airtime_mvc/application/views/scripts/playlist/set-cue.phtml index eb35d17fc..4b44e51ac 100644 --- a/airtime_mvc/application/views/scripts/playlist/set-cue.phtml +++ b/airtime_mvc/application/views/scripts/playlist/set-cue.phtml @@ -1,11 +1,11 @@ <dl id="spl_cue_editor" class="inline-list"> <dt>Cue In:</dt> - <dd id="spl_cue_in_<?php echo $this->pos; ?>" class="spl_cue_in"> + <dd id="spl_cue_in_<?php echo $this->id; ?>" class="spl_cue_in"> <span contenteditable="true" class="spl_text_input"><?php echo $this->cueIn; ?></span> </dd> <dd class="edit-error"></dd> <dt>Cue Out:</dt> - <dd id="spl_cue_out_<?php echo $this->pos; ?>" class="spl_cue_out"> + <dd id="spl_cue_out_<?php echo $this->id; ?>" class="spl_cue_out"> <span contenteditable="true" class="spl_text_input"><?php echo $this->cueOut; ?></span> </dd> <dd class="edit-error"></dd> diff --git a/airtime_mvc/application/views/scripts/playlist/update.phtml b/airtime_mvc/application/views/scripts/playlist/update.phtml index 464f6a58a..ff45eb417 100644 --- a/airtime_mvc/application/views/scripts/playlist/update.phtml +++ b/airtime_mvc/application/views/scripts/playlist/update.phtml @@ -30,7 +30,7 @@ if (count($items)) : ?> <div id="cues_<?php echo $i ?>" class="cue-edit clearfix" style="display: none"> <?php echo $this->partial('playlist/set-cue.phtml', array( - 'pos' => $i, + 'id' => $item["id"], 'cueIn' => $item['cuein'], 'cueOut' => $item['cueout'], 'origLength' => $item["CcFiles"]['length'])); ?> diff --git a/airtime_mvc/public/css/contextmenu/images/cut.png b/airtime_mvc/public/css/contextmenu/images/cut.png new file mode 100755 index 000000000..f215d6f6b Binary files /dev/null and b/airtime_mvc/public/css/contextmenu/images/cut.png differ diff --git a/airtime_mvc/public/css/contextmenu/images/door.png b/airtime_mvc/public/css/contextmenu/images/door.png new file mode 100755 index 000000000..369fc46ed Binary files /dev/null and b/airtime_mvc/public/css/contextmenu/images/door.png differ diff --git a/airtime_mvc/public/css/contextmenu/images/page_white_copy.png b/airtime_mvc/public/css/contextmenu/images/page_white_copy.png new file mode 100755 index 000000000..a9f31a278 Binary files /dev/null and b/airtime_mvc/public/css/contextmenu/images/page_white_copy.png differ diff --git a/airtime_mvc/public/css/contextmenu/images/page_white_delete.png b/airtime_mvc/public/css/contextmenu/images/page_white_delete.png new file mode 100755 index 000000000..af1ecaf29 Binary files /dev/null and b/airtime_mvc/public/css/contextmenu/images/page_white_delete.png differ diff --git a/airtime_mvc/public/css/contextmenu/images/page_white_edit.png b/airtime_mvc/public/css/contextmenu/images/page_white_edit.png new file mode 100755 index 000000000..b93e77600 Binary files /dev/null and b/airtime_mvc/public/css/contextmenu/images/page_white_edit.png differ diff --git a/airtime_mvc/public/css/contextmenu/images/page_white_paste.png b/airtime_mvc/public/css/contextmenu/images/page_white_paste.png new file mode 100755 index 000000000..5b2cbb3fd Binary files /dev/null and b/airtime_mvc/public/css/contextmenu/images/page_white_paste.png differ diff --git a/airtime_mvc/public/css/contextmenu/jquery.contextMenu.css b/airtime_mvc/public/css/contextmenu/jquery.contextMenu.css new file mode 100755 index 000000000..fc8d709f3 --- /dev/null +++ b/airtime_mvc/public/css/contextmenu/jquery.contextMenu.css @@ -0,0 +1,134 @@ +/* + * jQuery contextMenu - Plugin for simple contextMenu handling + * + * Version: 1.5.2 + * + * Authors: Rodney Rehm, Addy Osmani (patches for FF) + * Web: http://medialize.github.com/jQuery-contextMenu/ + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * GPL v3 http://opensource.org/licenses/GPL-3.0 + * + */ + +.context-menu-list { + margin:0; + padding:0; + + min-width: 120px; + max-width: 250px; + display: inline-block; + position: absolute; + list-style-type: none; + + border: 1px solid #DDD; + background: #EEE; + + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + -ms-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + -o-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 11px; +} + +.context-menu-item { + padding: 2px 2px 2px 24px; + background-color: #EEE; + position: relative; + -moz-user-select: -moz-none; +} + +.context-menu-separator { + padding-bottom:0; + border-bottom: 1px solid #DDD; +} + +.context-menu-item > label { + -moz-user-select: text; +} + +.context-menu-item.hover { + cursor: pointer; + background-color: #39F; +} + +.context-menu-item.disabled { + color: #666; +} + +.context-menu-input.hover, +.context-menu-item.disabled.hover { + cursor: default; + background-color: #EEE; +} + +.context-menu-submenu:after { + content: ">"; + color: #666; + position: absolute; + top: 0; + right: 3px; + z-index: 1; +} + +/* icons + #protip: + In case you want to use sprites for icons (which I would suggest you do) have a look at + http://css-tricks.com/13224-pseudo-spriting/ to get an idea of how to implement + .context-menu-item.icon:before {} + */ +.context-menu-item.icon { min-height: 18px; background-repeat: no-repeat; background-position: 4px 2px; } +.context-menu-item.icon-edit { background-image: url(images/page_white_edit.png); } +.context-menu-item.icon-cut { background-image: url(images/cut.png); } +.context-menu-item.icon-copy { background-image: url(images/page_white_copy.png); } +.context-menu-item.icon-paste { background-image: url(images/page_white_paste.png); } +.context-menu-item.icon-delete { background-image: url(images/page_white_delete.png); } +.context-menu-item.icon-quit { background-image: url(images/door.png); } + +/* vertically align inside labels */ +.context-menu-input > label > * { vertical-align: top; } + +/* position checkboxes and radios as icons */ +.context-menu-input > label > input[type="checkbox"], +.context-menu-input > label > input[type="radio"] { + margin-left: -17px; +} +.context-menu-input > label > span { + margin-left: 5px; +} + +.context-menu-input > label, +.context-menu-input > label > input[type="text"], +.context-menu-input > label > textarea, +.context-menu-input > label > select { + display: block; + width: 100%; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; +} + +.context-menu-input > label > textarea { + height: 100px; +} +.context-menu-item > .context-menu-list { + display: none; + /* re-positioned by js */ + right: -5px; + top: 5px; +} + +.context-menu-item.hover > .context-menu-list { + display: block; +} + +.context-menu-accesskey { + text-decoration: underline; +} diff --git a/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js b/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js index 6ddcbb19d..a2aed00bc 100644 --- a/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js +++ b/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js @@ -27,7 +27,7 @@ function fnLibraryTableDrawCallback() { }, */ cursor: 'pointer', - connectToSortable: '#side_playlist' + connectToSortable: '#spl_sortable' }); } diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js index 9a63325a4..a7469073d 100644 --- a/airtime_mvc/public/js/airtime/library/spl.js +++ b/airtime_mvc/public/js/airtime/library/spl.js @@ -1,8 +1,9 @@ //-------------------------------------------------------------------------------------------------------------------------------- -//Side Playlist Functions +// Playlist Functions //-------------------------------------------------------------------------------------------------------------------------------- var AIRTIME = (function(AIRTIME){ + AIRTIME.playlist = {}; var mod = AIRTIME.playlist; @@ -20,6 +21,228 @@ var AIRTIME = (function(AIRTIME){ $(el).removeClass("ui-state-active"); } + + function isTimeValid(time) { + var regExpr = new RegExp("^\\d{2}[:]\\d{2}[:]\\d{2}([.]\\d{1,6})?$"); + + if (!regExpr.test(time)) { + return false; + } + + return true; + } + + function showError(el, error) { + $(el).parent().next() + .empty() + .append(error) + .show(); + } + + function hideError(el) { + $(el).parent().next() + .empty() + .hide(); + } + + function changeCueIn(event) { + event.stopPropagation(); + + var id, url, cueIn, li, unqid; + + span = $(this); + id = span.parent().attr("id").split("_").pop(); + url = "/Playlist/set-cue"; + cueIn = $.trim(span.text()); + li = span.parent().parent().parent().parent(); + unqid = li.attr("unqid"); + + if(!isTimeValid(cueIn)){ + showError(span, "please put in a time '00:00:00 (.000000)'"); + return; + } + + $.post(url, {format: "json", cueIn: cueIn, id: id, type: event.type}, function(json){ + + if(json.response !== undefined && json.response.error) { + showError(span, json.response.error); + return; + } + + setPlaylistContent(json); + + li = $('#side_playlist li[unqid='+unqid+']'); + li.find(".cue-edit").toggle(); + highlightActive(li); + highlightActive(li.find('.spl_cue')); + }); + } + + function changeCueOut(event) { + event.stopPropagation(); + + var id, url, cueOut, li, unqid; + + span = $(this); + id = span.parent().attr("id").split("_").pop(); + url = "/Playlist/set-cue"; + cueOut = $.trim(span.text()); + li = span.parent().parent().parent().parent(); + unqid = li.attr("unqid"); + + if(!isTimeValid(cueOut)){ + showError(span, "please put in a time '00:00:00 (.000000)'"); + return; + } + + $.post(url, {format: "json", cueOut: cueOut, id: id}, function(json){ + + if(json.response !== undefined && json.response.error) { + showError(span, json.response.error); + return; + } + + setPlaylistContent(json); + + li = $('#side_playlist li[unqid='+unqid+']'); + li.find(".cue-edit").toggle(); + highlightActive(li); + highlightActive(li.find('.spl_cue')); + }); + } + + function changeFadeIn(event) { + event.stopPropagation(); + + var pos, url, fadeIn, li, unqid; + + span = $(this); + pos = span.parent().attr("id").split("_").pop(); + url = "/Playlist/set-fade"; + fadeIn = $.trim(span.text()); + li = span.parent().parent().parent().parent(); + unqid = li.attr("unqid"); + + if(!isTimeValid(fadeIn)){ + showError(span, "please put in a time '00:00:00 (.000000)'"); + return; + } + + $.post(url, {format: "json", fadeIn: fadeIn, pos: pos}, function(json){ + + if(json.response !== undefined && json.response.error) { + showError(span, json.response.error); + return; + } + + setPlaylistContent(json); + + li = $('#side_playlist li[unqid='+unqid+']'); + li.find('.crossfade').toggle(); + highlightActive(li.find('.spl_fade_control')); + }); + } + + function changeFadeOut(event) { + event.stopPropagation(); + + var pos, url, fadeOut, li, unqid; + + span = $(this); + pos = span.parent().attr("id").split("_").pop(); + url = "/Playlist/set-fade"; + fadeOut = $.trim(span.text()); + li = span.parent().parent().parent().parent(); + unqid = li.attr("unqid"); + + if(!isTimeValid(fadeOut)){ + showError(span, "please put in a time '00:00:00 (.000000)'"); + return; + } + + $.post(url, {format: "json", fadeOut: fadeOut, pos: pos}, function(json){ + if(json.response !== undefined && json.response.error) { + showError(span, json.response.error); + return; + } + + setPlaylistContent(json); + + li = $('#side_playlist li[unqid='+unqid+']'); + li.find('.crossfade').toggle(); + highlightActive(li.find('.spl_fade_control')); + }); + } + + function submitOnEnter(event) { + //enter was pressed + if(event.keyCode === 13) { + event.preventDefault(); + $(this).blur(); + } + } + + function openFadeEditor(event) { + var pos, url, li; + + event.stopPropagation(); + + li = $(this).parent().parent(); + li.find(".crossfade").toggle(); + + if($(this).hasClass("ui-state-active")) { + unHighlightActive(this); + } + else { + highlightActive(this); + } + } + + function openCueEditor(event) { + var pos, url, li, icon; + + event.stopPropagation(); + + icon = $(this); + li = $(this).parent().parent().parent(); + li.find(".cue-edit").toggle(); + + if (li.hasClass("ui-state-active")) { + unHighlightActive(li); + unHighlightActive(icon); + } + else { + highlightActive(li); + highlightActive(icon); + } + } + + function editName() { + var nameElement = $(this); + var playlistName = nameElement.text(); + + $("#playlist_name_input") + .removeClass('element_hidden') + .val(playlistName) + .keydown(function(event){ + if(event.keyCode === 13) { + event.preventDefault(); + var input = $(this); + var url; + url = '/Playlist/set-playlist-name'; + + $.post(url, {format: "json", name: input.val()}, function(json){ + if(json.playlist_error == true){ + alertPlaylistErrorAndReload(); + } + input.addClass('element_hidden'); + nameElement.text(json.playlistName); + redrawLib(); + }); + } + }) + .focus(); + } function redrawLib() { var dt; @@ -58,6 +281,264 @@ var AIRTIME = (function(AIRTIME){ $("#side_playlist") .empty() .append(json.html); + + mod.setUpPlaylist(); + } + + function setPlaylistButtonEvents(el) { + + $(el).delegate("#spl_new", + {"click": AIRTIME.playlist.fnNew}); + + $(el).delegate("#spl_delete", + {"click": AIRTIME.playlist.fnDelete}); + } + + //sets events dynamically for playlist entries (each row in the playlist) + function setPlaylistEntryEvents(el) { + + $(el).delegate("#spl_sortable .ui-icon-closethick", + {"click": function(ev){ + var id; + id = parseInt($(this).attr("id").split("_").pop(), 10); + AIRTIME.playlist.fnDeleteItems([id]); + }}); + + $(el).delegate(".spl_fade_control", + {"click": openFadeEditor}); + + $(el).delegate(".spl_cue", + {"click": openCueEditor}); + + } + + //sets events dynamically for the cue editor. + function setCueEvents(el) { + + $(el).delegate(".spl_cue_in span", + {"focusout": changeCueIn, + "keydown": submitOnEnter}); + + $(el).delegate(".spl_cue_out span", + {"focusout": changeCueOut, + "keydown": submitOnEnter}); + } + + //sets events dynamically for the fade editor. + function setFadeEvents(el) { + + $(el).delegate(".spl_fade_in span", + {"focusout": changeFadeIn, + "keydown": submitOnEnter}); + + $(el).delegate(".spl_fade_out span", + {"focusout": changeFadeOut, + "keydown": submitOnEnter}); + } + + mod.setUpPlaylist = function() { + + var playlist = $("#side_playlist"), + sortableConf; + + playlist.find("#spl_crossfade").on("click", function(){ + + if ($(this).hasClass("ui-state-active")) { + $(this).removeClass("ui-state-active"); + playlist.find("#crossfade_main").hide(); + } + else { + $(this).addClass("ui-state-active"); + + var url = '/Playlist/set-playlist-fades'; + + $.get(url, {format: "json"}, function(json){ + if(json.playlist_error == true){ + alertPlaylistErrorAndReload(); + } + playlist.find("#spl_fade_in_main").find("span") + .empty() + .append(json.fadeIn); + playlist.find("#spl_fade_out_main").find("span") + .empty() + .append(json.fadeOut); + + playlist.find("#crossfade_main").show(); + }); + } + }); + + playlist.find("#playlist_name_display").on("click", editName); + + playlist.find("#fieldset-metadate_change > legend").on("click", function(){ + var descriptionElement = $(this).parent(); + + if(descriptionElement.hasClass("closed")) { + descriptionElement.removeClass("closed"); + } + else { + descriptionElement.addClass("closed"); + } + }); + + playlist.find("#description_save").on("click", function(){ + var textarea = playlist.find("#fieldset-metadate_change textarea"), + description = textarea.val(), + url; + + url = '/Playlist/set-playlist-description'; + + $.post(url, {format: "json", description: description}, function(json){ + if(json.playlist_error == true){ + alertPlaylistErrorAndReload(); + } + else{ + textarea.val(json.playlistDescription); + } + + playlist.find("#fieldset-metadate_change").addClass("closed"); + redrawLib(); + }); + }); + + playlist.find("#description_cancel").on("click", function(){ + var textarea = playlist.find("#fieldset-metadate_change textarea"), + url; + + url = '/Playlist/set-playlist-description'; + + $.post(url, {format: "json"}, function(json){ + if(json.playlist_error == true){ + alertPlaylistErrorAndReload(); + } + else{ + textarea.val(json.playlistDescription); + } + + playlist.find("#fieldset-metadate_change").addClass("closed"); + }); + }); + + playlist.find("#spl_fade_in_main span:first").on("blur", function(event){ + event.stopPropagation(); + + var url, fadeIn, span; + + span = $(this); + url = "/Playlist/set-playlist-fades"; + fadeIn = $.trim(span.text()); + + if (!isTimeValid(fadeIn)){ + showError(span, "please put in a time '00:00:00 (.000000)'"); + return; + } + + $.post(url, {format: "json", fadeIn: fadeIn}, function(json){ + if(json.playlist_error == true){ + alertPlaylistErrorAndReload(); + } + if(json.response.error) { + return; + } + + hideError(span); + }); + }); + + playlist.find("#spl_fade_out_main span:first").on("blur", function(event){ + event.stopPropagation(); + + var url, fadeIn, span; + + span = $(this); + url = "/Playlist/set-playlist-fades"; + fadeOut = $.trim(span.text()); + + if(!isTimeValid(fadeOut)){ + showError(span, "please put in a time '00:00:00 (.000000)'"); + return; + } + + $.post(url, {format: "json", fadeOut: fadeOut}, function(json){ + if(json.playlist_error == true){ + alertPlaylistErrorAndReload(); + } + if(json.response.error) { + return; + } + + hideError(span); + }); + }); + + playlist.find("#spl_fade_in_main span:first, #spl_fade_out_main span:first") + .on("keydown", submitOnEnter); + + playlist.find("#crossfade_main > .ui-icon-closethick").on("click", function(){ + playlist.find("#spl_crossfade").removeClass("ui-state-active"); + playlist.find("#crossfade_main").hide(); + }); + + setPlaylistButtonEvents(playlist); + setPlaylistEntryEvents(playlist); + setCueEvents(playlist); + setFadeEvents(playlist); + + sortableConf = (function(){ + var origRow, + fnReceive, + fnUpdate; + + fnReceive = function(event, ui) { + origRow = ui.item; + }; + + fnUpdate = function(event, ui) { + var prev, + aItem = [], + iAfter, + sAddType; + + prev = ui.item.prev(); + if (prev.hasClass("spl_empty") || prev.length === 0) { + iAfter = undefined; + sAddType = 'before'; + } + else { + iAfter = parseInt(prev.attr("id").split("_").pop(), 10); + sAddType = 'after'; + } + + //item was dragged in from library datatable + if (origRow !== undefined) { + aItem.push(origRow.data("aData").id); + origRow = undefined; + AIRTIME.playlist.fnAddItems(aItem, iAfter, sAddType); + } + //item was reordered. + else { + aItem.push(parseInt(ui.item.attr("id").split("_").pop(), 10)); + AIRTIME.playlist.fnMoveItems(aItem, iAfter); + } + }; + + return { + items: 'li', + placeholder: "placeholder lib-placeholder ui-state-highlight", + forcePlaceholderSize: true, + handle: 'div.list-item-container', + start: function(event, ui) { + ui.placeholder.html("PLACE HOLDER") + .width("99.5%") + .height(56) + .append('<div style="clear:both;"/>'); + }, + receive: fnReceive, + update: fnUpdate + }; + }()); + + playlist.find("#spl_sortable").sortable(sortableConf); } mod.fnNew = function() { @@ -72,6 +553,10 @@ var AIRTIME = (function(AIRTIME){ }); } + mod.fnEdit = function() { + + } + mod.fnDelete = function() { var url, id, lastMod; @@ -88,10 +573,10 @@ var AIRTIME = (function(AIRTIME){ }); } - mod.fnAddItems = function(aItem, iAfter) { + mod.fnAddItems = function(aItems, iAfter, sAddType) { $.post("/playlist/add-items", - {format: "json", "ids": aItem, "afterItem": iAfter}, + {format: "json", "ids": aItems, "afterItem": iAfter, "type": sAddType}, function(json){ setPlaylistContent(json); }); @@ -120,489 +605,7 @@ var AIRTIME = (function(AIRTIME){ }(AIRTIME || {})); -function isTimeValid(time) { - var regExpr = new RegExp("^\\d{2}[:]\\d{2}[:]\\d{2}([.]\\d{1,6})?$"); - - if (!regExpr.test(time)) { - return false; - } - - return true; -} - -function showError(el, error) { - $(el).parent().next() - .empty() - .append(error) - .show(); -} - -function hideError(el) { - $(el).parent().next() - .empty() - .hide(); -} - -function changeCueIn(event) { - event.stopPropagation(); - - var pos, url, cueIn, li, unqid; - - span = $(this); - pos = span.parent().attr("id").split("_").pop(); - url = "/Playlist/set-cue"; - cueIn = $.trim(span.text()); - li = span.parent().parent().parent().parent(); - unqid = li.attr("unqid"); - - if(!isTimeValid(cueIn)){ - showError(span, "please put in a time '00:00:00 (.000000)'"); - return; - } - - $.post(url, {format: "json", cueIn: cueIn, pos: pos, type: event.type}, function(json){ - - if(json.response !== undefined && json.response.error) { - showError(span, json.response.error); - return; - } - - setSPLContent(json); - - li = $('#side_playlist li[unqid='+unqid+']'); - li.find(".cue-edit").toggle(); - highlightActive(li); - highlightActive(li.find('.spl_cue')); - }); -} - -function changeCueOut(event) { - event.stopPropagation(); - - var pos, url, cueOut, li, unqid; - - span = $(this); - pos = span.parent().attr("id").split("_").pop(); - url = "/Playlist/set-cue"; - cueOut = $.trim(span.text()); - li = span.parent().parent().parent().parent(); - unqid = li.attr("unqid"); - - if(!isTimeValid(cueOut)){ - showError(span, "please put in a time '00:00:00 (.000000)'"); - return; - } - - $.post(url, {format: "json", cueOut: cueOut, pos: pos}, function(json){ - - if(json.response !== undefined && json.response.error) { - showError(span, json.response.error); - return; - } - - setSPLContent(json); - - li = $('#side_playlist li[unqid='+unqid+']'); - li.find(".cue-edit").toggle(); - highlightActive(li); - highlightActive(li.find('.spl_cue')); - }); -} - -function changeFadeIn(event) { - event.stopPropagation(); - - var pos, url, fadeIn, li, unqid; - - span = $(this); - pos = span.parent().attr("id").split("_").pop(); - url = "/Playlist/set-fade"; - fadeIn = $.trim(span.text()); - li = span.parent().parent().parent().parent(); - unqid = li.attr("unqid"); - - if(!isTimeValid(fadeIn)){ - showError(span, "please put in a time '00:00:00 (.000000)'"); - return; - } - - $.post(url, {format: "json", fadeIn: fadeIn, pos: pos}, function(json){ - - if(json.response !== undefined && json.response.error) { - showError(span, json.response.error); - return; - } - - setSPLContent(json); - - li = $('#side_playlist li[unqid='+unqid+']'); - li.find('.crossfade').toggle(); - highlightActive(li.find('.spl_fade_control')); - }); -} - -function changeFadeOut(event) { - event.stopPropagation(); - - var pos, url, fadeOut, li, unqid; - - span = $(this); - pos = span.parent().attr("id").split("_").pop(); - url = "/Playlist/set-fade"; - fadeOut = $.trim(span.text()); - li = span.parent().parent().parent().parent(); - unqid = li.attr("unqid"); - - if(!isTimeValid(fadeOut)){ - showError(span, "please put in a time '00:00:00 (.000000)'"); - return; - } - - $.post(url, {format: "json", fadeOut: fadeOut, pos: pos}, function(json){ - if(json.response !== undefined && json.response.error) { - showError(span, json.response.error); - return; - } - - setSPLContent(json); - - li = $('#side_playlist li[unqid='+unqid+']'); - li.find('.crossfade').toggle(); - highlightActive(li.find('.spl_fade_control')); - }); -} - -function submitOnEnter(event) { - //enter was pressed - if(event.keyCode === 13) { - event.preventDefault(); - $(this).blur(); - } -} - -function openFadeEditor(event) { - var pos, url, li; - - event.stopPropagation(); - - li = $(this).parent().parent(); - li.find(".crossfade").toggle(); - - if($(this).hasClass("ui-state-active")) { - unHighlightActive(this); - } - else { - highlightActive(this); - } -} - -function openCueEditor(event) { - var pos, url, li, icon; - - event.stopPropagation(); - - icon = $(this); - li = $(this).parent().parent().parent(); - li.find(".cue-edit").toggle(); - - if (li.hasClass("ui-state-active")) { - unHighlightActive(li); - unHighlightActive(icon); - } - else { - highlightActive(li); - highlightActive(icon); - } -} - -function editName() { - var nameElement = $(this); - var playlistName = nameElement.text(); - - $("#playlist_name_input") - .removeClass('element_hidden') - .val(playlistName) - .keydown(function(event){ - if(event.keyCode === 13) { - event.preventDefault(); - var input = $(this); - var url; - url = '/Playlist/set-playlist-name'; - - $.post(url, {format: "json", name: input.val()}, function(json){ - if(json.playlist_error == true){ - alertPlaylistErrorAndReload(); - } - input.addClass('element_hidden'); - nameElement.text(json.playlistName); - redrawDataTablePage(); - }); - } - }) - .focus(); -} - - - $(document).ready(function() { - var playlist = $("#side_playlist"), - sortableConf; - function setUpSPL() { - - /* - $("#spl_crossfade").on("click", function(){ - - if ($(this).hasClass("ui-state-active")) { - $(this).removeClass("ui-state-active"); - $("#crossfade_main").hide(); - } - else { - $(this).addClass("ui-state-active"); - - var url = '/Playlist/set-playlist-fades'; - - $.get(url, {format: "json"}, function(json){ - if(json.playlist_error == true){ - alertPlaylistErrorAndReload(); - } - $("#spl_fade_in_main").find("span") - .empty() - .append(json.fadeIn); - $("#spl_fade_out_main").find("span") - .empty() - .append(json.fadeOut); - - $("#crossfade_main").show(); - }); - } - }); - - $("#playlist_name_display").on("click", editName); - - $("#fieldset-metadate_change > legend").on("click", function(){ - var descriptionElement = $(this).parent(); - - if(descriptionElement.hasClass("closed")) { - descriptionElement.removeClass("closed"); - } - else { - descriptionElement.addClass("closed"); - } - }); - - $("#description_save").on("click", function(){ - var textarea = $("#fieldset-metadate_change textarea"), - description = textarea.val(), - url; - - url = '/Playlist/set-playlist-description'; - - $.post(url, {format: "json", description: description}, function(json){ - if(json.playlist_error == true){ - alertPlaylistErrorAndReload(); - } - else{ - textarea.val(json.playlistDescription); - } - - $("#fieldset-metadate_change").addClass("closed"); - - // update the "Last Modified" time for this playlist - redrawDataTablePage(); - }); - }); - - $("#description_cancel").on("click", function(){ - var textarea = $("#fieldset-metadate_change textarea"), - url; - - url = '/Playlist/set-playlist-description'; - - $.post(url, {format: "json"}, function(json){ - if(json.playlist_error == true){ - alertPlaylistErrorAndReload(); - } - else{ - textarea.val(json.playlistDescription); - } - - $("#fieldset-metadate_change").addClass("closed"); - }); - }); - - $("#spl_fade_in_main span:first").on("blur", function(event){ - event.stopPropagation(); - - var url, fadeIn, span; - - span = $(this); - url = "/Playlist/set-playlist-fades"; - fadeIn = $.trim(span.text()); - - if (!isTimeValid(fadeIn)){ - showError(span, "please put in a time '00:00:00 (.000000)'"); - return; - } - - $.post(url, {format: "json", fadeIn: fadeIn}, function(json){ - if(json.playlist_error == true){ - alertPlaylistErrorAndReload(); - } - if(json.response.error) { - return; - } - - hideError(span); - }); - }); - - $("#spl_fade_out_main span:first").on("blur", function(event){ - event.stopPropagation(); - - var url, fadeIn, span; - - span = $(this); - url = "/Playlist/set-playlist-fades"; - fadeOut = $.trim(span.text()); - - if(!isTimeValid(fadeOut)){ - showError(span, "please put in a time '00:00:00 (.000000)'"); - return; - } - - $.post(url, {format: "json", fadeOut: fadeOut}, function(json){ - if(json.playlist_error == true){ - alertPlaylistErrorAndReload(); - } - if(json.response.error) { - return; - } - - hideError(span); - }); - }); - - $("#spl_fade_in_main span:first, #spl_fade_out_main span:first") - .on("keydown", submitOnEnter); - - $("#crossfade_main > .ui-icon-closethick").on("click", function(){ - $("#spl_crossfade").removeClass("ui-state-active"); - $("#crossfade_main").hide(); - }); - */ - - } - - function setPlaylistButtonEvents(el) { - - $(el).delegate("#spl_new", - {"click": AIRTIME.playlist.fnNew}); - - $(el).delegate("#spl_delete", - {"click": AIRTIME.playlist.fnDelete}); - } - - //sets events dynamically for playlist entries (each row in the playlist) - function setPlaylistEntryEvents(el) { - - $(el).delegate("#spl_sortable .ui-icon-closethick", - {"click": function(ev){ - var id; - id = parseInt($(this).attr("id").split("_").pop(), 10); - AIRTIME.playlist.fnDeleteItems([id]); - }}); - - /* - $(el).delegate(".spl_fade_control", - {"click": openFadeEditor}); - - $(el).delegate(".spl_cue", - {"click": openCueEditor}); - */ - } - - //sets events dynamically for the cue editor. - function setCueEvents(el) { - - $(el).delegate(".spl_cue_in span", - {"focusout": changeCueIn, - "keydown": submitOnEnter}); - - $(el).delegate(".spl_cue_out span", - {"focusout": changeCueOut, - "keydown": submitOnEnter}); - } - - //sets events dynamically for the fade editor. - function setFadeEvents(el) { - - $(el).delegate(".spl_fade_in span", - {"focusout": changeFadeIn, - "keydown": submitOnEnter}); - - $(el).delegate(".spl_fade_out span", - {"focusout": changeFadeOut, - "keydown": submitOnEnter}); - } - - - - setPlaylistButtonEvents(playlist); - setPlaylistEntryEvents(playlist); - //setCueEvents(playlist); - //setFadeEvents(playlist); - - sortableConf = (function(){ - var origRow, - fnReceive, - fnUpdate; - - fnReceive = function(event, ui) { - origRow = ui.item; - }; - - fnUpdate = function(event, ui) { - var prev, - aItem = [], - iAfter; - - prev = ui.item.prev(); - if (prev.hasClass("spl_empty") || prev.length === 0) { - iAfter = undefined; - } - else { - iAfter = parseInt(prev.attr("id").split("_").pop(), 10); - } - - //item was dragged in from library datatable - if (origRow !== undefined) { - aItem.push(origRow.data("aData").id); - origRow = undefined; - AIRTIME.playlist.fnAddItems(aItem, iAfter); - } - //item was reordered. - else { - aItem.push(parseInt(ui.item.attr("id").split("_").pop(), 10)); - AIRTIME.playlist.fnMoveItems(aItem, iAfter); - } - }; - - return { - items: 'li', - placeholder: "placeholder lib-placeholder ui-state-highlight", - forcePlaceholderSize: true, - handle: 'div.list-item-container', - start: function(event, ui) { - ui.placeholder.html("PLACE HOLDER") - .width("99.5%") - .height(56) - .append('<div style="clear:both;"/>'); - }, - receive: fnReceive, - update: fnUpdate - }; - }()); - - playlist.sortable(sortableConf); + AIRTIME.playlist.setUpPlaylist(); }); diff --git a/airtime_mvc/public/js/contextmenu/jquery.contextMenu.js b/airtime_mvc/public/js/contextmenu/jquery.contextMenu.js new file mode 100755 index 000000000..92153b0ca --- /dev/null +++ b/airtime_mvc/public/js/contextmenu/jquery.contextMenu.js @@ -0,0 +1,1449 @@ +/* + * jQuery contextMenu - Plugin for simple contextMenu handling + * + * Version: 1.5.8 + * + * Authors: Rodney Rehm, Addy Osmani (patches for FF) + * Web: http://medialize.github.com/jQuery-contextMenu/ + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * GPL v3 http://opensource.org/licenses/GPL-3.0 + * + */ + +(function($, undefined){ + + // TODO: - + // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio + // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative + +// determine html5 compatibility +$.support.htmlMenuitem = ('HTMLMenuItemElement' in window); +$.support.htmlCommand = ('HTMLCommandElement' in window); + +var // currently active contextMenu trigger + $currentTrigger = null, + // is contextMenu initialized with at least one menu? + initialized = false, + // flag stating to ignore the contextmenu event + ignoreThisClick = false, + // window handle + $win = $(window), + // number of registered menus + counter = 0, + // mapping selector to namespace + namespaces = {}, + // mapping namespace to options + menus = {}, + // custom command type handlers + types = {}, + // default values + defaults = { + // selector of contextMenu trigger + selector: null, + // where to append the menu to + appendTo: null, + // method to trigger context menu ["right", "left", "hover"] + trigger: "right", + // hide menu when mouse leaves trigger / menu elements + autoHide: false, + // ignore right click triggers for left, hover or custom activation + ignoreRightClick: false, + // ms to wait before showing a hover-triggered context menu + delay: 200, + // determine position to show menu at + determinePosition: function($menu) { + // position to the lower middle of the trigger element + if ($.ui && $.ui.position) { + // .position() is provided as a jQuery UI utility + // (...and it won't work on hidden elements) + $menu.css('display', 'block').position({ + my: "center top", + at: "center bottom", + of: this, + offset: "0 5", + collision: "fit" + }).css('display', 'none'); + } else { + // determine contextMenu position + var offset = this.offset(); + offset.top += this.outerHeight(); + offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2; + $menu.css(offset); + } + }, + // position menu + position: function(opt, x, y) { + var $this = this, + offset; + // determine contextMenu position + if (!x && !y) { + opt.determinePosition.call(this, opt.$menu); + return; + } else if (x === "maintain" && y === "maintain") { + // x and y must not be changed (after re-show on command click) + offset = opt.$menu.position(); + } else { + // x and y are given (by mouse event) + var triggerIsFixed = opt.$trigger.parents().andSelf() + .filter(function() { + return $(this).css('position') == "fixed"; + }).length; + + if (triggerIsFixed) { + y -= $win.scrollTop(); + x -= $win.scrollLeft(); + } + offset = {top: y, left: x}; + } + + // correct offset if viewport demands it + var bottom = $win.scrollTop() + $win.height(), + right = $win.scrollLeft() + $win.width(), + height = opt.$menu.height(), + width = opt.$menu.width(); + + if (offset.top + height > bottom) { + offset.top -= height; + } + + if (offset.left + width > right) { + offset.left -= width; + } + + opt.$menu.css(offset); + }, + // position the sub-menu + positionSubmenu: function($menu) { + if ($.ui && $.ui.position) { + // .position() is provided as a jQuery UI utility + // (...and it won't work on hidden elements) + $menu.css('display', 'block').position({ + my: "left top", + at: "right top", + of: this, + collision: "fit" + }).css('display', ''); + } else { + // determine contextMenu position + var offset = this.offset(); + offset.top += 0; + offset.left += this.outerWidth(); + $menu.css(offset); + } + }, + // offset to add to zIndex + zIndex: 1, + // show hide animation settings + animation: { + duration: 50, + show: 'slideDown', + hide: 'slideUp' + }, + // events + events: { + show: $.noop, + hide: $.noop + }, + // default callback + callback: null, + // list of contextMenu items + items: {} + }, + // mouse position for hover activation + hoveract = { + timer: null, + pageX: null, + pageY: null + }, + // determine zIndex + zindex = function($t) { + var zin = 0, + $tt = $t; + + while (true) { + zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0); + $tt = $tt.parent(); + if (!$tt || !$tt.length || $tt.prop('nodeName').toLowerCase() == 'body') { + break; + } + } + + return zin; + }, + // event handlers + handle = { + // abort anything + abortevent: function(e){ + e.preventDefault(); + e.stopImmediatePropagation(); + }, + + // contextmenu show dispatcher + contextmenu: function(e) { + var $this = $(this); + // disable actual context-menu + e.preventDefault(); + e.stopImmediatePropagation(); + + // ignore right click trigger + if (ignoreThisClick) { + ignoreThisClick = false; + return; + } + + if (!$this.hasClass('context-menu-disabled')) { + // theoretically need to fire a show event at <menu> + // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus + // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this }); + // e.data.$menu.trigger(evt); + + $currentTrigger = $this; + if (e.data.build) { + // dynamically build menu on invocation + $.extend(true, e.data, defaults, e.data.build($currentTrigger, e) || {}); + op.create(e.data); + } + // show menu + op.show.call($this, e.data, e.pageX, e.pageY); + } + }, + // contextMenu left-click trigger + click: function(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + $(this).trigger(jQuery.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY })); + }, + // contextMenu right-click trigger + mousedown: function(e) { + // register mouse down + var $this = $(this); + + // hide any previous menus + if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) { + $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide'); + } + + // activate on right click + if (e.button == 2) { + $currentTrigger = $this.data('contextMenuActive', true); + } + }, + // contextMenu right-click trigger + mouseup: function(e) { + // show menu + var $this = $(this); + if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) { + e.preventDefault(); + e.stopImmediatePropagation(); + $currentTrigger = $this; + $this.trigger(jQuery.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY })); + } + + $this.removeData('contextMenuActive'); + }, + // contextMenu hover trigger + mouseenter: function(e) { + var $this = $(this), + $related = $(e.relatedTarget), + $document = $(document); + + // abort if we're coming from a menu + if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) { + return; + } + + // abort if a menu is shown + if ($currentTrigger && $currentTrigger.length) { + return; + } + + hoveract.pageX = e.pageX; + hoveract.pageY = e.pageY; + hoveract.data = e.data; + $document.on('mousemove.contextMenuShow', handle.mousemove); + hoveract.timer = setTimeout(function() { + hoveract.timer = null; + $document.off('mousemove.contextMenuShow'); + $currentTrigger = $this; + $this.trigger(jQuery.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY })); + }, e.data.delay ); + }, + // contextMenu hover trigger + mousemove: function(e) { + hoveract.pageX = e.pageX; + hoveract.pageY = e.pageY; + }, + // contextMenu hover trigger + mouseleave: function(e) { + // abort if we're leaving for a menu + var $related = $(e.relatedTarget); + if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) { + return; + } + + try { + clearTimeout(hoveract.timer); + } catch(e) {} + + hoveract.timer = null; + }, + + // ignore right click trigger + ignoreRightClick: function(e) { + if (e.button == 2) { + ignoreThisClick = true; + } + }, + + // click on layer to hide contextMenu + layerClick: function(e) { + var $this = $(this), + root = $this.data('contextMenuRoot'); + + e.preventDefault(); + e.stopImmediatePropagation(); + $this.remove(); + root.$menu.trigger('contextmenu:hide'); + }, + // key handled :hover + keyStop: function(e, opt) { + if (!opt.isInput) { + e.preventDefault(); + } + + e.stopPropagation(); + }, + key: function(e) { + var opt = $currentTrigger.data('contextMenu') || {}, + $children = opt.$menu.children(), + $round; + + switch (e.keyCode) { + case 9: + case 38: // up + handle.keyStop(e, opt); + // if keyCode is [38 (up)] or [9 (tab) with shift] + if (opt.isInput) { + if (e.keyCode == 9 && e.shiftKey) { + e.preventDefault(); + opt.$selected && opt.$selected.find('input, textarea, select').blur(); + opt.$menu.trigger('prevcommand'); + return; + } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') { + // checkboxes don't capture this key + e.preventDefault(); + return; + } + } else if (e.keyCode != 9 || e.shiftKey) { + opt.$menu.trigger('prevcommand'); + return; + } + + case 9: // tab + case 40: // down + handle.keyStop(e, opt); + if (opt.isInput) { + if (e.keyCode == 9) { + e.preventDefault(); + opt.$selected && opt.$selected.find('input, textarea, select').blur(); + opt.$menu.trigger('nextcommand'); + return; + } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') { + // checkboxes don't capture this key + e.preventDefault(); + return; + } + } else { + opt.$menu.trigger('nextcommand'); + return; + } + break; + + case 37: // left + handle.keyStop(e, opt); + if (opt.isInput || !opt.$selected || !opt.$selected.length) { + break; + } + + if (!opt.$selected.parent().hasClass('context-menu-root')) { + var $parent = opt.$selected.parent().parent(); + opt.$selected.trigger('contextmenu:blur'); + opt.$selected = $parent; + return; + } + break; + + case 39: // right + handle.keyStop(e, opt); + if (opt.isInput || !opt.$selected || !opt.$selected.length) { + break; + } + + var itemdata = opt.$selected.data('contextMenu') || {}; + if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) { + opt.$selected = null; + itemdata.$selected = null; + itemdata.$menu.trigger('nextcommand'); + return; + } + break; + + case 35: // end + case 36: // home + if (opt.$selected && opt.$selected.find('input, textarea, select').length) { + return; + } else { + (opt.$selected && opt.$selected.parent() || opt.$menu) + .children(':not(.disabled, .not-selectable)')[e.keyCode == 36 ? 'first' : 'last']() + .trigger('contextmenu:focus'); + e.preventDefault(); + return; + } + break; + + case 13: // enter + handle.keyStop(e, opt); + if (opt.isInput) { + if (opt.$selected && !opt.$selected.is('textarea, select')) { + e.preventDefault(); + return; + } + break; + } + opt.$selected && opt.$selected.trigger('mouseup'); + return; + + case 32: // space + case 33: // page up + case 34: // page down + // prevent browser from scrolling down while menu is visible + handle.keyStop(e, opt); + return; + + case 27: // esc + handle.keyStop(e, opt); + opt.$menu.trigger('contextmenu:hide'); + return; + + default: // 0-9, a-z + var k = (String.fromCharCode(e.keyCode)).toUpperCase(); + if (opt.accesskeys[k]) { + // according to the specs accesskeys must be invoked immediately + opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu + ? 'contextmenu:focus' + : 'mouseup' + ); + return; + } + break; + } + // pass event to selected item, + // stop propagation to avoid endless recursion + e.stopPropagation(); + opt.$selected && opt.$selected.trigger(e); + }, + + // select previous possible command in menu + prevItem: function(e) { + e.stopPropagation(); + var opt = $(this).data('contextMenu') || {}; + + // obtain currently selected menu + if (opt.$selected) { + var $s = opt.$selected; + opt = opt.$selected.parent().data('contextMenu') || {}; + opt.$selected = $s; + } + + var $children = opt.$menu.children(), + $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(), + $round = $prev; + + // skip disabled + while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) { + if ($prev.prev().length) { + $prev = $prev.prev(); + } else { + $prev = $children.last(); + } + if ($prev.is($round)) { + // break endless loop + return; + } + } + + // leave current + if (opt.$selected) { + handle.itemMouseleave.call(opt.$selected.get(0), e); + } + + // activate next + handle.itemMouseenter.call($prev.get(0), e); + + // focus input + var $input = $prev.find('input, textarea, select'); + if ($input.length) { + $input.focus(); + } + }, + // select next possible command in menu + nextItem: function(e) { + e.stopPropagation(); + var opt = $(this).data('contextMenu') || {}; + + // obtain currently selected menu + if (opt.$selected) { + var $s = opt.$selected; + opt = opt.$selected.parent().data('contextMenu') || {}; + opt.$selected = $s; + } + + var $children = opt.$menu.children(), + $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(), + $round = $next; + + // skip disabled + while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) { + if ($next.next().length) { + $next = $next.next(); + } else { + $next = $children.first(); + } + if ($next.is($round)) { + // break endless loop + return; + } + } + + // leave current + if (opt.$selected) { + handle.itemMouseleave.call(opt.$selected.get(0), e); + } + + // activate next + handle.itemMouseenter.call($next.get(0), e); + + // focus input + var $input = $next.find('input, textarea, select'); + if ($input.length) { + $input.focus(); + } + }, + + // flag that we're inside an input so the key handler can act accordingly + focusInput: function(e) { + var $this = $(this).closest('.context-menu-item'), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + root.$selected = opt.$selected = $this; + root.isInput = opt.isInput = true; + }, + // flag that we're inside an input so the key handler can act accordingly + blurInput: function(e) { + var $this = $(this).closest('.context-menu-item'), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + root.isInput = opt.isInput = false; + }, + + // :hover on menu + menuMouseenter: function(e) { + var root = $(this).data().contextMenuRoot; + root.hovering = true; + }, + // :hover on menu + menuMouseleave: function(e) { + var root = $(this).data().contextMenuRoot; + if (root.$layer && root.$layer.is(e.relatedTarget)) { + root.hovering = false; + } + }, + + + // :hover done manually so key handling is possible + itemMouseenter: function(e) { + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + root.hovering = true; + + // abort if we're re-entering + if (e && root.$layer && root.$layer.is(e.relatedTarget)) { + e.preventDefault(); + e.stopImmediatePropagation(); + } + + // make sure only one item is selected + (opt.$menu ? opt : root).$menu + .children('.hover').trigger('contextmenu:blur'); + + if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) { + opt.$selected = null; + return; + } + + $this.trigger('contextmenu:focus'); + }, + // :hover done manually so key handling is possible + itemMouseleave: function(e) { + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) { + root.$selected && root.$selected.trigger('contextmenu:blur'); + e.preventDefault(); + e.stopImmediatePropagation(); + root.$selected = opt.$selected = opt.$node; + return; + } + + $this.trigger('contextmenu:blur'); + }, + // contextMenu item click + itemClick: function(e) { + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot, + key = data.contextMenuKey, + callback; + + // abort if the key is unknown or disabled + if (!opt.items[key] || $this.hasClass('disabled')) { + return; + } + + e.preventDefault(); + e.stopImmediatePropagation(); + + if ($.isFunction(root.callbacks[key])) { + // item-specific callback + callback = root.callbacks[key]; + } else if ($.isFunction(root.callback)) { + // default callback + callback = root.callback; + } else { + // no callback, no action + return; + } + + // hide menu if callback doesn't stop that + if (callback.call(root.$trigger, key, root) !== false) { + root.$menu.trigger('contextmenu:hide'); + } else { + op.update.call(root.$trigger, root); + } + }, + // ignore click events on input elements + inputClick: function(e) { + e.stopImmediatePropagation(); + }, + + // hide <menu> + hideMenu: function(e) { + var root = $(this).data('contextMenuRoot'); + op.hide.call(root.$trigger, root); + }, + // focus <command> + focusItem: function(e) { + e.stopPropagation(); + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + $this.addClass('hover') + .siblings('.hover').trigger('contextmenu:blur'); + + // remember selected + opt.$selected = root.$selected = $this; + + // position sub-menu - do after show so dumb $.ui.position can keep up + if (opt.$node) { + root.positionSubmenu.call(opt.$node, opt.$menu); + } + }, + // blur <command> + blurItem: function(e) { + e.stopPropagation(); + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + $this.removeClass('hover'); + opt.$selected = null; + } + }, + // operations + op = { + show: function(opt, x, y) { + var $this = $(this), + offset, + css = {}; + + // hide any open menus + $('#context-menu-layer').trigger('mousedown'); + + // show event + if (opt.events.show.call($this, opt) === false) { + $currentTrigger = null; + return; + } + + // backreference for callbacks + opt.$trigger = $this; + + // create or update context menu + op.update.call($this, opt); + + // position menu + opt.position.call($this, opt, x, y); + + // make sure we're in front + if (opt.zIndex) { + css.zIndex = zindex($this) + opt.zIndex; + } + + // add layer + op.layer.call(opt.$menu, opt, css.zIndex); + + // adjust sub-menu zIndexes + opt.$menu.find('ul').css('zIndex', css.zIndex + 1); + + // position and show context menu + opt.$menu.css( css )[opt.animation.show](opt.animation.duration); + // make options available + $this.data('contextMenu', opt); + // register key handler + $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key); + // register autoHide handler + if (opt.autoHide) { + // trigger element coordinates + var pos = $this.position(); + pos.right = pos.left + $this.outerWidth(); + pos.bottom = pos.top + this.outerHeight(); + // mouse position handler + $(document).on('mousemove.contextMenuAutoHide', function(e) { + if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) { + // if mouse in menu... + opt.$layer.trigger('mousedown'); + } + }); + } + }, + hide: function(opt) { + var $this = $(this); + if (!opt) { + opt = $this.data('contextMenu') || {}; + } + + // hide event + if (opt.events && opt.events.hide.call($this, opt) === false) { + return; + } + + if (opt.$layer) { + try { + opt.$layer.remove(); + delete opt.$layer; + } catch(e) { + opt.$layer = null; + } + } + + // remove handle + $currentTrigger = null; + // remove selected + opt.$menu.find('.hover').trigger('contextmenu:blur'); + opt.$selected = null; + // unregister key and mouse handlers + //$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705 + $(document).off('.contextMenuAutoHide').off('keydown.contextMenu'); + // hide menu + opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration); + + // tear down dynamically built menu + if (opt.build) { + opt.$menu.remove(); + $.each(opt, function(key, value) { + switch (key) { + case 'ns': + case 'selector': + case 'build': + case 'trigger': + case 'ignoreRightClick': + return true; + + default: + opt[key] = undefined; + try { + delete opt[key]; + } catch (e) {} + return true; + } + }); + } + }, + create: function(opt, root) { + if (root === undefined) { + root = opt; + } + // create contextMenu + opt.$menu = $('<ul class="context-menu-list ' + (opt.className || "") + '"></ul>').data({ + 'contextMenu': opt, + 'contextMenuRoot': root + }); + + $.each(['callbacks', 'commands', 'inputs'], function(i,k){ + opt[k] = {}; + if (!root[k]) { + root[k] = {}; + } + }); + + root.accesskeys || (root.accesskeys = {}); + + // create contextMenu items + $.each(opt.items, function(key, item){ + var $t = $('<li class="context-menu-item ' + (item.className || "") +'"></li>'), + $label = null, + $input = null; + + item.$node = $t.data({ + 'contextMenu': opt, + 'contextMenuRoot': root, + 'contextMenuKey': key + }); + + // register accesskey + // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that + if (item.accesskey) { + var aks = splitAccesskey(item.accesskey); + for (var i=0, ak; ak = aks[i]; i++) { + if (!root.accesskeys[ak]) { + root.accesskeys[ak] = item; + item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>'); + break; + } + } + } + + if (typeof item == "string") { + $t.addClass('context-menu-separator not-selectable'); + } else if (item.type && types[item.type]) { + // run custom type handler + types[item.type].call($t, item, opt, root); + // register commands + $.each([opt, root], function(i,k){ + k.commands[key] = item; + if ($.isFunction(item.callback)) { + k.callbacks[key] = item.callback; + } + }); + } else { + // add label for input + if (item.type == 'html') { + $t.addClass('context-menu-html not-selectable'); + } else if (item.type) { + $label = $('<label></label>').appendTo($t); + $('<span></span>').html(item._name || item.name).appendTo($label); + $t.addClass('context-menu-input'); + opt.hasTypes = true; + $.each([opt, root], function(i,k){ + k.commands[key] = item; + k.inputs[key] = item; + }); + } else if (item.items) { + item.type = 'sub'; + } + + switch (item.type) { + case 'text': + $input = $('<input type="text" value="1" name="context-menu-input-'+ key +'" value="">') + .val(item.value || "").appendTo($label); + break; + + case 'textarea': + $input = $('<textarea name="context-menu-input-'+ key +'"></textarea>') + .val(item.value || "").appendTo($label); + + if (item.height) { + $input.height(item.height); + } + break; + + case 'checkbox': + $input = $('<input type="checkbox" value="1" name="context-menu-input-'+ key +'" value="">') + .val(item.value || "").prop("checked", !!item.selected).prependTo($label); + break; + + case 'radio': + $input = $('<input type="radio" value="1" name="context-menu-input-'+ item.radio +'" value="">') + .val(item.value || "").prop("checked", !!item.selected).prependTo($label); + break; + + case 'select': + $input = $('<select name="context-menu-input-'+ key +'">').appendTo($label); + if (item.options) { + $.each(item.options, function(value, text) { + $('<option></option>').val(value).text(text).appendTo($input); + }); + $input.val(item.selected); + } + break; + + case 'sub': + $('<span></span>').html(item._name || item.name).appendTo($t); + item.appendTo = item.$node; + op.create(item, root); + $t.data('contextMenu', item).addClass('context-menu-submenu'); + item.callback = null; + break; + + case 'html': + $(item.html).appendTo($t); + break; + + default: + $.each([opt, root], function(i,k){ + k.commands[key] = item; + if ($.isFunction(item.callback)) { + k.callbacks[key] = item.callback; + } + }); + + $('<span></span>').html(item._name || item.name || "").appendTo($t); + break; + } + + // disable key listener in <input> + if (item.type && item.type != 'sub' && item.type != 'html') { + $input + .on('focus', handle.focusInput) + .on('blur', handle.blurInput); + + if (item.events) { + $input.on(item.events); + } + } + + // add icons + if (item.icon) { + $t.addClass("icon icon-" + item.icon); + } + } + + // cache contained elements + item.$input = $input; + item.$label = $label; + + // attach item to menu + $t.appendTo(opt.$menu); + + // Disable text selection + if (!opt.hasTypes) { + if($.browser.msie) { + $t.on('selectstart.disableTextSelect', handle.abortevent); + } else if(!$.browser.mozilla) { + $t.on('mousedown.disableTextSelect', handle.abortevent); + } + } + }); + // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element) + if (!opt.$node) { + opt.$menu.css('display', 'none').addClass('context-menu-root'); + } + opt.$menu.appendTo(opt.appendTo || document.body); + }, + update: function(opt, root) { + var $this = this; + if (root === undefined) { + root = opt; + // determine widths of submenus, as CSS won't grow them automatically + // position:absolute > position:absolute; min-width:100; max-width:200; results in width: 100; + // kinda sucks hard... + opt.$menu.find('ul').andSelf().css({position: 'static', display: 'block'}).each(function(){ + var $this = $(this); + $this.width($this.css('position', 'absolute').width()) + .css('position', 'static'); + }).css({position: '', display: ''}); + } + // re-check disabled for each item + opt.$menu.children().each(function(){ + var $item = $(this), + key = $item.data('contextMenuKey'), + item = opt.items[key], + disabled = ($.isFunction(item.disabled) && item.disabled.call($this, key, root)) || item.disabled === true; + + // dis- / enable item + $item[disabled ? 'addClass' : 'removeClass']('disabled'); + + if (item.type) { + // dis- / enable input elements + $item.find('input, select, textarea').prop('disabled', disabled); + + // update input states + switch (item.type) { + case 'text': + case 'textarea': + item.$input.val(item.value || ""); + break; + + case 'checkbox': + case 'radio': + item.$input.val(item.value || "").prop('checked', !!item.selected); + break; + + case 'select': + item.$input.val(item.selected || ""); + break; + } + } + + if (item.$menu) { + // update sub-menu + op.update.call($this, item, root); + } + }); + }, + layer: function(opt, zIndex) { + // add transparent layer for click area + // filter and background for Internet Explorer, Issue #23 + return opt.$layer = $('<div id="context-menu-layer" style="position:fixed; z-index:' + zIndex + '; top:0; left:0; opacity: 0; filter: alpha(opacity=0); background-color: #000;"></div>') + .css({height: $win.height(), width: $win.width(), display: 'block'}) + .data('contextMenuRoot', opt) + .insertBefore(this) + .on('mousedown', handle.layerClick); + } + }; + +// split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key +function splitAccesskey(val) { + var t = val.split(/\s+/), + keys = []; + + for (var i=0, k; k = t[i]; i++) { + k = k[0].toUpperCase(); // first character only + // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it. + // a map to look up already used access keys would be nice + keys.push(k); + } + + return keys; +} + +// handle contextMenu triggers +$.fn.contextMenu = function(operation) { + if (operation === undefined) { + this.first().trigger('contextmenu'); + } else if (operation.x && operation.y) { + this.first().trigger(jQuery.Event("contextmenu", {pageX: operation.x, pageY: operation.y})); + } else if (operation === "hide") { + var $menu = this.data('contextMenu').$menu; + $menu && $menu.trigger('contextmenu:hide'); + } else if (operation) { + this.removeClass('context-menu-disabled'); + } else if (!operation) { + this.addClass('context-menu-disabled'); + } + + return this; +}; + +// manage contextMenu instances +$.contextMenu = function(operation, options) { + if (typeof operation != 'string') { + options = operation; + operation = 'create'; + } + + if (typeof options == 'string') { + options = {selector: options}; + } else if (options === undefined) { + options = {}; + } + + // merge with default options + var o = $.extend(true, {}, defaults, options || {}), + $body = $body = $(document); + + switch (operation) { + case 'create': + // no selector no joy + if (!o.selector) { + throw new Error('No selector specified'); + } + // make sure internal classes are not bound to + if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) { + throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className'); + } + if (!o.build && (!o.items || $.isEmptyObject(o.items))) { + throw new Error('No Items sepcified'); + } + counter ++; + o.ns = '.contextMenu' + counter; + namespaces[o.selector] = o.ns; + menus[o.ns] = o; + + if (!initialized) { + // make sure item click is registered first + $body + .on({ + 'contextmenu:hide.contextMenu': handle.hideMenu, + 'prevcommand.contextMenu': handle.prevItem, + 'nextcommand.contextMenu': handle.nextItem, + 'contextmenu.contextMenu': handle.abortevent, + 'mouseenter.contextMenu': handle.menuMouseenter, + 'mouseleave.contextMenu': handle.menuMouseleave + }, '.context-menu-list') + .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick) + .on({ + 'mouseup.contextMenu': handle.itemClick, + 'contextmenu:focus.contextMenu': handle.focusItem, + 'contextmenu:blur.contextMenu': handle.blurItem, + 'contextmenu.contextMenu': handle.abortevent, + 'mouseenter.contextMenu': handle.itemMouseenter, + 'mouseleave.contextMenu': handle.itemMouseleave + }, '.context-menu-item'); + + initialized = true; + } + + // engage native contextmenu event + $body + .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu); + + switch (o.trigger) { + case 'hover': + $body + .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter) + .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave); + break; + + case 'left': + $body.on('click' + o.ns, o.selector, o, handle.click); + break; + /* + default: + // http://www.quirksmode.org/dom/events/contextmenu.html + $body + .on('mousedown' + o.ns, o.selector, o, handle.mousedown) + .on('mouseup' + o.ns, o.selector, o, handle.mouseup); + break; + */ + } + + if (o.trigger != 'hover' && o.ignoreRightClick) { + $body.on('mousedown' + o.ns, o.selector, handle.ignoreRightClick); + } + + // create menu + if (!o.build) { + op.create(o); + } + break; + + case 'destroy': + if (!o.selector) { + $body.off('.contextMenu .contextMenuAutoHide'); + $.each(namespaces, function(key, value) { + $body.off(value); + }); + + namespaces = {}; + menus = {}; + counter = 0; + initialized = false; + + $('.context-menu-list').remove(); + } else if (namespaces[o.selector]) { + try { + if (menus[namespaces[o.selector]].$menu) { + menus[namespaces[o.selector]].$menu.remove(); + } + + delete menus[namespaces[o.selector]]; + } catch(e) { + menus[namespaces[o.selector]] = null; + } + + $body.off(namespaces[o.selector]); + } + break; + + case 'html5': + // if <command> or <menuitem> are not handled by the browser, + // or options was a bool true, + // initialize $.contextMenu for them + if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) { + $('menu[type="context"]').each(function() { + if (this.id) { + $.contextMenu({ + selector: '[contextmenu=' + this.id +']', + items: $.contextMenu.fromMenu(this) + }); + } + }).css('display', 'none'); + } + break; + + default: + throw new Error('Unknown operation "' + operation + '"'); + } + + return this; +}; + +// import values into <input> commands +$.contextMenu.setInputValues = function(opt, data) { + if (data === undefined) { + data = {}; + } + + $.each(opt.inputs, function(key, item) { + switch (item.type) { + case 'text': + case 'textarea': + item.value = data[key] || ""; + break; + + case 'checkbox': + item.selected = data[key] ? true : false; + break; + + case 'radio': + item.selected = (data[item.radio] || "") == item.value ? true : false; + break; + + case 'select': + item.selected = data[key] || ""; + break; + } + }); +}; + +// export values from <input> commands +$.contextMenu.getInputValues = function(opt, data) { + if (data === undefined) { + data = {}; + } + + $.each(opt.inputs, function(key, item) { + switch (item.type) { + case 'text': + case 'textarea': + case 'select': + data[key] = item.$input.val(); + break; + + case 'checkbox': + data[key] = item.$input.prop('checked'); + break; + + case 'radio': + if (item.$input.prop('checked')) { + data[item.radio] = item.value; + } + break; + } + }); + + return data; +}; + +// find <label for="xyz"> +function inputLabel(node) { + return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name; +} + +// convert <menu> to items object +function menuChildren(items, $children, counter) { + if (!counter) { + counter = 0; + } + + $children.each(function() { + var $node = $(this), + node = this, + nodeName = this.nodeName.toLowerCase(), + label, + item; + + // extract <label><input> + if (nodeName == 'label' && $node.find('input, textarea, select').length) { + label = $node.text(); + $node = $node.children().first(); + node = $node.get(0); + nodeName = node.nodeName.toLowerCase(); + } + + /* + * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items. + * Not being the sadistic kind, $.contextMenu only accepts: + * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>. + * Everything else will be imported as an html node, which is not interfaced with contextMenu. + */ + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command + switch (nodeName) { + // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element + case 'menu': + item = {name: $node.attr('label'), items: {}}; + menuChildren(item.items, $node.children(), counter); + break; + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command + case 'a': + // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command + case 'button': + item = { + name: $node.text(), + disabled: !!$node.attr('disabled'), + callback: (function(){ return function(){ $node.click(); }; })() + }; + break; + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command + + case 'menuitem': + case 'command': + switch ($node.attr('type')) { + case undefined: + case 'command': + case 'menuitem': + item = { + name: $node.attr('label'), + disabled: !!$node.attr('disabled'), + callback: (function(){ return function(){ $node.click(); }; })() + }; + break; + + case 'checkbox': + item = { + type: 'checkbox', + disabled: !!$node.attr('disabled'), + name: $node.attr('label'), + selected: !!$node.attr('checked') + }; + break; + + case 'radio': + item = { + type: 'radio', + disabled: !!$node.attr('disabled'), + name: $node.attr('label'), + radio: $node.attr('radiogroup'), + value: $node.attr('id'), + selected: !!$node.attr('checked') + }; + break; + + default: + item = undefined; + } + break; + + case 'hr': + item = '-------'; + break; + + case 'input': + switch ($node.attr('type')) { + case 'text': + item = { + type: 'text', + name: label || inputLabel(node), + disabled: !!$node.attr('disabled'), + value: $node.val() + }; + break; + + case 'checkbox': + item = { + type: 'checkbox', + name: label || inputLabel(node), + disabled: !!$node.attr('disabled'), + selected: !!$node.attr('checked') + }; + break; + + case 'radio': + item = { + type: 'radio', + name: label || inputLabel(node), + disabled: !!$node.attr('disabled'), + radio: !!$node.attr('name'), + value: $node.val(), + selected: !!$node.attr('checked') + }; + break; + + default: + item = undefined; + break; + } + break; + + case 'select': + item = { + type: 'select', + name: label || inputLabel(node), + disabled: !!$node.attr('disabled'), + selected: $node.val(), + options: {} + }; + $node.children().each(function(){ + item.options[this.value] = $(this).text(); + }); + break; + + case 'textarea': + item = { + type: 'textarea', + name: label || inputLabel(node), + disabled: !!$node.attr('disabled'), + value: $node.val() + }; + break; + + case 'label': + break; + + default: + item = {type: 'html', html: $node.clone(true)}; + break; + } + + if (item) { + counter++; + items['key' + counter] = item; + } + }); +} + +// convert html5 menu +$.contextMenu.fromMenu = function(element) { + var $this = $(element), + items = {}; + + menuChildren(items, $this.children()); + + return items; +}; + +// make defaults accessible +$.contextMenu.defaults = defaults; +$.contextMenu.types = types; + +})(jQuery);