diff --git a/app/assets/javascripts/direct_upload.js.coffee b/app/assets/javascripts/direct_upload.js.coffee deleted file mode 100644 index 10ec9d9df8..0000000000 --- a/app/assets/javascripts/direct_upload.js.coffee +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2011-2023, The Trustees of Indiana University and Northwestern -# University. 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. -# --- END LICENSE_HEADER BLOCK --- - -$ -> - $('.directupload').find("input:file").each (i, elem)-> - file_input = $(elem) - form = $(file_input.parents('form:first')) - submit_button = form.find('input[type="submit"], *[data-trigger="submit"]') - submit_button.on 'click', -> - form.addClass('form-disabled') - $('.directupload input:file').fileupload 'send', - files: $('.directupload input:file').prop('files') - return false - progress_bar = $("
"); - bar_container = $("
").append(progress_bar); - $('div.fileinput').after(bar_container) - file_input.fileupload - fileInput: file_input - url: form.data('url') - type: 'POST' - autoUpload: false - formData: form.data('form-data') - paramName: 'file' - dataType: 'XML' - replaceFileInput: false - progressall: (e, data)-> - progress = parseInt(data.loaded / data.total * 100, 10) - progress_bar.css('width', "#{progress}%") - start: (e)-> - progress_bar. - css('background', 'green'). - css('display', 'block'). - css('width', '0%'). - css('padding', '7px'). - text("Loading...") - done: (e, data)-> - form.removeClass('form-disabled') - progress_bar.text("Uploading done") - - # extract key and generate URL from response - key = $(data.jqXHR.responseXML).find("Key").text(); - bucket = $(data.jqXHR.responseXML).find("Bucket").text(); - url = "s3://#{bucket}/#{key}" - - # create hidden field - input = $ "", - type: 'hidden' - name: 'selected_files[0][url]' - value: url - file_input.replaceWith(input) - form.submit() - fail: (e, data)-> - form.removeClass('form-disabled') - progress_bar. - css("background", "red"). - text("Failed") diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index 2e34a79336..1008793928 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -367,16 +367,6 @@ span.constraints-label { white-space: nowrap; } -.fileinput { - max-width: 500px; - - .form-control { - max-width: 500px; - white-space: nowrap; - overflow-x: hidden; - } -} - a[data-trigger='submit'] { text-decoration: none; color: $dark; @@ -990,12 +980,6 @@ h5.card-title { word-break: break-all; } -// Fixes the input displaying over the dropdown datepicker calendar -.fileinput { - position: relative; - z-index: 1; -} - .is-invalid { border-color: $danger; } @@ -1111,37 +1095,6 @@ h5.card-title { } } -/* File upload step */ -.file-upload-buttons { - display: block; - padding-top: 5px; - min-width: 75px; - margin-right: 5px; - height: 30px; -} - -.fileinput-filename { - width: auto; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-top: 5px; -} - -.form-disabled { - pointer-events: none; - opacity: 0.4; -} - -.fileinput-close { - padding-top: 5px; - float: none; -} - -#file-upload { - display: flex; -} - /* DataTables in Playlists, Timelines, Persona Users, and Encode Dashboard */ .dataTableToolsTop { text-align: right; diff --git a/app/assets/stylesheets/avalon/_form.scss b/app/assets/stylesheets/avalon/_form.scss index bceb1e904a..c79de24fcb 100644 --- a/app/assets/stylesheets/avalon/_form.scss +++ b/app/assets/stylesheets/avalon/_form.scss @@ -98,17 +98,6 @@ label { color: $dark; } -.fileinput-submit { - background-color: $blue !important; - border-color: $blue !important; - - &:hover { - background-color: darken($blue, 15%) !important; - } - - color: $white !important; -} - .upload-file-wrapper { padding: 5px 5px 0 5px !important; } @@ -118,9 +107,3 @@ label { font-weight: bold; } } - -#resource-description { - .input-group { - flex-wrap: nowrap; - } -} diff --git a/app/helpers/upload_form_helper.rb b/app/helpers/upload_form_helper.rb index 7a34d4b97f..3a577b550e 100644 --- a/app/helpers/upload_form_helper.rb +++ b/app/helpers/upload_form_helper.rb @@ -40,4 +40,12 @@ def upload_form_data {} end end + + # def multipart_upload_data + # if direct_upload? + # bucket = Aws::S3::Bucket.new(name: Settings.encoding.masterfile_bucket) + # multipart_upload = Aws::S3::Client.new.create_multipart_upload(bucket: bucket.name, key: 'lo') + # { 'upload_id' => multipart_upload[:upload_id] } + # end + # end end diff --git a/app/javascript/components/FileUploadUppy.jsx b/app/javascript/components/FileUploadUppy.jsx new file mode 100755 index 0000000000..759c640f78 --- /dev/null +++ b/app/javascript/components/FileUploadUppy.jsx @@ -0,0 +1,117 @@ +import React from "react"; +import Uppy from '@uppy/core'; +import { Dashboard } from '@uppy/react'; +import ActiveStorageUpload from '@excid3/uppy-activestorage-upload'; +import GoogleDrive from '@uppy/google-drive'; +import AwsS3 from '@uppy/aws-s3'; +import AwsS3Multipart from '@uppy/aws-s3-multipart'; + +require('@uppy/core/dist/style.css') +require('@uppy/dashboard/dist/style.css') + +class FileUploadUppy extends React.Component { + constructor(props) { + super(props); + // this.state = { + // dirUploadURL: document.querySelector("meta[name='direct-upload-url']").getAttribute("content"), + // }; + } + + componentWillMount() { + const t = this; + this.uppy = new Uppy({ + id: "uppy-file-upload", + autoProceed: true, + restrictions: { + allowedFileTypes: [".mp4", ".mp3"] + }, + }); + + this.uppy + // .use(ActiveStorageUpload, { + // directUploadUrl: this.state.dirUploadURL + // }) + .use(GoogleDrive, { + companionUrl: 'http://localhost:3020' + }) + // .use(AwsS3Multipart, { + // limit: 5, + // timeout: 60*1000, // set to 1min + // companionUrl: '/', + // }) + .use(AwsS3, { + limit: 5, + timeout: 60*1000, // set to 1min + companionUrl: 'http://localhost:3020', + getUploadParameters() { + return Promise.resolve({ + method: 'POST', + url: t.props.uploadData['url'], + fields: t.props.uploadData['form-data'] + }); + } + }) + .on("file-removed", (file, c, d) => { + console.log("File Removed --- ", file, c, d); + }) + .on("file-added", () => { + console.log("File Added, ", t.props.uploadData); + }) + .on("complete", function({ failed, successful }) { + if(failed.length > 0) { + console.log("File Upload S3 --- Error"); + } + if(successful.length > 0) { + console.log("File Upload S3 --- Success"); + const { containerID, step } = t.props; + let formData = new FormData(); + successful.map((res, index) => { + const s3Url = res.uploadURL.replace('http://localhost:9000/', 's3://'); + formData.append('selected_files[' + index + '][url]', s3Url); + }) + formData.append('container_id', containerID); + formData.append('step', step); + + fetch('http://localhost:3000/master_files', { + method: 'POST', + headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') }, + body: formData + }) + .then(res => { + location.reload(); + }) + .catch(error => { + console.error('MasterFile creation failed, ', error); + }); + } + }); + } + + componentWillUnmount() { + this.uppy.close(); + } + + render() { + return ( + + + + + ); + } +} + +export default FileUploadUppy; diff --git a/app/models/master_file.rb b/app/models/master_file.rb index 59e201efc1..1455c7bf63 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -181,7 +181,7 @@ def error # be set up in a configuration file somewhere # # 250 MB is the file limit for now - MAXIMUM_UPLOAD_SIZE = Settings.max_upload_size || 2.gigabytes + MAXIMUM_UPLOAD_SIZE = Settings.max_upload_size || 5.gigabytes WORKFLOWS = ['fullaudio', 'avalon', 'pass_through', 'avalon-skip-transcoding', 'avalon-skip-transcoding-audio'].freeze AUDIO_TYPES = ["audio/vnd.wave", "audio/mpeg", "audio/mp3", "audio/mp4", "audio/wav", "audio/x-wav"] diff --git a/app/views/media_objects/_file_upload.html.erb b/app/views/media_objects/_file_upload.html.erb index b011eb0645..768895c523 100644 --- a/app/views/media_objects/_file_upload.html.erb +++ b/app/views/media_objects/_file_upload.html.erb @@ -168,99 +168,11 @@ Unless required by applicable law or agreed to in writing, software distributed
<% end %> -
-
- -

Upload through the web (files must not exceed <%= number_to_human_size MasterFile::MAXIMUM_UPLOAD_SIZE %>)

-
- <%= form_tag(master_files_path, :enctype=>"multipart/form-data", class: upload_form_classes, data: upload_form_data) do -%> - - - - <%= hidden_field_tag("container_content_type", container_content_type, :id => "file_upload_content_type") if defined?(container_content_type) %> - - <%- field_tag_options = defined?(uploader_options) ? uploader_options : {multiple: true} %> - - - <%= check_box_tag(:workflow, 'skip_transcoding', false, id: 'skip_transcoding')%> - <%= label_tag(:skip_transcoding) do %> -
- Skip transcoding -
- <% end %> -
- -
- Upload - - Select file - Change - - - - × -
- - <%= hidden_field_tag(:new_asset, true, :id => "files_new_asset") if params[:new_asset] %> - <%= hidden_field_tag("id",params[:id], :id => "file_upload_id") if params[:id] %> - <%= hidden_field_tag(:original, params[:original], :id => "files_original") %> - <% end %> -
- -
- -

<%= t("file_upload_tip.skip_transcoding").html_safe %>

-
- - -
- - -
-
-
-

- Use the dropbox to import large files. - - - -

- - - <%= content_tag :span, '', class: 'close fa fa-times' %> -

- Attach selected files after uploading. Files will begin - processing when you click "Save and continue". -

- <%= render partial: "dropbox_details" %> -
-
-
- - <%= form_tag(master_files_path, id: 'dropbox_form', method: 'post') do %> - <%= hidden_field_tag("workflow") %> - -
- <%= button_tag("Open Dropbox", type: 'button', class: 'btn btn-outline', id: "browse-btn", - 'data-toggle' => 'browse-everything', 'data-route' => browse_everything_engine.root_path, - 'data-target' => '#dropbox_form', 'data-context' => @media_object.collection.id ) %> -
- <% end %> -
+
+ +

Upload through the web (files must not exceed <%= number_to_human_size MasterFile::MAXIMUM_UPLOAD_SIZE %>)

+ <%= react_component("FileUploadUppy", { uploadData: upload_form_data, containerID: @media_object.id, step: 'file_upload' }) %>
-
diff --git a/db/schema.rb b/db/schema.rb index cae4d0b097..5fed79e84b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,12 +12,15 @@ ActiveRecord::Schema.define(version: 2022_08_22_170237) do - create_table "active_encode_encode_records", force: :cascade do |t| + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "active_encode_encode_records", id: :serial, force: :cascade do |t| t.string "global_id" t.string "state" t.string "adapter" t.string "title" - t.text "raw_object", limit: 16777215 + t.text "raw_object" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "create_options" @@ -33,8 +36,8 @@ create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false - t.integer "record_id", null: false - t.integer "blob_id", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false t.datetime "created_at", null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true @@ -54,7 +57,7 @@ create_table "annotations", force: :cascade do |t| t.string "uuid" t.string "source_uri" - t.integer "playlist_item_id" + t.bigint "playlist_item_id" t.text "annotation" t.string "type" t.index ["playlist_item_id"], name: "index_annotations_on_playlist_item_id" @@ -72,12 +75,12 @@ end create_table "batch_entries", force: :cascade do |t| - t.integer "batch_registries_id" - t.text "payload", limit: 1073741823 + t.bigint "batch_registries_id" + t.text "payload" t.boolean "complete", default: false, null: false t.boolean "error", default: false, null: false t.string "current_status" - t.text "error_message", limit: 65535 + t.text "error_message" t.string "media_object_pid" t.integer "position" t.datetime "created_at", null: false @@ -116,7 +119,7 @@ end create_table "checkouts", force: :cascade do |t| - t.integer "user_id" + t.bigint "user_id" t.string "media_object_id" t.datetime "checkout_time" t.datetime "return_time" @@ -167,7 +170,7 @@ t.string "namespace", default: "default", null: false t.string "template", null: false t.text "counters" - t.integer "seq", default: 0 + t.bigint "seq", default: 0 t.binary "rand" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -175,8 +178,8 @@ end create_table "playlist_items", force: :cascade do |t| - t.integer "playlist_id", null: false - t.integer "clip_id", null: false + t.bigint "playlist_id", null: false + t.bigint "clip_id", null: false t.integer "position" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -186,7 +189,7 @@ create_table "playlists", force: :cascade do |t| t.string "title" - t.integer "user_id", null: false + t.bigint "user_id", null: false t.string "comment" t.string "visibility" t.datetime "created_at", null: false @@ -234,13 +237,13 @@ create_table "timelines", force: :cascade do |t| t.string "title" - t.integer "user_id" + t.bigint "user_id" t.string "visibility" t.text "description" t.string "access_token" t.string "tags" t.string "source" - t.text "manifest", limit: 16777215 + t.text "manifest" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["user_id"], name: "index_timelines_on_user_id" @@ -280,4 +283,5 @@ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "checkouts", "users" + add_foreign_key "timelines", "users" end diff --git a/docker-compose.yml b/docker-compose.yml index 1038e2caaf..c5c6838bdf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,6 +75,49 @@ services: redis-test: <<: *redis + uppy: + image: transloadit/companion + # build: + # context: . + # dockerfile: Dockerfile + # volumes: + # - /app/node_modules + # - /mnt/uppy-server-data:/mnt/uppy-server-data + ports: + - "3020:3020" + networks: + internal: + external: + environment: + - COMPANION_S3_KEY=minio + - COMPANION_S3_SECRET=minio123 + - COMPANION_S3_BUCKET=masterfiles + # - COMPANION_S3_REGION=us-east-1 + # AWS config + - COMPANION_AWS_KEY=minio + - COMPANION_AWS_SECRET=minio123 + - COMPANION_AWS_BUCKET=masterfiles + # - COMPANION_AWS_REGION=us-east-1 + # - COMPANION_AWS_ENDPOINT="http://localhost:9000" + # to enable S3 Transfer Acceleration (default: false) + - COMPANION_AWS_USE_ACCELERATE_ENDPOINT="false" + # to set a canned ACL for uploaded objects: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl + - COMPANION_AWS_ACL="private" + # - COMPANION_S3_ENDPOINT=http://localhost:9000 + # any long set of random characters for the server session + - COMPANION_SECRET="shh!Issa Secret!" + # specifying a secret file will override a directly set secret + # - COMPANION_SECRET_FILE="PATH/TO/COMPANION/SECRET/FILE" + # corresponds to the server.host option + - COMPANION_DOMAIN="localhost" + - COMPANION_PROTOCOL="http" + # corresponds to the filePath option + - COMPANION_DATADIR=/tmp + # to enable Google Drive + - COMPANION_GOOGLE_KEY='826510525745-v1p7b4nlv4h5o1k3qde7r526o5l3un4s.apps.googleusercontent.com' + - COMPANION_GOOGLE_SECRET='xoK-_Di1Ijt6wsY1yvilpa2B' + + avalon: &avalon image: avalonmediasystem/avalon:7.4.0-dev build: @@ -88,6 +131,7 @@ services: - redis - hls - minio + - uppy environment: - APP_NAME=avalon - BUNDLE_FLAGS=--with development postgres --without production test @@ -166,6 +210,8 @@ services: environment: MINIO_ACCESS_KEY: minio MINIO_SECRET_KEY: minio123 + # MINIO_SERVER_URL: http://minio:9000 + # MINIO_BROWSER_REDIRECT_URL: http://minio:9000 volumes: - data:/data ports: diff --git a/package.json b/package.json index fa2041f6ef..4509c55358 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,20 @@ "dependencies": { "@babel/core": "^7.0.0", "@babel/preset-react": "^7.0.0", + "@excid3/uppy-activestorage-upload": "https://github.com/excid3/uppy-activestorage-upload.git", + "@rails/activestorage": "^7.0.4", "@rails/webpacker": "^5.2.2", "@samvera/iiif-react-media-player": "1.2.1", + "@uppy/aws-s3": "^3.0.4", + "@uppy/aws-s3-multipart": "^3.1.1", + "@uppy/companion": "^4.0.5", + "@uppy/core": "^3.0.4", + "@uppy/dashboard": "^3.1.0", + "@uppy/drag-drop": "^3.0.1", + "@uppy/file-input": "^3.0.1", + "@uppy/google-drive": "^3.0.1", + "@uppy/progress-bar": "^3.0.1", + "@uppy/react": "^3.0.2", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "cropperjs": "^1.5.5", "hls.js": "https://github.com/avalonmediasystem/hls.js#stricter_ts_probing", diff --git a/vendor/assets/javascripts/jasny-bootstrap.min.js b/vendor/assets/javascripts/jasny-bootstrap.min.js deleted file mode 100644 index 0242942f43..0000000000 --- a/vendor/assets/javascripts/jasny-bootstrap.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Jasny Bootstrap v4.0.0 (http://jasny.github.io/bootstrap) - * Copyright 2012-2019 Arnold Daniels - * Licensed under () - */ -if("undefined"==typeof jQuery)throw new Error("Jasny Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}void 0===a.support.transition&&(a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()}))}(window.jQuery),function(a){"use strict";var b=navigator.userAgent.match(/iPhone/i)||navigator.userAgent.match(/iPod/i),c=function(b,d){if(this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.state=null,this.placement=null,this.$calcClone=null,this.calcClone(),this.options.recalc&&a(window).on("resize",a.proxy(this.recalc,this)),this.options.autohide&&!this.options.modal){navigator.userAgent.match(/(iPad|iPhone)/i);a(document).on("click touchstart",a.proxy(this.autohide,this))}a(this.$element).on("shown.bs.dropdown",a.proxy(function(b){a(this.$element).find(".dropdown .dropdown-backdrop").remove()},this)),"boolean"==typeof this.options.disablescrolling&&(this.options.disableScrolling=this.options.disablescrolling,delete this.options.disablescrolling),this.options.toggle&&this.toggle()};c.DEFAULTS={toggle:!0,placement:"auto",autohide:!0,recalc:!0,disableScrolling:!0,modal:!1,backdrop:!1,exclude:null},c.prototype.setWidth=function(){var b=this.$element.outerWidth(),c=a(window).width();c-=68,this.$element.css("width",b>c?c:b)},c.prototype.offset=function(){switch(this.placement){case"left":case"right":return this.$element.outerWidth();case"top":case"bottom":return this.$element.outerHeight()}},c.prototype.calcPlacement=function(){function b(a,b){return"auto"===e.css(b)?a:"auto"===e.css(a)?b:parseInt(e.css(a),10)>parseInt(e.css(b),10)?b:a}if("auto"!==this.options.placement)return void(this.placement=this.options.placement);this.$element.hasClass("in")||this.$element.css("visiblity","hidden !important").addClass("in");var c=a(window).width()/this.$element.outerWidth(),d=a(window).height()/this.$element.outerHeight(),e=this.$element;this.placement=c>d?b("left","right"):b("top","bottom"),"hidden !important"===this.$element.css("visibility")&&this.$element.removeClass("in").css("visiblity","")},c.prototype.opposite=function(a){switch(a){case"top":return"bottom";case"left":return"right";case"bottom":return"top";case"right":return"left"}},c.prototype.getCanvasElements=function(){var b=this.options.canvas?a(this.options.canvas):this.$element,c=b.find("*").filter(function(){return"fixed"===getComputedStyle(this).getPropertyValue("position")}).not(this.options.exclude);return b.add(c)},c.prototype.slide=function(b,c,d){if(!a.support.transition){var e={};return e[this.placement]="+="+c,e[this.opposite(this.placement)]="-="+c,b.animate(e,350,d)}var f=this.placement,g=this.opposite(f);b.each(function(){"auto"!==a(this).css(f)&&a(this).css(f,(parseInt(a(this).css(f),10)||0)+c),"auto"!==a(this).css(g)&&a(this).css(g,(parseInt(a(this).css(g),10)||0)-c)}),this.$element.one(a.support.transition.end,d).emulateTransitionEnd(350)},c.prototype.disableScrolling=function(){var c=a("body").width(),d="padding-right";if(void 0===a("body").data("offcanvas-style")&&a("body").data("offcanvas-style",a("body").attr("style")||""),a("body").css("overflow","hidden"),b&&a("body").addClass("lockIphone"),a("body").width()>c){var e=parseInt(a("body").css(d),10)+a("body").width()-c;setTimeout(function(){a("body").css(d,e)},1)}a("body").on("touchmove.bs",function(b){a(event.target).closest(".offcanvas").length||b.preventDefault()})},c.prototype.enableScrolling=function(){a("body").off("touchmove.bs"),a("body").removeClass("lockIphone")},c.prototype.show=function(){if(!this.state){var c=a.Event("show.bs.offcanvas");this.$element.trigger(c),c.isDefaultPrevented()||this.hideOthers(a.proxy(function(){this.state="slide-in",this.$element.css("width",""),this.calcPlacement(),this.setWidth();var c=this.getCanvasElements(),d=this.placement,e=this.opposite(d),f=this.offset();-1!==c.index(this.$element)&&(a(this.$element).data("offcanvas-style",a(this.$element).attr("style")||""),this.$element.css(d,-1*f),this.$element.css(d)),c.addClass("canvas-sliding").each(function(){var c=a(this);void 0===c.data("offcanvas-style")&&c.data("offcanvas-style",c.attr("style")||""),"static"!==c.css("position")||b||c.css("position","relative"),"auto"!==c.css(d)&&"0px"!==c.css(d)||"auto"!==c.css(e)&&"0px"!==c.css(e)||c.css(d,0)}),this.options.disableScrolling&&this.disableScrolling(),(this.options.modal||this.options.backdrop)&&this.toggleBackdrop();var g=function(){"slide-in"==this.state&&(this.state="slid",c.removeClass("canvas-sliding").addClass("canvas-slid"),this.$element.trigger("shown.bs.offcanvas"))};setTimeout(a.proxy(function(){this.$element.addClass("in"),this.slide(c,f,a.proxy(g,this))},this),1)},this))}},c.prototype.hideOthers=function(b){var c=!1,d=this.$element.attr("id"),e=a('.offcanvas-clone:not([data-id="'+d+'"])');if(!e.length)return b();e.each(function(d,e){var f=a(e).attr("data-id"),g=a("#"+f);(c=g.hasClass("canvas-slid"))&&(g.one("hidden.bs.offcanvas",b),g.offcanvas("hide"))}),c||b()},c.prototype.hide=function(){if("slid"===this.state){var b=a.Event("hide.bs.offcanvas");if(this.$element.trigger(b),!b.isDefaultPrevented()){this.state="slide-out";var c=a(".canvas-slid"),d=(this.placement,-1*this.offset()),e=function(){"slide-out"==this.state&&(this.state=null,this.placement=null,this.$element.removeClass("in"),c.removeClass("canvas-sliding"),c.add(this.$element).add("body").each(function(){a(this).attr("style",a(this).data("offcanvas-style")).removeData("offcanvas-style")}),this.$element.css("width",""),this.$element.trigger("hidden.bs.offcanvas"))};this.options.disableScrolling&&this.enableScrolling(),(this.options.modal||this.options.backdrop)&&this.toggleBackdrop(),c.removeClass("canvas-slid").addClass("canvas-sliding"),setTimeout(a.proxy(function(){this.slide(c,d,a.proxy(e,this))},this),1)}}},c.prototype.toggle=function(){"slide-in"!==this.state&&"slide-out"!==this.state&&this["slid"===this.state?"hide":"show"]()},c.prototype.toggleBackdrop=function(b){b=b||a.noop;var c=150;if("slide-in"==this.state){var d=a("body"),e=a.support.transition;this.$backdrop=a('