diff --git a/Services/Form/classes/class.ilFileStandardDropzoneInputGUI.php b/Services/Form/classes/class.ilFileStandardDropzoneInputGUI.php new file mode 100755 index 000000000000..a5e0487a7c44 --- /dev/null +++ b/Services/Form/classes/class.ilFileStandardDropzoneInputGUI.php @@ -0,0 +1,229 @@ + + */ +class ilFileStandardDropzoneInputGUI extends ilFileInputGUI implements ilToolbarItem { + + const ASYNC_FILEUPLOAD = "async_fileupload"; + /** + * @var int if there are more than one ilFileStandardDropzoneInputGUI in the same Form, this + * value will be incremented during rendering to make sure all Inputs will be handled + * correctly + */ + protected static $count = 0; + /** + * @var string Set it to the URL (using ilCtrl->getFormAction() ) to override the Endpoint the + * Form will be sent to. If not set, the ilFileStandardDropzoneInputGUI will get the + * Form-Action of it's nearest form + */ + protected $upload_url = ''; + /** + * @var int The amount of files which can be uploaded. Standard is 1 since the old + * ilFileInputGUI in most cases allows one. + */ + protected $max_files = 1; + /** + * @var \ILIAS\Data\DataSize only files beneath this size will be accepted to upload. Currently + * this uses the defined valued of the php.ini + */ + protected $max_file_size; + /** + * @var string The message which will be rendered within the dropzone. + */ + protected $dropzone_message = ''; + + + /** + * @return string the URL where the form will be sent to. + */ + public function getUploadUrl() { + return $this->upload_url; + } + + + /** + * Set the URL (using ilCtrl->getFormAction() ) to override the Endpoint the + * Form will be sent to. If not set, the ilFileStandardDropzoneInputGUI will get the + * Form-Action of it's nearest form + * + * @param string $upload_url + * + * @return $this + */ + public function setUploadUrl($upload_url) { + $this->upload_url = $upload_url; + + return $this; + } + + + /** + * @return int Amount of allowed files in this input + */ + public function getMaxFiles() { + return $this->max_files; + } + + + /** + * @param int $max_files The amount of files which can be uploaded. Standard is 1 since the old + * ilFileInputGUI in most cases allows one. + */ + public function setMaxFiles($max_files) { + $this->max_files = $max_files; + } + + + /** + * @return \ILIAS\Data\DataSize allowed size of files which can be uploaded + */ + public function getMaxFilesize() { + return $this->max_file_size; + } + + + /** + * @param \ILIAS\Data\DataSize $max_file_size only files beneath this size will be accepted to + * upload. Currently this uses the defined valued of + * the php.ini + */ + public function setMaxFilesize(\ILIAS\Data\DataSize $max_file_size) { + $this->max_file_size = $max_file_size; + } + + + /** + * @return string The message which will be rendered within the dropzone. + */ + public function getDropzoneMessage() { + return $this->dropzone_message; + } + + + /** + * @param string $dropzone_message The message which will be rendered within the dropzone. + */ + public function setDropzoneMessage($dropzone_message) { + $this->dropzone_message = $dropzone_message; + } + + + /** + * @inheritdoc + */ + public function render($a_mode = "") { + global $DIC; + + $this->handleUploadURL(); + $this->handleSuffixes(); + + $f = $DIC->ui()->factory(); + $r = $DIC->ui()->renderer(); + + $dropzone = $f->dropzone() + ->file() + ->standard($this->getUploadUrl()) + ->withParameterName($this->getPostVar()) + ->withMaxFiles($this->getMaxFiles()) + ->withMessage($this->getDropzoneMessage()) + ->withAllowedFileTypes($this->getSuffixes()); + $dropzone = $this->handleMaxFileSize($dropzone); + if ($this->isFileNameSelectionEnabled()) { + $dropzone = $dropzone->withUserDefinedFileNamesEnabled(true); + } + + $render = $r->render($dropzone); + + $n = ++ self::$count; + $out = "
" . $render . '
'; + // We need some javascript magic + /** @var ilTemplate $tpl */ + $tpl = $DIC['tpl']; + $tpl->addJavaScript('./Services/Form/js/ilFileStandardDropzoneInputGUI.js'); + $tpl->addOnLoadCode("ilFileStandardDropzoneInputGUI.init('ilFileStandardDropzoneInputGUIWrapper{$n}');"); + + return $out; + } + + + /** + * @inheritdoc + */ + public function checkInput() { + global $DIC; + + $hasUploads = $DIC->upload()->hasUploads(); + if ($this->getRequired() && !$hasUploads) { + return false; // No file uploaded but is was required + } + + if ($hasUploads) { + try { + $_POST[$this->getPostVar()] = $_FILES[$this->getPostVar()]; + } catch (Exception $e) { + return false; + } + + return true; + } + + return true; + } + + + protected function handleUploadURL() { + if (!$this->getUploadUrl()) { + $parentWrapper = $this; + while (!$parentWrapper instanceof ilPropertyFormGUI && $parentWrapper !== null) { + $parentWrapper = $parentWrapper->getParent(); + } + + $str_replace = str_replace("&", "&", $parentWrapper->getFormAction()); + $this->setUploadUrl($str_replace . "&" . self::ASYNC_FILEUPLOAD . "=true"); + } + } + + + protected function handleSuffixes() { + if (!is_array($this->getSuffixes())) { + $this->setSuffixes(array()); + } + } + + + /** + * @param ILIAS\UI\Component\Dropzone\File\Standard $dropzone + * + * @return ILIAS\UI\Component\Dropzone\File\Standard + */ + protected function handleMaxFileSize($dropzone) { + if ($this->getMaxFilesize()) { + $dropzone = $dropzone->withFileSizeLimit($this->getMaxFilesize()); + } + + return $dropzone; + } +} diff --git a/Services/Form/js/ilFileStandardDropzoneInputGUI.js b/Services/Form/js/ilFileStandardDropzoneInputGUI.js new file mode 100644 index 000000000000..82fe8aac8ffd --- /dev/null +++ b/Services/Form/js/ilFileStandardDropzoneInputGUI.js @@ -0,0 +1,61 @@ +var ilFileStandardDropzoneInputGUI = (function ($) { + + var init = function (wrapper_id) { + var wrapper = $('#' + wrapper_id); + var uploadId = wrapper.find('.il-dropzone-base').attr('id'); + var form = wrapper.closest('form'); + var handledUpload = false; + var buttonName = form.find('input[type=submit]:first').attr('name'); + form.find('input[type=submit]').on("click", function () { + buttonName = $(this).attr('name'); + }); + + form.on('submit', function (event) { + if (handledUpload) { + return; + } + if ($(this)[0].checkValidity()) { + // If we have any files to upload, start uploading process prior to submitting form + if (il.UI.uploader.getUploads(uploadId).length) { + event.preventDefault(); + + var params = {}; + + $.each($(this).serializeArray(), function (_, kv) { + if (params.hasOwnProperty(kv.name)) { + params[kv.name] = $.makeArray(params[kv.name]); + params[kv.name].push(kv.value); + } else { + params[kv.name] = kv.value; + } + }); + + params[buttonName] = true; + + il.UI.uploader.setUploadParams(uploadId, params); + il.UI.uploader.onError(uploadId, function (xmlHttpRequest) { + handledUpload = true; + return false; + }); + il.UI.uploader.onAllUploadCompleted(uploadId, function () { + handledUpload = true; + return true; + }, function () { + handledUpload = true; + return false; + }); + + il.UI.uploader.upload(uploadId); + } + else { + handledUpload = true; + } + } + }); + }; + + return { + init: init + } + +})($); \ No newline at end of file diff --git a/Services/Style/System/data/data.json b/Services/Style/System/data/data.json index e1fe4674e7d3..fbe3e92b2f25 100755 --- a/Services/Style/System/data/data.json +++ b/Services/Style/System/data/data.json @@ -1 +1 @@ -{"FactoryUIComponent":{"id":"FactoryUIComponent","title":"UIComponent","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"What is to be done by this control","composition":"What happens if the control is operated","effect":"What happens if the control is operated","rivals":{"Rival 1":"What other controls are similar, what is their distinction"}},"background ":"Relevant academic information","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Where and when an element is to be used or not."},"composition":[],"interaction":{"2":"How the interaction with this object takes place."},"wording":{"3":"How the wording of labels or captions must be."},"ordering":{"5":"How different elements of this instance are to be ordered."},"style":{"4":"How this element should look like."},"responsiveness":{"6":"How this element behaves on changing screen sizes"},"accessibility":{"7":"How this element is made accessible"}},"parent":false,"children":["CounterFactoryCounter","GlyphFactoryGlyph","ButtonFactoryButton","CardCardCard","DeckDeckDeck","ListingFactoryListing","ImageFactoryImage","LegacyLegacyLegacy","PanelFactoryPanel","ModalFactoryModal","PopoverFactoryPopover","DividerFactoryDivider","LinkFactoryLink","DropdownFactoryDropdown","ItemFactoryItem","IconFactoryIcon","ViewControlFactoryViewControl","BreadcrumbsBreadcrumbsBreadcrumbs","ChartFactoryChart"],"less_variables":[],"path":"src\/UI\/Factory"},"CounterFactoryCounter":{"id":"CounterFactoryCounter","title":"Counter","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Counter inform users about the quantity of items indicated by a glyph.","composition":"Counters consist of a number and some background color and are placed one the 'end of the line' in reading direction of the item they state the count for.","effect":"Counters convey information, they are not interactive.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":["http:\/\/www.ilias.de\/docu\/goto_docu_wiki_wpage_3854_1357.html"],"rules":{"usage":{"1":"A counter MUST only be used in combination with a glyph."},"composition":{"1":"A counter MUST contain exactly one number greater than zero and no other characters."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["CounterCounterStatus","CounterCounterNovelty"],"less_variables":[],"path":"src\/UI\/Component\/Counter\/Factory"},"GlyphFactoryGlyph":{"id":"GlyphFactoryGlyph","title":"Glyph","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Glyphs map a generally known concept or symbol to a specific concept in ILIAS. Glyphs are used when space is scarce.","composition":"A glyph is a typographical character that represents something else. As any other typographical character, they can be manipulated by regular CSS. If hovered they change their background to indicate possible interactions.","effect":"Glyphs act as trigger for some action such as opening a certain Overlay type or as shortcut.","rivals":{"icon":"Standalone Icons are not interactive. Icons can be in an interactive container however. Icons merely serve as additional hint of the functionality described by some title. Glyphs are visually distinguished from object icons: they are monochrome."}},"background ":"\"In typography, a glyph is an elemental symbol within an agreed set of symbols, intended to represent a readable character for the purposes of writing and thereby expressing thoughts, ideas and concepts.\" (https:\/\/en.wikipedia.org\/wiki\/Glyph) Lidwell states that such symbols are used \"to improve the recognition and recall of signs and controls\".","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Glyphs MUST NOT be used in content titles.","2":"Glyphs MUST be used for cross-sectional functionality such as mail for example and NOT for representing objects.","3":"Glyphs SHOULD be used for very simple tasks that are repeated at many places throughout the system.","4":"Services such as mail MAY be represented by a glyph AND an icon."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"All Glyphs MUST be taken from the Bootstrap Glyphicon Halflings set. Exceptions MUST be approved by the JF."},"responsiveness":[],"accessibility":{"1":"The functionality triggered by the Glyph must be indicated to screen readers with by the attribute aria-label or aria-labelledby attribute."}},"parent":"FactoryUIComponent","children":["GlyphGlyphSettings","GlyphGlyphCollapse","GlyphGlyphExpand","GlyphGlyphAdd","GlyphGlyphRemove","GlyphGlyphUp","GlyphGlyphDown","GlyphGlyphBack","GlyphGlyphNext","GlyphGlyphSortAscending","GlyphGlyphSortDescending","GlyphGlyphUser","GlyphGlyphMail","GlyphGlyphNotification","GlyphGlyphTag","GlyphGlyphNote","GlyphGlyphComment"],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Factory"},"ButtonFactoryButton":{"id":"ButtonFactoryButton","title":"Button","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Buttons trigger interactions that change the system\u2019s status. Usually Buttons are contained in an Input Collection. The Toolbar is the main exception to this rule, since buttons in the Toolbar might also perform view changes.","composition":"Button is a clickable, graphically obtrusive control element. It can bear text.","effect":"On-click, the action indicated by the button is carried out.","rivals":{"glyph":"Glyphs are used if the enclosing Container Collection can not provide enough space for textual information or if such an information would clutter the screen.","links":"Links are used to trigger Interactions that do not change the systems status. They are usually contained inside a Navigational Collection."}},"background ":"Wording rules have been inspired by the iOS Human Interface Guidelines (UI-Elements->Controls->System Button) Style rules have been inspired from the GNOME Human Interface Guidelines->Buttons.","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Buttons MUST NOT be used inside a Textual Paragraph."},"composition":[],"interaction":{"1":"A Button SHOULD trigger an action. Only in Toolbars, Buttons MAY also change the view.","2":"If an action is temporarily not available, Buttons MUST be disabled by setting as type 'disabled'."},"wording":{"1":"The caption of a Button SHOULD contain no more than two words.","2":"The wording of the button SHOULD describe the action the button performs by using a verb or a verb phrase.","3":"Every word except articles, coordinating conjunctions and prepositions of four or fewer letters MUST be capitalized.","4":"For standard events such as saving or canceling the existing standard terms MUST be used if possible: Save, Cancel, Delete, Cut, Copy.","5":"There are cases where a non-standard label such as \u201cSend Mail\u201d for saving and sending the input of a specific form might deviate from the standard. These cases MUST however specifically justified."},"ordering":[],"style":{"1":"If Text is used inside a Button, the Button MUST be at least six characters wide."},"responsiveness":[],"accessibility":{"1":"DOM elements of type \"button\" MUST be used to properly identify an element as a Button if there is no good reason to do otherwise.","2":"Button DOM elements MUST either be of type \"button\", of type \"a\" accompanied with the aria-role \u201cButton\u201d or input along with the type attribute \u201cbutton\u201d or \"submit\"."}},"parent":"FactoryUIComponent","children":["ButtonStandardStandard","ButtonPrimaryPrimary","ButtonCloseClose","ButtonShyShy","ButtonMonthMonth","ButtonTagTag"],"less_variables":[],"path":"src\/UI\/Component\/Button\/Factory"},"CardCardCard":{"id":"CardCardCard","title":"Card","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A card is a flexible content container for small chunks of structured data. Cards are often used in so-called Decks which are a gallery of Cards.","composition":"Cards contain a header, which often includes an Image or Icon and a Title as well as possible actions as Default Buttons and 0 to n sections that may contain further textual descriptions, links and buttons.","effect":"Cards may contain Interaction Triggers.","rivals":{"Heading Panel":"Heading Panels fill up the complete available width in the Center Content Section. Multiple Heading Panels are stacked vertically.","Block Panels":"Block Panels are used in Sidebars"}},"background ":"","selector":"","feature_wiki_references ":["http:\/\/www.ilias.de\/docu\/goto_docu_wiki_wpage_3208_1357.html"],"rules":{"usage":[],"composition":{"1":"Cards MUST contain a title.","2":"Cards SHOULD contain an Image or Icon in the header section.","3":"Cards MAY contain Interaction Triggers."},"interaction":[],"wording":[],"ordering":[],"style":{"1":"Sections of Cards MUST be separated by Dividers."},"responsiveness":[],"accessibility":{"1":"If multiple Cards are used, they MUST be contained in a Deck."}},"parent":"FactoryUIComponent","children":[],"less_variables":[],"path":"src\/UI\/Component\/Card\/Card"},"DeckDeckDeck":{"id":"DeckDeckDeck","title":"Deck","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Decks are used to display multiple Cards in a grid. They should be used if a page contains many content items that have similar style and importance. A Deck gives each item equal horizontal space indicating that they are of equal importance.","composition":"Decks are composed only of Cards arranged in a grid. The cards displayed by decks are all of equal size. This Size ranges very small (XS) to very large (XL).","effect":"The Deck is a mere scaffolding element, is has no effect.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":["http:\/\/www.ilias.de\/docu\/goto_docu_wiki_wpage_3992_1357.html"],"rules":{"usage":{"1":"Decks MUST only be used to display multiple Cards."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"The number of cards displayed per row MUST adapt to the screen size."},"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":[],"less_variables":[],"path":"src\/UI\/Component\/Deck\/Deck"},"ListingFactoryListing":{"id":"ListingFactoryListing","title":"Listing","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Listings are used to structure itemised textual information.","composition":"Listings may contain ordered, unordered, or labeled items.","effect":"Listings hold only textual information. They may contain Links but no Buttons.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Listings MUST NOT contain Buttons."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["ListingUnorderedUnordered","ListingOrderedOrdered","ListingDescriptiveDescriptive"],"less_variables":[],"path":"src\/UI\/Component\/Listing\/Factory"},"ImageFactoryImage":{"id":"ImageFactoryImage","title":"Image","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Image component is used to display images of various sources.","composition":"An Image is composed of the image and an alternative text for screen readers.","effect":"Images may be included in interacted components but not interactive on their own.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"Images MUST contain the alt attribute. This attribute MAY be left empty (alt=\"\") if the image is of decorative nature. According to the WAI, decorative images don\u2019t add information to the content of a page. For example, the information provided by the image might already be given using adjacent text, or the image might be included to make the website more visually attractive (see https:\/\/www.w3.org\/WAI\/tutorials\/images\/decorative\/<\/a>)."}},"parent":"FactoryUIComponent","children":["ImageImageStandard","ImageImageResponsive"],"less_variables":[],"path":"src\/UI\/Component\/Image\/Factory"},"LegacyLegacyLegacy":{"id":"LegacyLegacyLegacy","title":"Legacy","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"This component is used to wrap an existing ILIAS UI element into a UI component. This is useful if a container of the UI components needs to contain content that is not yet implement in the centralized UI components.","composition":"The legacy component contains html or any other content as string.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"This component MUST only be used to ensure backwards compatibility with existing UI elements in ILIAS, therefore it SHOULD only contain Elements which cannot be generated using other UI Components from the UI Service."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":[],"less_variables":[],"path":"src\/UI\/Component\/Legacy\/Legacy"},"PanelFactoryPanel":{"id":"PanelFactoryPanel","title":"Panel","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Panels are used to group titled content.","composition":"Panels consist of a header and content section. They form one Gestalt and so build a perceivable cluster of information.","effect":"The effect of interaction with panels heavily depends on their content.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":{"1":"Panels MUST contain a title."},"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["PanelStandardStandard","PanelSubSub","PanelReportReport","PanelListingFactoryListing"],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Factory"},"ModalFactoryModal":{"id":"ModalFactoryModal","title":"Modal","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Modal forces users to focus on the task at hand.","composition":"A Modal is a full-screen dialog on top of the greyed-out ILIAS screen. The Modal consists of a header with a close button and a typography modal title, a content section and might have a footer.","effect":"All controls of the original context are inaccessible until the Modal is completed. Upon completion the user returns to the original context.","rivals":{"1":"Modals have some relations to popovers. The main difference between the two is the disruptive nature of the Modal and the larger amount of data that might be displayed inside a modal. Also popovers perform mostly action to add or consult metadata of an item while Modals manipulate or focus items or their sub-items directly."}},"background ":"http:\/\/quince.infragistics.com\/Patterns\/Modal%20Panel.aspx","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The main purpose of the Modals MUST NOT be navigational. But Modals MAY be dialogue of one or two steps and thus encompass \"next\"-buttons or the like.","2":"Modals MUST NOT contain other modals (Modal in Modal).","3":"Modals SHOULD not be used to perform complex workflows.","4":"Modals MUST be closable by a little \u201cx\u201d-button on the right side of the header.","5":"Modals MUST contain a title in the header."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["ModalInterruptiveInterruptive","InterruptiveItemInterruptiveItem","ModalRoundTripRoundtrip","ModalLightboxLightbox","LightboxImagePageLightboxImagePage"],"less_variables":[],"path":"src\/UI\/Component\/Modal\/Factory"},"PopoverFactoryPopover":{"id":"PopoverFactoryPopover","title":"Popover","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Popovers can be used when space is scarce i.e. within List GUI items, table cells or menus in the Header section. They offer either secondary information on object like a preview or rating to be displayed or entered. They display information about ongoing processes","composition":"Popovers consist of a layer displayed above all other content. The content of the Popover depends on the functionality it performs. A Popover MAY display a title above its content. All Popovers contain a pointer pointing from the Popover to the Triggerer of the Popover.","effect":"Popovers are shown by clicking a Triggerer component such as a Button or Glyph. The position of the Popover is calculated automatically be default. However, it is possible to specify if the popover appears horizontal (left, right) or vertical (top, bottom) relative to its Triggerer component. Popovers disappear by clicking anywhere outside the Popover or by pressing the ESC key.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Popovers MUST NOT contain horizontal scrollbars.","2":"Popovers MAY contain vertical scrollbars. The content component is responsible to define its own height and show vertical scrollbars.","3":"If Popovers are used to present secondary information of an object, they SHOULD display a title representing the object."},"composition":[],"interaction":{"1":"A Popover MUST only be displayed if the Trigger component is clicked. This behaviour is different from Tooltips that appear on hovering. Popovers disappear by clicking anywhere outside the Popover or by pressing the ESC key."},"wording":[],"ordering":[],"style":{"1":"Popovers MUST always relate to the Trigger component by a little pointer."},"responsiveness":[],"accessibility":{"1":"There MUST be a way to open the Popover by only using the keyboard.","2":"The focus MUST be inside the Popover, once it is open if it contains at least one interactive item. Otherwise the focus MUST remain on the Triggerer component.","3":"The focus MUST NOT leave the Popover for as long as it is open.","4":"There MUST be a way to reach every control in the Popover by only using the keyboard.","5":"The Popover MUST be closable by pressing the ESC key.","6":"Once the Popover is closed, the focus MUST return to the element triggering the opening of the Popover or the element being clicked if the Popover was closed on click."}},"parent":"FactoryUIComponent","children":["PopoverStandardStandard","PopoverListingListing"],"less_variables":[],"path":"src\/UI\/Component\/Popover\/Factory"},"DividerFactoryDivider":{"id":"DividerFactoryDivider","title":"Divider","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A divider marks a thematic change in a sequence of other components. A Horizontal Divider is used to mark a thematic change in sequence of elements that are stacked from top to bottom, e.g. in a Dropdown. A Vertical Divider is used to mark a thematic change in a sequence of elements that are lined up from left to right, e.g. a Toolbar.","composition":"","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Dividers MUST only be used in container components that explicitly state and define the usage of Dividers within the container."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["DividerHorizontalHorizontal"],"less_variables":[],"path":"src\/UI\/Component\/Divider\/Factory"},"LinkFactoryLink":{"id":"LinkFactoryLink","title":"Link","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Links are used navigate to other resources or views of the system. Clicking on a link does not change the systems status.","composition":"Link is a clickable, graphically minimal obtrusive control element. It can bear text or other content. Links always contain a valid href tag which should not not just contain a hash sign.","effect":"On-click, the resource or view indicated by the link is requested and presented. Links are not used to trigger Javascript events.","rivals":{"buttons":"Buttons are used to trigger Interactions that usually change the systems status. Buttons are much more obtrusive than links and may fire JS events."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Links MAY be used inline in a Textual Paragraphs."},"composition":[],"interaction":{"1":"Hovering an active link should indicate a possible interaction.","2":"Links MUST not be used to fire Javascript events."},"wording":{"1":"The wording of the link SHOULD name the target view or resource."},"ordering":[],"style":{"1":"Links SHOULD not be presented with a separate background color."},"responsiveness":[],"accessibility":{"1":"DOM elements of type \"a\" MUST be used to properly identify an element."}},"parent":"FactoryUIComponent","children":["LinkStandardStandard"],"less_variables":[],"path":"src\/UI\/Component\/Link\/Factory"},"DropdownFactoryDropdown":{"id":"DropdownFactoryDropdown","title":"Dropdown","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Dropdowns reveal a list of interactions that change the system\u2019s status or navigate to a different view.","composition":"Dropdown is a clickable, graphically obtrusive control element. It can bear text. On-click a list of Shy Buttons and optional Dividers is shown.","effect":"On-click, a list of actions is revealed. Clicking an item will trigger the action indicated. Clicking outside of an opened Dropdown will close the list of items.","rivals":{"button":"Buttons are used, if single actions should be presented directly in the user interface.","links":"Links are used to trigger actions that do not change the systems status. They are usually contained inside a Navigational Collection.","popovers":"Dropdowns only provide a list of possible actions. Popovers can include more diverse and flexible content."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Dropdowns MUST NOT be used standalone. They are only parts of more complex UI elements. These elements MUST define their use of Dropdown. E.g. a List or a Table MAY define that a certain kind of Dropdown is used as part of the UI element."},"composition":[],"interaction":{"1":"Only Dropdown Items MUST trigger an action or change a view. The Dropdown trigger element is only used to show and hide the list of Dropdown Items."},"wording":{"1":"The label of a Dropdown SHOULD contain no more than two words.","2":"Every word except articles, coordinating conjunctions and prepositions of four or fewer letters MUST be capitalized.","3":"For standard events such as saving or canceling the existing standard terms MUST be used if possible: Delete, Cut, Copy.","4":"There are cases where a non-standard label such as \u201cSend Mail\u201d for saving and sending the input of a specific form might deviate from the standard. These cases MUST however specifically justified."},"ordering":[],"style":{"1":"If Text is used inside a Dropdown label, the Dropdown MUST be at least six characters wide."},"responsiveness":[],"accessibility":{"1":"DOM elements of type \"button\" MUST be used to properly identify an element as a Dropdown.","2":"Dropdown items MUST be implemented as \"ul\" list with a set of \"li\" elements and nested Shy Button elements for the actions.","3":"Triggers of Dropdowns MUST indicate their effect by the aria-haspopup attribute set to true.","4":"Triggers of Dropdowns MUST indicate the current state of the Dropdown by the aria-expanded label.","5":"Dropdowns MUST be accessible by keyboard by focusing the trigger element and clicking the return key.","6":"Entries in a Dropdown MUST be accessible by the tab-key if opened.","7":"The focus MAY leave the Dropdown if tab is pressed while focusing the last element. This differs from the behaviour in Popovers and Modals."}},"parent":"FactoryUIComponent","children":["DropdownStandardStandard"],"less_variables":[],"path":"src\/UI\/Component\/Dropdown\/Factory"},"ItemFactoryItem":{"id":"ItemFactoryItem","title":"Item","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"An item displays a unique entity within the system. It shows information about that entity in a structured way.","composition":"Items contain the name of the entity as a title. The item contains three sections, where one section contains important information about the item, the second section shows the content of the item and another section shows metadata about the entity.","effect":"Items may contain Interaction Triggers such as Glyphs, Buttons or Tags.","rivals":{"Card":"Cards define the look of items in a deck. Todo: We need to refactor cards."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Items MUST contain the name of the displayed entity as a title.","2":"Items SHOULD contain a section with it's content.","3":"Items MAY contain Interaction Triggers.","4":"Items MAY contain a section with metadata."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["ItemStandardStandard","ItemGroupGroup"],"less_variables":[],"path":"src\/UI\/Component\/Item\/Factory"},"IconFactoryIcon":{"id":"IconFactoryIcon","title":"Icon","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Icons are quickly comprehensible and recognizable graphics. They indicate the functionality or nature of a text-element or context: Icons will mainly be used in front of object-titles, e.g. in the header, the tree and in repository listing.","composition":"Icons come in three fixed sizes: small, medium and large. They can be configured with an additional \"abbreviation\", a text of a few characters that will be rendered on top of the image.","effect":"Icons themselves are not interactive; however they are allowed within interactive containers.","rivals":{"1":"Glyphs are typographical characters that act as a trigger for some action.","2":"Images belong to the content and can be purely decorative."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Icons MUST be used to represent objects or context.","2":"Icons MUST be used in combination with a title or label.","3":"An unique Icon MUST always refer to the same thing."},"composition":[],"interaction":[],"wording":{"1":"The aria-label MUST state the represented object-type.","2":"The abbreviation SHOULD consist of one or two letters."},"ordering":[],"style":{"1":"Icons MUST have a class indicating their usage.","2":"Icons MUST be tagged with a CSS-class indicating their size."},"responsiveness":[],"accessibility":{"1":"Icons MUST use aria-label."}},"parent":"FactoryUIComponent","children":["IconStandardStandard","IconCustomCustom"],"less_variables":[],"path":"src\/UI\/Component\/Icon\/Factory"},"ViewControlFactoryViewControl":{"id":"ViewControlFactoryViewControl","title":"View Control","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"View Controls switch between different visualisation of data.","composition":"View Controls are composed mainly of buttons, they are often found in toolbars.","effect":"Interacting with a view control changes to display in some content area.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"FactoryUIComponent","children":["ViewControlModeMode","ViewControlSectionSection"],"less_variables":[],"path":"src\/UI\/Component\/ViewControl\/Factory"},"BreadcrumbsBreadcrumbsBreadcrumbs":{"id":"BreadcrumbsBreadcrumbsBreadcrumbs","title":"Breadcrumbs","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Breadcrumbs is a supplemental navigation scheme. It eases the user's navigation to higher items in hierarchical structures. Breadcrumbs also serve as an effective visual aid indicating the user's location on a website.","composition":"Breadcrumbs-entries are rendered as horizontally arranged UI Links with a seperator in-between.","effect":"Clicking on an entry will get the user to the respective location.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Crumbs MUST trigger navigation to other resources of the system."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":[],"less_variables":[],"path":"src\/UI\/Component\/Breadcrumbs\/Breadcrumbs"},"ChartFactoryChart":{"id":"ChartFactoryChart","title":"Chart","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Charts are used to graphically represent data in various forms such as maps, graphs or diagrams.","composition":"Charts are composed of various graphical and textual elements representing the raw data.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"Charts SHOULD not rely on colors to convey information."},"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["ChartScaleBarScaleBar"],"less_variables":[],"path":"src\/UI\/Component\/Chart\/Factory"},"CounterCounterStatus":{"id":"CounterCounterStatus","title":"Status","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Status counter is used to display information about the total number of some items like users active on the system or total number of comments.","composition":"The Status Counter is a non-obstrusive Counter.","effect":"Status Counters convey information, they are not interactive.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"The Status Counter MUST be displayed on the lower right of the item it accompanies.","2":"The Status Counter SHOULD have a non-obstrusive background color, such as grey."},"responsiveness":[],"accessibility":[]},"parent":"CounterFactoryCounter","children":[],"less_variables":[],"path":"src\/UI\/Component\/Counter\/Counter"},"CounterCounterNovelty":{"id":"CounterCounterNovelty","title":"Novelty","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Novelty counters inform users about the arrival or creation of new items of the kind indicated by the accompanying glyph.","composition":"A Novelty Counter is an obtrusive counter.","effect":"They count down \/ disappear as soon as the change has been consulted by the user.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Novelty Counter MAY be used with the Status Counter."},"composition":[],"interaction":{"2":"There MUST be a way for the user to consult the changes indicated by the counter.","3":"After the consultation, the Novelty Counter SHOULD disappear or the number it contains is reduced by one.","4":"Depending on the content, the reduced number MAY be added in an additional Status Counter."},"wording":[],"ordering":[],"style":{"5":"The Novelty Counter MUST be displayed on the top at the 'end of the line' in reading direction of the item it accompanies. This would be top right for latin script and top left for arabic script.","6":"The Novelty Counter SHOULD have an obstrusive background color, such as red or orange."},"responsiveness":[],"accessibility":[]},"parent":"CounterFactoryCounter","children":[],"less_variables":[],"path":"src\/UI\/Component\/Counter\/Counter"},"GlyphGlyphSettings":{"id":"GlyphGlyphSettings","title":"Settings","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Settings Glyph triggers opening a Dropdown to edit settings of the displayed block.","composition":"The Settings Glyph uses the glyphicon-cog.","effect":"Upon clicking a settings Dropdown is opened.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Settings Glyph MUST only be used in Blocks."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u201cSettings\u201d."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphCollapse":{"id":"GlyphGlyphCollapse","title":"Collapse","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Collapse Glyph is used to trigger the collapsing of some neighbouring Container Collection such as a the content of a Dropdown or an Accordion currently shown.","composition":"The Collapse Glyph is composed of a triangle pointing to the bottom indicating that content is currently shown.","effect":"Clicking the Collapse Glyph hides the display of some Container Collection.","rivals":{"Expand Glyph":"The Expand Glyphs triggers the display of some Container Collection.","Previous Glyph":"The Previous\/Next Glyph opens a completely new view. It serves a navigational purpose."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Collapse Glyph MUST indicate if the toggled Container Collection is visible or not."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Collapse Content'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphExpand":{"id":"GlyphGlyphExpand","title":"Expand","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Expand Glyph is used to trigger the display of some neighbouring Container Collection such as a the content of a Dropdown or an Accordion currently shown.","composition":"The Expand Glyph is composed of a triangle pointing to the right indicating that content is currently shown.","effect":"Clicking the Expand Glyph displays some Container Collection.","rivals":{"Collapse Glyph":"The Collapse Glyphs hides the display of some Container Collection.","Previous Glyph":"The Previous\/Next Glyph opens a completely new view. It serves a navigational purpose."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Expand Glyph MUST indicate if the toggled Container Collection is visible or not."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Expand Content'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphAdd":{"id":"GlyphGlyphAdd","title":"Add","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The glyphed add-button serves as stand-in for the respective textual buttons in very crowded screens. It allows adding a new item.","composition":"The Add Glyph uses the glyphicon-plus-sign.","effect":"Clicking on the Add Glyph adds a new input to a form or an event to the calendar.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Add Glyph SHOULD not come without a Remove Glyph and vice versa. Because either there is not enough place for textual buttons or there is place. Exceptions to this rule, such as the Calendar, where only elements can be added in a certain place are possible, are to be run through the Jour Fixe.","2":"The Add Glyph stands for an Action and SHOULD be placed in the action column of a form.","3":"The Add Glyph MUST not be used to add lines to tables."},"composition":[],"interaction":{"1":"Newly added items MUST be placed below the line in which the Add Glyph has been clicked"},"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Add'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphRemove":{"id":"GlyphGlyphRemove","title":"Remove","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Remove Glyph serves as stand-in for the respective textual buttons in very crowded screens. It allows removing an item.","composition":"The Remove Glyph uses the glyphicon-plus-sign.","effect":"Clicking on the Remove Glyph adds a new input to a form or an event to the calendar.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Remove Glyph SHOULD not come without a glyphed Add Glyph and vice versa. Because either there is not enough place for textual buttons or there is place. Exceptions to this rule, such as the Calendar, where only elements can be added in a certain place are possible, are to be run through the Jour Fixe.","2":"The Remove Glyph stands for an Action and SHOULD be placed in the action column of a form.","3":"The Remove Glyph MUST not be used to add lines to tables."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Remove'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphUp":{"id":"GlyphGlyphUp","title":"Up","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Up Glyph allows for manually arranging rows in tables embedded in forms. It allows moving an item up.","composition":"The Up Glyph uses the glyphicon-circle-arrow-up. The Up Glyph can be combined with the Add\/Remove Glyph.","effect":"Clicking on the Up Glyph moves an item up.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":["http:\/\/www.ilias.de\/docu\/goto_docu_wiki_wpage_813_1357.html"],"rules":{"usage":{"1":"The Up Glyph MUST NOT be used to sort tables. There is an established sorting control for that.","2":"The Up Glyph SHOULD not come without a Down and vice versa.","3":"The Up Glyph is an action and SHOULD be listed in the action column of a form."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Up'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphDown":{"id":"GlyphGlyphDown","title":"Down","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Down Glyph allows for manually arranging rows in tables embedded in forms. It allows moving an item down.","composition":"The Down Glyph uses the glyphicon-circle-arrow-down. The Down Glyph can be combined with the Add\/Remove Glyph.","effect":"Clicking on the Down Glyph moves an item up.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":["http:\/\/www.ilias.de\/docu\/goto_docu_wiki_wpage_813_1357.html"],"rules":{"usage":{"1":"The Down Glyph MUST NOT be used to sort tables. There is an established sorting control for that.","2":"The Down Glyph SHOULD not come without a Up and vice versa.","3":"The Down Glyph is an action and SHOULD be listed in the action column of a form."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Down'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphBack":{"id":"GlyphGlyphBack","title":"Back","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Back Glyph indicates a possible change of the view. The view change leads back to some previous view.","composition":"The chevron-left glyphicon is used.","effect":"The click on a Back Glyph leads back to a previous view.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Back and Next Buttons MUST be accompanied by the respective Back\/Next Glyph."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"If clicking on the Back\/Next GLYPH opens a new view of an object, the Next Glyph MUST be used.","2":"If clicking on the Back\/Next GLYPH opens a previous view of an object, the Back Glyph MUST be used."},"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Back'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphNext":{"id":"GlyphGlyphNext","title":"Next","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Next Glyph indicates a possible change of the view. The view change leads back to some previous view.","composition":"The chevron-right glyphicon is used.","effect":"The click on a Next Glyph opens a new view.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Back and Next Buttons MUST be accompanied by the respective Back\/Next Glyph."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"If clicking on the Back\/Next GLYPH opens a new view of an object, the Next Glyph MUST be used.","2":"If clicking on the Back\/Next GLYPH opens a previous view of an object, the Back Glyph MUST be used."},"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Next'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphSortAscending":{"id":"GlyphGlyphSortAscending","title":"Sort Ascending","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Sorting Glyphs indicate the sorting direction of a column in a table as ascending (up) or descending (down). It is a toggle reversing the ordering of a column.","composition":"The Sort Ascending Glyph uses glyphicon-arrow-up.","effect":"Clicking the Sort Ascending Glyph reverses the direction of ordering in a table.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Sort Ascending'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphSortDescending":{"id":"GlyphGlyphSortDescending","title":"Sort Descending","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Sorting Glyphs indicate the sorting direction of a column in a table as ascending (up) or descending (down). It is a toggle reversing the ordering of a column.","composition":"The Sort Descending Glyph uses glyphicon-arrow-descending.","effect":"Clicking the Sort Descending Glyph reverses the direction of ordering in a table.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Sort Descending'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphUser":{"id":"GlyphGlyphUser","title":"User","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The User Glyph triggers the \u201cWho is online?\u201d Popover in the Top Navigation. The User Glyph indicates the number of pending contact requests and users online via the the Novelty Counter and Status Counter respectively.","composition":"The User Glyph uses the glyphicon-user.","effect":"Clicking the User Glyph opens the \u201cWho is online?\u201d Popover.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Show who is online'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphMail":{"id":"GlyphGlyphMail","title":"Mail","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Mail Glyph provides a shortcut to the mail service. The Mail Glyph indicates the number of new mails received.","composition":"The Mail Glyph uses the glyphicon-envelope.","effect":"Upon clicking on the Mail Glyph the user is transferred to the full-screen mail service.","rivals":{"Mail Icon":"The Mail Icon is used to indicate the user is currently located in the Mail service The Mail Glyph acts as shortcut to the Mail service."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Mail'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphNotification":{"id":"GlyphGlyphNotification","title":"Notification","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Notification Glyph allows users to activate \/ deactivate the notification service for a specific object or sub-item. It is a toggle indicating by colour whether it is activated or not.","composition":"The Notification Glyph uses the glyphicon-bell in link-color if notifications are not active or brand-warning color if they are.","effect":"Upon clicking the notification activation is toggled: Clicking the Notification Glyph activates respectively deactivates the notification service for the current object or sub-item.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Notification Glyph MUST only be used in the Content Top Actions."},"composition":[],"interaction":{"1":"Clicking the Notification Glyph MUST toggle the activation of Notifications."},"wording":[],"ordering":[],"style":{"1":"If notifications are activated the Notification Glyph MUST use the brand-warning color."},"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Notifications'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphTag":{"id":"GlyphGlyphTag","title":"Tag","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Tag Glyph is used to indicate the possibility of adding tags to an object.","composition":"The Tag Glyph uses the glyphicon-tag.","effect":"Upon clicking the Round Trip Modal to add new Tags is opened.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Novelty and Status Counter MUST show the amount of tags that has been given for an specific object."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Tags'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphNote":{"id":"GlyphGlyphNote","title":"Note","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Note Glyph is used to indicate the possibilty of adding notes to an object.","composition":"The Note Glyph uses the glyphicon-pushpin.","effect":"Upon clicking the Round Trip Modal to add new notes is opened","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Novelty and Status Counter MUST show the amount of notes that has been given for an specific object."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Notes'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphComment":{"id":"GlyphGlyphComment","title":"Comment","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Comment Glyph is used to indicate the possibilty of adding comments to an object.","composition":"The Comment Glyph uses the glyphicon-comment.","effect":"Upon clicking the Round Trip Modal to add new comments is opened.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Novelty and Status Counter MUST show the amount of comments that has been given for an specific object."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Comments'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"ButtonStandardStandard":{"id":"ButtonStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The standard button is the default button to be used in ILIAS. If there is no good reason using another button instance in ILIAS, this is the one that should be used.","composition":"The standard button uses the primary color as background.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Standard buttons MUST be used if there is no good reason using another instance."},"composition":[],"interaction":[],"wording":[],"ordering":{"1":"The most important standard button SHOULD be first in reading direction if there are several buttons.","2":"In the toolbar and in forms special regulations for the ordering of the buttons MAY apply."},"style":[],"responsiveness":{"1":"The most important standard button in multi-action bars MUST be sticky (stay visible on small screens)."},"accessibility":{"1":"Standard buttons MAY define aria-label attribute. Use it in cases where a text label is not visible on the screen or when the label does not provide enough information about the action.","2":"Standard buttons MAY define aria-checked attribute. Use it to inform which is the currently active button."}},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Standard"},"ButtonPrimaryPrimary":{"id":"ButtonPrimaryPrimary","title":"Primary","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The primary button indicates the most important action on a screen. By definition there can only be one single \u201cmost important\u201d action on any given screen and thus only one single primary button per screen.","composition":"The background color is the btn-primary-color. This screen-unique button-color ensures that it stands out and attracts the user\u2019s attention while there are several buttons competing for attention.","effect":"In toolbars the primary button are required to be sticky, meaning they stay in view in the responsive view.","rivals":[]},"background ":"Tiddwell refers to the primary button as \u201cprominent done button\u201d and describes that \u201cthe button that finishes a transaction should be placed at the end of the visual flow; and is to be made big and well labeled.\u201d She explains that \u201cA well-understood, obvious last step gives your users a sense of closure. There\u2019s no doubt that the transaction will be done when that button is clicked; don\u2019t leave them hanging, wondering whether their work took effect\u201d. The GNOME Human Interface Guidelines -> Buttons also describes a button indicated as most important for dialogs.","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Most pages SHOULD NOT have any Primary Button at all.","2":"There MUST be no more than one Primary Button per page in ILIAS.","3":"The decision to make a Button a Primary Button MUST be confirmed by the JF."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Primary"},"ButtonCloseClose":{"id":"ButtonCloseClose","title":"Close","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The close button triggers the closing of some collection displayed temporarily such as an overlay.","composition":"The close button is displayed without border.","effect":"Clicking the close button closes the enclosing collection.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":{"1":"The Close Button MUST always be positioned in the top right of a collection."},"style":[],"responsiveness":[],"accessibility":{"1":"The functionality of the close button MUST be indicated for screen readers by an aria-label."}},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Close"},"ButtonShyShy":{"id":"ButtonShyShy","title":"Shy","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Shy buttons are used in contexts that need a less obtrusive presentation than usual buttons have, e.g. in UI collections like Dropdowns.","composition":"Shy buttons do not come with a separte background color.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Shy buttons MUST only be used, if a standard button presentation is not appropriate. E.g. if usual buttons destroy the presentation of an outer UI component or if there is not enough space for a standard button presentation."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Shy"},"ButtonMonthMonth":{"id":"ButtonMonthMonth","title":"Month","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Month Button enables to select a specific month to fire some action (probably a change of view).","composition":"The Month Button is composed of a Button showing the default month directly (probably the month currently rendered by some view). A dropdown contains an interface enabling the selection of a month from the future or the past.","effect":"Selecting a month from the dropdown directly fires the according action (e.g. switching the view to the selected month). Technically this is currently a Javascript event being fired.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":{"1":"Selecting a month from the dropdown MUST directly fire the according action."},"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Month"},"ButtonTagTag":{"id":"ButtonTagTag","title":"Tag","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Tags classify entities. Thus, their primary purpose is the visualization of those classifications for one entity. However, tags are usually clickable - either to edit associations or list related entities, i.e. objects with the same tag.","composition":"Tags are a colored area with text on it. When used in a tag-cloud (a list of tags), tags can be visually \"weighted\" according to the number of their occurences, be it with different (font-)sizes, different colors or all of them.","effect":"Tags may trigger an action or change the view when clicked. There is no visual difference (besides the cursor) between clickable tags and tags with unavailable action.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"Tags SHOULD be used with an additonal class to adjust colors.","2":"The font-color SHOULD be set with high contrast to the chosen background color."},"responsiveness":[],"accessibility":{"1":"The functionality of the tag button MUST be indicated for screen readers by an aria-label."}},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Tag"},"ListingUnorderedUnordered":{"id":"ListingUnorderedUnordered","title":"Unordered","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Unordered Lists are used to display a unordered set of textual elements.","composition":"Unordered Lists are composed of a set of bullets labeling the listed items.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ListingFactoryListing","children":[],"less_variables":[],"path":"src\/UI\/Component\/Listing\/Unordered"},"ListingOrderedOrdered":{"id":"ListingOrderedOrdered","title":"Ordered","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Ordered Lists are used to displayed a numbered set of textual elements. They are used if the order of the elements is relevant.","composition":"Ordered Lists are composed of a set of numbers labeling the items enumerated.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ListingFactoryListing","children":[],"less_variables":[],"path":"src\/UI\/Component\/Listing\/Ordered"},"ListingDescriptiveDescriptive":{"id":"ListingDescriptiveDescriptive","title":"Descriptive","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Descriptive Lists are used to display key-value doubles of textual-information.","composition":"Descriptive Lists are composed of a key acting as title describing the type of information being displayed underneath.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ListingFactoryListing","children":[],"less_variables":[],"path":"src\/UI\/Component\/Listing\/Descriptive"},"ImageImageStandard":{"id":"ImageImageStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The standard image is used if the image is to be rendered in it's the original size.","composition":"","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ImageFactoryImage","children":[],"less_variables":[],"path":"src\/UI\/Component\/Image\/Image"},"ImageImageResponsive":{"id":"ImageImageResponsive","title":"Responsive","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A responsive image is to be used if the image needs to adapt to changing amount of space available.","composition":"Responsive images scale nicely to the parent element.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ImageFactoryImage","children":[],"less_variables":[],"path":"src\/UI\/Component\/Image\/Image"},"PanelStandardStandard":{"id":"PanelStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Standard Panels are used in the Center Content section to group content.","composition":"Standard Panels consist of a title and a content section. The structure of this content might be varying from Standard Panel to Standard Panel. Standard Panels may contain Sub Panels.","effect":"","rivals":{"Cards":"Often Cards are used in Decks to display multiple uniformly structured chunks of Data horizontally and vertically."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"In Forms Standard Panels MUST be used to group different sections into Form Parts.","2":"Standard Panels SHOULD be used in the Center Content as primary Container for grouping content of varying content."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PanelFactoryPanel","children":[],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Standard"},"PanelSubSub":{"id":"PanelSubSub","title":"Sub","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Sub Panels are used to structure the content of Standard panels further into titled sections.","composition":"Sub Panels consist of a title and a content section. They may contain a Card on their right side to display meta information about the content displayed.","effect":"","rivals":{"Standard Panel":"The Standard Panel might contain a Sub Panel.","Card":"The Sub Panels may contain one Card."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Sub Panels MUST only be inside Standard Panels"},"composition":{"1":"Sub Panels MUST NOT contain Sub Panels or Standard Panels as content."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PanelFactoryPanel","children":[],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Sub"},"PanelReportReport":{"id":"PanelReportReport","title":"Report","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Report Panels display user-generated data combining text in lists, tables and sometimes charts. Report Panels always draw from two distinct sources: the structure \/ scaffolding of the Report Panels stems from user-generated content (i.e a question of a survey, a competence with levels) and is filled with user-generated content harvested by that very structure (i.e. participants\u2019 answers to the question, self-evaluation of competence).","composition":"They are composed of a Standard Panel which contains several Sub Panels. They might also contain a card to display information meta information in their first block.","effect":"Report Panels are predominantly used for displaying data. They may however comprise links or buttons.","rivals":{"Standard Panels":"The Report Panels contains sub panels used to structure information."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Report Panels SHOULD be used when user generated content of two sources (i.e results, guidelines in a template) is to be displayed alongside each other."},"composition":[],"interaction":{"1":"Links MAY open new views.","2":"Buttons MAY trigger actions or inline editing."},"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PanelFactoryPanel","children":[],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Report"},"PanelListingFactoryListing":{"id":"PanelListingFactoryListing","title":"Listing","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Listing Panels are used to list items following all one single template.","composition":"Listing Panels are composed of several titled Item Groups. They further may contain a filter.","effect":"The List Items of Listing Panels may contain a dropdown offering options to interact with the item. Further Listing Panels may be filtered and the number of sections or items to be displayed may be configurable.","rivals":{"Report Panels":"Report Panels contain sections as Sub Panels each displaying different aspects of one item."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Listing Panels SHOULD be used, if a large number of items using the same template are to be displayed in an inviting way not using a Table."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PanelFactoryPanel","children":["PanelListingStandardStandard"],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Listing\/Factory"},"PanelListingStandardStandard":{"id":"PanelListingStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Standard item lists present lists of items with similar presentation. All items are passed by using Item Groups.","composition":"This Listing is composed of title and a set of Item Groups. Additionally an optional dropdown to select the number\/types of items to be shown at the top of the Listing.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"PanelListingFactoryListing","children":[],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Listing\/Standard"},"ModalInterruptiveInterruptive":{"id":"ModalInterruptiveInterruptive","title":"Interruptive","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"An Interruptive modal disrupts the user in critical situation, forcing him or her to focus on the task at hand.","composition":"The modal states why this situation needs attention and may point out consequences.","effect":"All controls of the original context are inaccessible until the modal is completed. Upon completion the user returns to the original context.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Due to the heavily disruptive nature of this type of modal it MUST be restricted to critical situations (e.g. loss of data).","2":"All actions where data is deleted from the system are considered to be critical situations and SHOULD be implemented as an Interruptive modal. Exceptions are possible if items from lists in forms are to be deleted or if the modal would heavily disrupt the workflow.","3":"Interruptive modals MUST contain a primary button continuing the action that initiated the modal (e.g. Delete the item) on the left side of the footer of the modal and a default button canceling the action on the right side of the footer.","4":"The cancel button in the footer and the close button in the header MUST NOT perform any additional action than closing the Interruptive modal."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ModalFactoryModal","children":[],"less_variables":[],"path":"src\/UI\/Component\/Modal\/Interruptive"},"InterruptiveItemInterruptiveItem":{"id":"InterruptiveItemInterruptiveItem","title":"Interruptive Item","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Interruptive items are displayed in an Interruptive modal and represent the object(s) being affected by the critical action, e.g. deleting.","composition":"An Interruptive item is composed of an Id, title, description and an icon.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"An interruptive item MUST have an ID and title.","2":"An interruptive item SHOULD have an icon representing the affected object.","3":"An interruptive item MAY have a description which helps to further identify the object. If an Interruptive modal displays multiple items having the the same title, the description MUST be used in order to distinct these objects from each other.","4":"If an interruptive item represents an ILIAS object, e.g. a course, then the Id, title, description and icon of the item MUST correspond to the Id, title, description and icon from the ILIAS object."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ModalFactoryModal","children":[],"less_variables":[],"path":"InterruptiveItem"},"ModalRoundTripRoundtrip":{"id":"ModalRoundTripRoundtrip","title":"Roundtrip","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Round-Trip modals are to be used if the context would be lost by performing this action otherwise. Round-Trip modals accommodate sub-workflows within an overriding workflow. The Round-Trip modal ensures that the user does not leave the trajectory of the overriding workflow. This is typically the case if an ILIAS service is being called while working in an object.","composition":"Round-Trip modals are completed by a well defined sequence of only a few steps that might be displayed on a sequence of different modals connected through some \"next\" button.","effect":"Round-Trip modals perform sub-workflow involving some kind of user input. Sub-workflow is completed and user is returned to starting point allowing for continuing the overriding workflow.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Round-Trip modals MUST contain at least two buttons at the bottom of the modals: a button to cancel (right) the workflow and a button to finish or reach the next step in the workflow (left).","2":"Round-Trip modals SHOULD be used, if the user would lose the context otherwise. If the action can be performed within the same context (e.g. add a post in a forum, edit a wiki page), a Round-Trip modal MUST NOT be used.","3":"When the workflow is completed, Round-Trip modals SHOULD show the same view that was displayed when initiating the modal.","4":"Round-Trip modals SHOULD NOT be used to add new items of any kind since adding item is a linear workflow redirecting to the newly added item setting- or content-tab.","5":"Round-Trip modals SHOULD NOT be used to perform complex workflows."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ModalFactoryModal","children":[],"less_variables":[],"path":"src\/UI\/Component\/Modal\/RoundTrip"},"ModalLightboxLightbox":{"id":"ModalLightboxLightbox","title":"Lightbox","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Lightbox modal displays media data such as images or videos.","composition":"A Lightbox modal consists of one or multiple lightbox pages representing the media together with a title and description.","effect":"Lightbox modals are activated by clicking the full view glyphicon, the title of the object or it's thumbnail. If multiple pages are to be displayed, they can flipped through.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Lightbox modals MUST contain a title above the presented item.","2":"Lightbox modals SHOULD contain a descriptional text below the presented items.","3":"Multiple media items inside a Lightbox modal MUST be presented in carousel like manner allowing to flickr through items."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ModalFactoryModal","children":[],"less_variables":[],"path":"src\/UI\/Component\/Modal\/Lightbox"},"LightboxImagePageLightboxImagePage":{"id":"LightboxImagePageLightboxImagePage","title":"Lightbox Image Page","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A Lightbox image page represents an image inside a Lightbox modal.","composition":"The page consists of the image, a title and optional description.","effect":"The image is displayed in the content section of the Lightbox modal and the title is used as modal title. If a description is present, it will be displayed below the image.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"2":"A Lighbox image page MUST have an image and a short title.","1":"A Lightbox image page SHOULD have short a description, describing the presented image. If the description is omitted, the Lightbox image page falls back to the alt tag of the image."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ModalFactoryModal","children":[],"less_variables":[],"path":"LightboxImagePage"},"PopoverStandardStandard":{"id":"PopoverStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Standard Popovers are used to display other components. Whenever you want to use the standard-popover, please hand in a PullRequest and discuss it.","composition":"The content of a Standard Popover displays the components together with an optional title.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Standard Popovers MUST NOT be used to render lists, use a Listing Popover for this purpose.","2":"Standard Popovers SHOULD NOT contain complex or large components.","3":"Usages of Standard Popovers MUST be accepted by JourFixe."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PopoverFactoryPopover","children":[],"less_variables":[],"path":"src\/UI\/Component\/Popover\/Standard"},"PopoverListingListing":{"id":"PopoverListingListing","title":"Listing","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Listing Popovers are used to display list items.","composition":"The content of a Listing Popover displays the list together with an optional title.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Listing Popovers MUST be used if one needs to display lists inside a Popover."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PopoverFactoryPopover","children":[],"less_variables":[],"path":"src\/UI\/Component\/Popover\/Listing"},"DividerHorizontalHorizontal":{"id":"DividerHorizontalHorizontal","title":"Horizontal","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A Horizontal Divider is used to mark a thematic change in a sequence of elements that are stacked from top to bottom.","composition":"Horiztonal dividers consists of a horizontal ruler which may comprise a label.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Horizontal Dividers MUST only be used in container components that render a sequence of items from top to bottom."},"composition":[],"interaction":[],"wording":[],"ordering":{"1":"Horizontal Dividers MUST always have a succeeding element in a sequence of elments, which MUST NOT be another Horizontal Divider.","2":"Horizontal Dividers without label MUST always have a preceding element in a sequence of elments, which MUST NOT be another Horizontal Divider."},"style":[],"responsiveness":[],"accessibility":[]},"parent":"DividerFactoryDivider","children":[],"less_variables":[],"path":"src\/UI\/Component\/Divider\/Horizontal"},"LinkStandardStandard":{"id":"LinkStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A standard link is a link with a text label as content of the link.","composition":"The standard link uses the default link color as text color an no background.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Standard links MUST be used if there is no good reason using another instance."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"LinkFactoryLink","children":[],"less_variables":[],"path":"src\/UI\/Component\/Link\/Standard"},"DropdownStandardStandard":{"id":"DropdownStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Standard Dropdown is the default Dropdown to be used in ILIAS. If there is no good reason using another Dropdown instance in ILIAS, this is the one that should be used.","composition":"The Standard Dropdown uses the primary color as background.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Standard Dropdown MUST be used if there is no good reason using another instance."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"DropdownFactoryDropdown","children":[],"less_variables":[],"path":"src\/UI\/Component\/Dropdown\/Standard"},"ItemStandardStandard":{"id":"ItemStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"This is a standard item to be used in lists or similar contexts.","composition":"A list item consists of a title and the following optional elements: description, action drop down, properties (name\/value), a text or image lead and a color.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"Information MUST NOT be provided by color alone. The same information could be presented, e.g. in a property to enable screen reader access."}},"parent":"ItemFactoryItem","children":[],"less_variables":[],"path":"src\/UI\/Component\/Item\/Standard"},"ItemGroupGroup":{"id":"ItemGroupGroup","title":"Group","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"An Item Group groups items of a certain type.","composition":"An Item Group consists of a header with an optional action Dropdown and a list if Items.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ItemFactoryItem","children":[],"less_variables":[],"path":"src\/UI\/Component\/Item\/Group"},"IconStandardStandard":{"id":"IconStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Standard Icons represent ILIAS Objects.","composition":"A Standard Icon is displayed as a block-element with a background-graphic. By default, a fallback icon will be rendered; this is until a background image is defined in the icon's CSS-class.","effect":"","rivals":{"1":"Custom Icons are constructed with a path to an (uploaded) image."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"IconFactoryIcon","children":[],"less_variables":[],"path":"src\/UI\/Component\/Icon\/Standard"},"IconCustomCustom":{"id":"IconCustomCustom","title":"Custom","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"ILIAS allows users to upload icons for repository objects. Those, in opposite to the standard icons, need to be constructed with a path.","composition":"Instead of setting a background image via CSS-class, an image-tag is contained in the icons's div.","effect":"","rivals":{"1":"Standard Icons MUST be used for core-objects."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Custom Icons MAY still use an abbreviation."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"Custom Icons MUST use SVG as graphic.","2":"Icons MUST have a transparent background so they could be put on all kinds of backgrounds.","3":"Images used for Custom Icons SHOULD have equal width and height (=be quadratic) in order not to be distorted."},"responsiveness":[],"accessibility":[]},"parent":"IconFactoryIcon","children":[],"less_variables":[],"path":"src\/UI\/Component\/Icon\/Custom"},"ViewControlModeMode":{"id":"ViewControlModeMode","title":"Mode","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Mode View Controls enable the switching between different aspects of some data. The different modes are mutually exclusive and can therefore not be activated at once.","composition":"Mode View Controls are composed of Buttons switching between active and inactive states.","effect":"Clicking on an inactive Button turns this button active and all other inactive. Clicking on an active button has no effect.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Exactly one Button MUST always be active."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The HTML container enclosing the buttons of the Mode View Control MUST cary the role-attribute \"group\".","2":"The HTML container enclosing the buttons of the Mode View Control MUST set an aria-label describing the element. Eg. \"Mode View Control\"","3":"The Buttons of the Mode View Control MUST set an aria-label clearly describing what the button shows if clicked. E.g. \"List View\", \"Month View\", ...","4":"The currently active Button must be labeled by setting aria-checked to \"true\"."}},"parent":"ViewControlFactoryViewControl","children":[],"less_variables":[],"path":"src\/UI\/Component\/ViewControl\/Mode"},"ViewControlSectionSection":{"id":"ViewControlSectionSection","title":"Section","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Section View Controls enable the switching between different sections of some data. Examples are subsequent days\/weeks\/months in a calendar or entries in a blog.","composition":"Section View Controls are composed of three Buttons. The Button on the left caries a Back Glyph, the Button in the middle is either a Default- or Split Button labeling the data displayed below and the Button on the right carries a next Glyph.","effect":"Clicking on the Buttons left or right changes the selection of the displayed data by a fixed interval. Clicking the Button in the middle opens the sections hinted by the label of the button (e.g. \"Today\").","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ViewControlFactoryViewControl","children":[],"less_variables":[],"path":"src\/UI\/Component\/ViewControl\/Section"},"ChartScaleBarScaleBar":{"id":"ChartScaleBarScaleBar","title":"Scale Bar","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Scale Bars are used to display a set of items some of which especially highlighted. E.g. they can be used to inform about a score or target on a rank ordered scale.","composition":"Scale Bars are composed of of a set of bars of equal size. Each bar contains a title. The highlighted elements differ from the others through their darkened background.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Each Bar of the Scale Bars MUST bear a title.","2":"The title of Scale Bars MUST NOT contain any other content than text."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ChartFactoryChart","children":[],"less_variables":[],"path":"src\/UI\/Component\/Chart\/ScaleBar"}} \ No newline at end of file +{"FactoryUIComponent":{"id":"FactoryUIComponent","title":"UIComponent","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"What is to be done by this control","composition":"What happens if the control is operated","effect":"What happens if the control is operated","rivals":{"Rival 1":"What other controls are similar, what is their distinction"}},"background ":"Relevant academic information","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Where and when an element is to be used or not."},"composition":[],"interaction":{"2":"How the interaction with this object takes place."},"wording":{"3":"How the wording of labels or captions must be."},"ordering":{"5":"How different elements of this instance are to be ordered."},"style":{"4":"How this element should look like."},"responsiveness":{"6":"How this element behaves on changing screen sizes"},"accessibility":{"7":"How this element is made accessible"}},"parent":false,"children":["CounterFactoryCounter","GlyphFactoryGlyph","ButtonFactoryButton","CardCardCard","DeckDeckDeck","ListingFactoryListing","ImageFactoryImage","LegacyLegacyLegacy","PanelFactoryPanel","ModalFactoryModal","PopoverFactoryPopover","DividerFactoryDivider","LinkFactoryLink","DropzoneFactoryDropzone","DropdownFactoryDropdown","ItemFactoryItem","IconFactoryIcon","ViewControlFactoryViewControl","BreadcrumbsBreadcrumbsBreadcrumbs","ChartFactoryChart"],"less_variables":[],"path":"src\/UI\/Factory"},"CounterFactoryCounter":{"id":"CounterFactoryCounter","title":"Counter","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Counter inform users about the quantity of items indicated by a glyph.","composition":"Counters consist of a number and some background color and are placed one the 'end of the line' in reading direction of the item they state the count for.","effect":"Counters convey information, they are not interactive.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":["http:\/\/www.ilias.de\/docu\/goto_docu_wiki_wpage_3854_1357.html"],"rules":{"usage":{"1":"A counter MUST only be used in combination with a glyph."},"composition":{"1":"A counter MUST contain exactly one number greater than zero and no other characters."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["CounterCounterStatus","CounterCounterNovelty"],"less_variables":[],"path":"src\/UI\/Component\/Counter\/Factory"},"GlyphFactoryGlyph":{"id":"GlyphFactoryGlyph","title":"Glyph","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Glyphs map a generally known concept or symbol to a specific concept in ILIAS. Glyphs are used when space is scarce.","composition":"A glyph is a typographical character that represents something else. As any other typographical character, they can be manipulated by regular CSS. If hovered they change their background to indicate possible interactions.","effect":"Glyphs act as trigger for some action such as opening a certain Overlay type or as shortcut.","rivals":{"icon":"Standalone Icons are not interactive. Icons can be in an interactive container however. Icons merely serve as additional hint of the functionality described by some title. Glyphs are visually distinguished from object icons: they are monochrome."}},"background ":"\"In typography, a glyph is an elemental symbol within an agreed set of symbols, intended to represent a readable character for the purposes of writing and thereby expressing thoughts, ideas and concepts.\" (https:\/\/en.wikipedia.org\/wiki\/Glyph) Lidwell states that such symbols are used \"to improve the recognition and recall of signs and controls\".","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Glyphs MUST NOT be used in content titles.","2":"Glyphs MUST be used for cross-sectional functionality such as mail for example and NOT for representing objects.","3":"Glyphs SHOULD be used for very simple tasks that are repeated at many places throughout the system.","4":"Services such as mail MAY be represented by a glyph AND an icon."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"All Glyphs MUST be taken from the Bootstrap Glyphicon Halflings set. Exceptions MUST be approved by the JF."},"responsiveness":[],"accessibility":{"1":"The functionality triggered by the Glyph must be indicated to screen readers with by the attribute aria-label or aria-labelledby attribute."}},"parent":"FactoryUIComponent","children":["GlyphGlyphSettings","GlyphGlyphCollapse","GlyphGlyphExpand","GlyphGlyphAdd","GlyphGlyphRemove","GlyphGlyphUp","GlyphGlyphDown","GlyphGlyphBack","GlyphGlyphNext","GlyphGlyphSortAscending","GlyphGlyphSortDescending","GlyphGlyphBriefcase","GlyphGlyphUser","GlyphGlyphMail","GlyphGlyphNotification","GlyphGlyphTag","GlyphGlyphNote","GlyphGlyphComment"],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Factory"},"ButtonFactoryButton":{"id":"ButtonFactoryButton","title":"Button","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Buttons trigger interactions that change the system\u2019s status. Usually Buttons are contained in an Input Collection. The Toolbar is the main exception to this rule, since buttons in the Toolbar might also perform view changes.","composition":"Button is a clickable, graphically obtrusive control element. It can bear text.","effect":"On-click, the action indicated by the button is carried out.","rivals":{"glyph":"Glyphs are used if the enclosing Container Collection can not provide enough space for textual information or if such an information would clutter the screen.","links":"Links are used to trigger Interactions that do not change the systems status. They are usually contained inside a Navigational Collection."}},"background ":"Wording rules have been inspired by the iOS Human Interface Guidelines (UI-Elements->Controls->System Button) Style rules have been inspired from the GNOME Human Interface Guidelines->Buttons.","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Buttons MUST NOT be used inside a Textual Paragraph."},"composition":[],"interaction":{"1":"A Button SHOULD trigger an action. Only in Toolbars, Buttons MAY also change the view.","2":"If an action is temporarily not available, Buttons MUST be disabled by setting as type 'disabled'."},"wording":{"1":"The caption of a Button SHOULD contain no more than two words.","2":"The wording of the button SHOULD describe the action the button performs by using a verb or a verb phrase.","3":"Every word except articles, coordinating conjunctions and prepositions of four or fewer letters MUST be capitalized.","4":"For standard events such as saving or canceling the existing standard terms MUST be used if possible: Save, Cancel, Delete, Cut, Copy.","5":"There are cases where a non-standard label such as \u201cSend Mail\u201d for saving and sending the input of a specific form might deviate from the standard. These cases MUST however specifically justified."},"ordering":[],"style":{"1":"If Text is used inside a Button, the Button MUST be at least six characters wide."},"responsiveness":[],"accessibility":{"1":"DOM elements of type \"button\" MUST be used to properly identify an element as a Button if there is no good reason to do otherwise.","2":"Button DOM elements MUST either be of type \"button\", of type \"a\" accompanied with the aria-role \u201cButton\u201d or input along with the type attribute \u201cbutton\u201d or \"submit\"."}},"parent":"FactoryUIComponent","children":["ButtonStandardStandard","ButtonPrimaryPrimary","ButtonCloseClose","ButtonShyShy","ButtonMonthMonth","ButtonTagTag"],"less_variables":[],"path":"src\/UI\/Component\/Button\/Factory"},"CardCardCard":{"id":"CardCardCard","title":"Card","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A card is a flexible content container for small chunks of structured data. Cards are often used in so-called Decks which are a gallery of Cards.","composition":"Cards contain a header, which often includes an Image or Icon and a Title as well as possible actions as Default Buttons and 0 to n sections that may contain further textual descriptions, links and buttons.","effect":"Cards may contain Interaction Triggers.","rivals":{"Heading Panel":"Heading Panels fill up the complete available width in the Center Content Section. Multiple Heading Panels are stacked vertically.","Block Panels":"Block Panels are used in Sidebars"}},"background ":"","selector":"","feature_wiki_references ":["http:\/\/www.ilias.de\/docu\/goto_docu_wiki_wpage_3208_1357.html"],"rules":{"usage":[],"composition":{"1":"Cards MUST contain a title.","2":"Cards SHOULD contain an Image or Icon in the header section.","3":"Cards MAY contain Interaction Triggers."},"interaction":[],"wording":[],"ordering":[],"style":{"1":"Sections of Cards MUST be separated by Dividers."},"responsiveness":[],"accessibility":{"1":"If multiple Cards are used, they MUST be contained in a Deck."}},"parent":"FactoryUIComponent","children":[],"less_variables":[],"path":"src\/UI\/Component\/Card\/Card"},"DeckDeckDeck":{"id":"DeckDeckDeck","title":"Deck","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Decks are used to display multiple Cards in a grid. They should be used if a page contains many content items that have similar style and importance. A Deck gives each item equal horizontal space indicating that they are of equal importance.","composition":"Decks are composed only of Cards arranged in a grid. The cards displayed by decks are all of equal size. This Size ranges very small (XS) to very large (XL).","effect":"The Deck is a mere scaffolding element, is has no effect.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":["http:\/\/www.ilias.de\/docu\/goto_docu_wiki_wpage_3992_1357.html"],"rules":{"usage":{"1":"Decks MUST only be used to display multiple Cards."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"The number of cards displayed per row MUST adapt to the screen size."},"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":[],"less_variables":[],"path":"src\/UI\/Component\/Deck\/Deck"},"ListingFactoryListing":{"id":"ListingFactoryListing","title":"Listing","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Listings are used to structure itemised textual information.","composition":"Listings may contain ordered, unordered, or labeled items.","effect":"Listings hold only textual information. They may contain Links but no Buttons.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Listings MUST NOT contain Buttons."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["ListingUnorderedUnordered","ListingOrderedOrdered","ListingDescriptiveDescriptive"],"less_variables":[],"path":"src\/UI\/Component\/Listing\/Factory"},"ImageFactoryImage":{"id":"ImageFactoryImage","title":"Image","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Image component is used to display images of various sources.","composition":"An Image is composed of the image and an alternative text for screen readers.","effect":"Images may be included in interacted components but not interactive on their own.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"Images MUST contain the alt attribute. This attribute MAY be left empty (alt=\"\") if the image is of decorative nature. According to the WAI, decorative images don\u2019t add information to the content of a page. For example, the information provided by the image might already be given using adjacent text, or the image might be included to make the website more visually attractive (see https:\/\/www.w3.org\/WAI\/tutorials\/images\/decorative\/<\/a>)."}},"parent":"FactoryUIComponent","children":["ImageImageStandard","ImageImageResponsive"],"less_variables":[],"path":"src\/UI\/Component\/Image\/Factory"},"LegacyLegacyLegacy":{"id":"LegacyLegacyLegacy","title":"Legacy","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"This component is used to wrap an existing ILIAS UI element into a UI component. This is useful if a container of the UI components needs to contain content that is not yet implement in the centralized UI components.","composition":"The legacy component contains html or any other content as string.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"This component MUST only be used to ensure backwards compatibility with existing UI elements in ILIAS, therefore it SHOULD only contain Elements which cannot be generated using other UI Components from the UI Service."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":[],"less_variables":[],"path":"src\/UI\/Component\/Legacy\/Legacy"},"PanelFactoryPanel":{"id":"PanelFactoryPanel","title":"Panel","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Panels are used to group titled content.","composition":"Panels consist of a header and content section. They form one Gestalt and so build a perceivable cluster of information.","effect":"The effect of interaction with panels heavily depends on their content.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":{"1":"Panels MUST contain a title."},"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["PanelStandardStandard","PanelSubSub","PanelReportReport","PanelListingFactoryListing"],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Factory"},"ModalFactoryModal":{"id":"ModalFactoryModal","title":"Modal","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Modal forces users to focus on the task at hand.","composition":"A Modal is a full-screen dialog on top of the greyed-out ILIAS screen. The Modal consists of a header with a close button and a typography modal title, a content section and might have a footer.","effect":"All controls of the original context are inaccessible until the Modal is completed. Upon completion the user returns to the original context.","rivals":{"1":"Modals have some relations to popovers. The main difference between the two is the disruptive nature of the Modal and the larger amount of data that might be displayed inside a modal. Also popovers perform mostly action to add or consult metadata of an item while Modals manipulate or focus items or their sub-items directly."}},"background ":"http:\/\/quince.infragistics.com\/Patterns\/Modal%20Panel.aspx","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The main purpose of the Modals MUST NOT be navigational. But Modals MAY be dialogue of one or two steps and thus encompass \"next\"-buttons or the like.","2":"Modals MUST NOT contain other modals (Modal in Modal).","3":"Modals SHOULD not be used to perform complex workflows.","4":"Modals MUST be closable by a little \u201cx\u201d-button on the right side of the header.","5":"Modals MUST contain a title in the header."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["ModalInterruptiveInterruptive","InterruptiveItemInterruptiveItem","ModalRoundTripRoundtrip","ModalLightboxLightbox","LightboxImagePageLightboxImagePage"],"less_variables":[],"path":"src\/UI\/Component\/Modal\/Factory"},"PopoverFactoryPopover":{"id":"PopoverFactoryPopover","title":"Popover","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Popovers can be used when space is scarce i.e. within List GUI items, table cells or menus in the Header section. They offer either secondary information on object like a preview or rating to be displayed or entered. They display information about ongoing processes","composition":"Popovers consist of a layer displayed above all other content. The content of the Popover depends on the functionality it performs. A Popover MAY display a title above its content. All Popovers contain a pointer pointing from the Popover to the Triggerer of the Popover.","effect":"Popovers are shown by clicking a Triggerer component such as a Button or Glyph. The position of the Popover is calculated automatically be default. However, it is possible to specify if the popover appears horizontal (left, right) or vertical (top, bottom) relative to its Triggerer component. Popovers disappear by clicking anywhere outside the Popover or by pressing the ESC key.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Popovers MUST NOT contain horizontal scrollbars.","2":"Popovers MAY contain vertical scrollbars. The content component is responsible to define its own height and show vertical scrollbars.","3":"If Popovers are used to present secondary information of an object, they SHOULD display a title representing the object."},"composition":[],"interaction":{"1":"A Popover MUST only be displayed if the Trigger component is clicked. This behaviour is different from Tooltips that appear on hovering. Popovers disappear by clicking anywhere outside the Popover or by pressing the ESC key."},"wording":[],"ordering":[],"style":{"1":"Popovers MUST always relate to the Trigger component by a little pointer."},"responsiveness":[],"accessibility":{"1":"There MUST be a way to open the Popover by only using the keyboard.","2":"The focus MUST be inside the Popover, once it is open if it contains at least one interactive item. Otherwise the focus MUST remain on the Triggerer component.","3":"The focus MUST NOT leave the Popover for as long as it is open.","4":"There MUST be a way to reach every control in the Popover by only using the keyboard.","5":"The Popover MUST be closable by pressing the ESC key.","6":"Once the Popover is closed, the focus MUST return to the element triggering the opening of the Popover or the element being clicked if the Popover was closed on click."}},"parent":"FactoryUIComponent","children":["PopoverStandardStandard","PopoverListingListing"],"less_variables":[],"path":"src\/UI\/Component\/Popover\/Factory"},"DividerFactoryDivider":{"id":"DividerFactoryDivider","title":"Divider","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A divider marks a thematic change in a sequence of other components. A Horizontal Divider is used to mark a thematic change in sequence of elements that are stacked from top to bottom, e.g. in a Dropdown. A Vertical Divider is used to mark a thematic change in a sequence of elements that are lined up from left to right, e.g. a Toolbar.","composition":"","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Dividers MUST only be used in container components that explicitly state and define the usage of Dividers within the container."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["DividerHorizontalHorizontal"],"less_variables":[],"path":"src\/UI\/Component\/Divider\/Factory"},"LinkFactoryLink":{"id":"LinkFactoryLink","title":"Link","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Links are used navigate to other resources or views of the system. Clicking on a link does not change the systems status.","composition":"Link is a clickable, graphically minimal obtrusive control element. It can bear text or other content. Links always contain a valid href tag which should not not just contain a hash sign.","effect":"On-click, the resource or view indicated by the link is requested and presented. Links are not used to trigger Javascript events.","rivals":{"buttons":"Buttons are used to trigger Interactions that usually change the systems status. Buttons are much more obtrusive than links and may fire JS events."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Links MAY be used inline in a Textual Paragraphs."},"composition":[],"interaction":{"1":"Hovering an active link should indicate a possible interaction.","2":"Links MUST not be used to fire Javascript events."},"wording":{"1":"The wording of the link SHOULD name the target view or resource."},"ordering":[],"style":{"1":"Links SHOULD not be presented with a separate background color."},"responsiveness":[],"accessibility":{"1":"DOM elements of type \"a\" MUST be used to properly identify an element."}},"parent":"FactoryUIComponent","children":["LinkStandardStandard"],"less_variables":[],"path":"src\/UI\/Component\/Link\/Factory"},"DropzoneFactoryDropzone":{"id":"DropzoneFactoryDropzone","title":"Dropzone","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Dropzones are containers used to drop either files or other HTML elements.","composition":"A dropzone is a container on the page. Depending on the type of the dropzone, the container is visible by default or it gets highlighted once the user starts to drag the elements over the browser window.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Dropzones MUST be highlighted if the user is dragging compatible elements inside or over the browser window."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["DropzoneFileFactoryFile"],"less_variables":[],"path":"src\/UI\/Component\/Dropzone\/Factory"},"DropdownFactoryDropdown":{"id":"DropdownFactoryDropdown","title":"Dropdown","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Dropdowns reveal a list of interactions that change the system\u2019s status or navigate to a different view.","composition":"Dropdown is a clickable, graphically obtrusive control element. It can bear text. On-click a list of Shy Buttons and optional Dividers is shown.","effect":"On-click, a list of actions is revealed. Clicking an item will trigger the action indicated. Clicking outside of an opened Dropdown will close the list of items.","rivals":{"button":"Buttons are used, if single actions should be presented directly in the user interface.","links":"Links are used to trigger actions that do not change the systems status. They are usually contained inside a Navigational Collection.","popovers":"Dropdowns only provide a list of possible actions. Popovers can include more diverse and flexible content."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Dropdowns MUST NOT be used standalone. They are only parts of more complex UI elements. These elements MUST define their use of Dropdown. E.g. a List or a Table MAY define that a certain kind of Dropdown is used as part of the UI element."},"composition":[],"interaction":{"1":"Only Dropdown Items MUST trigger an action or change a view. The Dropdown trigger element is only used to show and hide the list of Dropdown Items."},"wording":{"1":"The label of a Dropdown SHOULD contain no more than two words.","2":"Every word except articles, coordinating conjunctions and prepositions of four or fewer letters MUST be capitalized.","3":"For standard events such as saving or canceling the existing standard terms MUST be used if possible: Delete, Cut, Copy.","4":"There are cases where a non-standard label such as \u201cSend Mail\u201d for saving and sending the input of a specific form might deviate from the standard. These cases MUST however specifically justified."},"ordering":[],"style":{"1":"If Text is used inside a Dropdown label, the Dropdown MUST be at least six characters wide."},"responsiveness":[],"accessibility":{"1":"DOM elements of type \"button\" MUST be used to properly identify an element as a Dropdown.","2":"Dropdown items MUST be implemented as \"ul\" list with a set of \"li\" elements and nested Shy Button elements for the actions.","3":"Triggers of Dropdowns MUST indicate their effect by the aria-haspopup attribute set to true.","4":"Triggers of Dropdowns MUST indicate the current state of the Dropdown by the aria-expanded label.","5":"Dropdowns MUST be accessible by keyboard by focusing the trigger element and clicking the return key.","6":"Entries in a Dropdown MUST be accessible by the tab-key if opened.","7":"The focus MAY leave the Dropdown if tab is pressed while focusing the last element. This differs from the behaviour in Popovers and Modals."}},"parent":"FactoryUIComponent","children":["DropdownStandardStandard"],"less_variables":[],"path":"src\/UI\/Component\/Dropdown\/Factory"},"ItemFactoryItem":{"id":"ItemFactoryItem","title":"Item","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"An item displays a unique entity within the system. It shows information about that entity in a structured way.","composition":"Items contain the name of the entity as a title. The item contains three sections, where one section contains important information about the item, the second section shows the content of the item and another section shows metadata about the entity.","effect":"Items may contain Interaction Triggers such as Glyphs, Buttons or Tags.","rivals":{"Card":"Cards define the look of items in a deck. Todo: We need to refactor cards."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Items MUST contain the name of the displayed entity as a title.","2":"Items SHOULD contain a section with it's content.","3":"Items MAY contain Interaction Triggers.","4":"Items MAY contain a section with metadata."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["ItemStandardStandard","ItemGroupGroup"],"less_variables":[],"path":"src\/UI\/Component\/Item\/Factory"},"IconFactoryIcon":{"id":"IconFactoryIcon","title":"Icon","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Icons are quickly comprehensible and recognizable graphics. They indicate the functionality or nature of a text-element or context: Icons will mainly be used in front of object-titles, e.g. in the header, the tree and in repository listing.","composition":"Icons come in three fixed sizes: small, medium and large. They can be configured with an additional \"abbreviation\", a text of a few characters that will be rendered on top of the image.","effect":"Icons themselves are not interactive; however they are allowed within interactive containers.","rivals":{"1":"Glyphs are typographical characters that act as a trigger for some action.","2":"Images belong to the content and can be purely decorative."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Icons MUST be used to represent objects or context.","2":"Icons MUST be used in combination with a title or label.","3":"An unique Icon MUST always refer to the same thing."},"composition":[],"interaction":[],"wording":{"1":"The aria-label MUST state the represented object-type.","2":"The abbreviation SHOULD consist of one or two letters."},"ordering":[],"style":{"1":"Icons MUST have a class indicating their usage.","2":"Icons MUST be tagged with a CSS-class indicating their size."},"responsiveness":[],"accessibility":{"1":"Icons MUST use aria-label."}},"parent":"FactoryUIComponent","children":["IconStandardStandard","IconCustomCustom"],"less_variables":[],"path":"src\/UI\/Component\/Icon\/Factory"},"ViewControlFactoryViewControl":{"id":"ViewControlFactoryViewControl","title":"View Control","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"View Controls switch between different visualisation of data.","composition":"View Controls are composed mainly of buttons, they are often found in toolbars.","effect":"Interacting with a view control changes to display in some content area.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"FactoryUIComponent","children":["ViewControlModeMode","ViewControlSectionSection"],"less_variables":[],"path":"src\/UI\/Component\/ViewControl\/Factory"},"BreadcrumbsBreadcrumbsBreadcrumbs":{"id":"BreadcrumbsBreadcrumbsBreadcrumbs","title":"Breadcrumbs","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Breadcrumbs is a supplemental navigation scheme. It eases the user's navigation to higher items in hierarchical structures. Breadcrumbs also serve as an effective visual aid indicating the user's location on a website.","composition":"Breadcrumbs-entries are rendered as horizontally arranged UI Links with a seperator in-between.","effect":"Clicking on an entry will get the user to the respective location.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Crumbs MUST trigger navigation to other resources of the system."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":[],"less_variables":[],"path":"src\/UI\/Component\/Breadcrumbs\/Breadcrumbs"},"ChartFactoryChart":{"id":"ChartFactoryChart","title":"Chart","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Charts are used to graphically represent data in various forms such as maps, graphs or diagrams.","composition":"Charts are composed of various graphical and textual elements representing the raw data.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"Charts SHOULD not rely on colors to convey information."},"responsiveness":[],"accessibility":[]},"parent":"FactoryUIComponent","children":["ChartScaleBarScaleBar"],"less_variables":[],"path":"src\/UI\/Component\/Chart\/Factory"},"CounterCounterStatus":{"id":"CounterCounterStatus","title":"Status","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Status counter is used to display information about the total number of some items like users active on the system or total number of comments.","composition":"The Status Counter is a non-obstrusive Counter.","effect":"Status Counters convey information, they are not interactive.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"The Status Counter MUST be displayed on the lower right of the item it accompanies.","2":"The Status Counter SHOULD have a non-obstrusive background color, such as grey."},"responsiveness":[],"accessibility":[]},"parent":"CounterFactoryCounter","children":[],"less_variables":[],"path":"src\/UI\/Component\/Counter\/Counter"},"CounterCounterNovelty":{"id":"CounterCounterNovelty","title":"Novelty","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Novelty counters inform users about the arrival or creation of new items of the kind indicated by the accompanying glyph.","composition":"A Novelty Counter is an obtrusive counter.","effect":"They count down \/ disappear as soon as the change has been consulted by the user.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Novelty Counter MAY be used with the Status Counter."},"composition":[],"interaction":{"2":"There MUST be a way for the user to consult the changes indicated by the counter.","3":"After the consultation, the Novelty Counter SHOULD disappear or the number it contains is reduced by one.","4":"Depending on the content, the reduced number MAY be added in an additional Status Counter."},"wording":[],"ordering":[],"style":{"5":"The Novelty Counter MUST be displayed on the top at the 'end of the line' in reading direction of the item it accompanies. This would be top right for latin script and top left for arabic script.","6":"The Novelty Counter SHOULD have an obstrusive background color, such as red or orange."},"responsiveness":[],"accessibility":[]},"parent":"CounterFactoryCounter","children":[],"less_variables":[],"path":"src\/UI\/Component\/Counter\/Counter"},"GlyphGlyphSettings":{"id":"GlyphGlyphSettings","title":"Settings","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Settings Glyph triggers opening a Dropdown to edit settings of the displayed block.","composition":"The Settings Glyph uses the glyphicon-cog.","effect":"Upon clicking a settings Dropdown is opened.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Settings Glyph MUST only be used in Blocks."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u201cSettings\u201d."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphCollapse":{"id":"GlyphGlyphCollapse","title":"Collapse","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Collapse Glyph is used to trigger the collapsing of some neighbouring Container Collection such as a the content of a Dropdown or an Accordion currently shown.","composition":"The Collapse Glyph is composed of a triangle pointing to the bottom indicating that content is currently shown.","effect":"Clicking the Collapse Glyph hides the display of some Container Collection.","rivals":{"Expand Glyph":"The Expand Glyphs triggers the display of some Container Collection.","Previous Glyph":"The Previous\/Next Glyph opens a completely new view. It serves a navigational purpose."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Collapse Glyph MUST indicate if the toggled Container Collection is visible or not."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Collapse Content'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphExpand":{"id":"GlyphGlyphExpand","title":"Expand","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Expand Glyph is used to trigger the display of some neighbouring Container Collection such as a the content of a Dropdown or an Accordion currently shown.","composition":"The Expand Glyph is composed of a triangle pointing to the right indicating that content is currently shown.","effect":"Clicking the Expand Glyph displays some Container Collection.","rivals":{"Collapse Glyph":"The Collapse Glyphs hides the display of some Container Collection.","Previous Glyph":"The Previous\/Next Glyph opens a completely new view. It serves a navigational purpose."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Expand Glyph MUST indicate if the toggled Container Collection is visible or not."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Expand Content'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphAdd":{"id":"GlyphGlyphAdd","title":"Add","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The glyphed add-button serves as stand-in for the respective textual buttons in very crowded screens. It allows adding a new item.","composition":"The Add Glyph uses the glyphicon-plus-sign.","effect":"Clicking on the Add Glyph adds a new input to a form or an event to the calendar.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Add Glyph SHOULD not come without a Remove Glyph and vice versa. Because either there is not enough place for textual buttons or there is place. Exceptions to this rule, such as the Calendar, where only elements can be added in a certain place are possible, are to be run through the Jour Fixe.","2":"The Add Glyph stands for an Action and SHOULD be placed in the action column of a form.","3":"The Add Glyph MUST not be used to add lines to tables."},"composition":[],"interaction":{"1":"Newly added items MUST be placed below the line in which the Add Glyph has been clicked"},"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Add'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphRemove":{"id":"GlyphGlyphRemove","title":"Remove","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Remove Glyph serves as stand-in for the respective textual buttons in very crowded screens. It allows removing an item.","composition":"The Remove Glyph uses the glyphicon-plus-sign.","effect":"Clicking on the Remove Glyph adds a new input to a form or an event to the calendar.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Remove Glyph SHOULD not come without a glyphed Add Glyph and vice versa. Because either there is not enough place for textual buttons or there is place. Exceptions to this rule, such as the Calendar, where only elements can be added in a certain place are possible, are to be run through the Jour Fixe.","2":"The Remove Glyph stands for an Action and SHOULD be placed in the action column of a form.","3":"The Remove Glyph MUST not be used to add lines to tables."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Remove'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphUp":{"id":"GlyphGlyphUp","title":"Up","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Up Glyph allows for manually arranging rows in tables embedded in forms. It allows moving an item up.","composition":"The Up Glyph uses the glyphicon-circle-arrow-up. The Up Glyph can be combined with the Add\/Remove Glyph.","effect":"Clicking on the Up Glyph moves an item up.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":["http:\/\/www.ilias.de\/docu\/goto_docu_wiki_wpage_813_1357.html"],"rules":{"usage":{"1":"The Up Glyph MUST NOT be used to sort tables. There is an established sorting control for that.","2":"The Up Glyph SHOULD not come without a Down and vice versa.","3":"The Up Glyph is an action and SHOULD be listed in the action column of a form."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Up'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphDown":{"id":"GlyphGlyphDown","title":"Down","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Down Glyph allows for manually arranging rows in tables embedded in forms. It allows moving an item down.","composition":"The Down Glyph uses the glyphicon-circle-arrow-down. The Down Glyph can be combined with the Add\/Remove Glyph.","effect":"Clicking on the Down Glyph moves an item up.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":["http:\/\/www.ilias.de\/docu\/goto_docu_wiki_wpage_813_1357.html"],"rules":{"usage":{"1":"The Down Glyph MUST NOT be used to sort tables. There is an established sorting control for that.","2":"The Down Glyph SHOULD not come without a Up and vice versa.","3":"The Down Glyph is an action and SHOULD be listed in the action column of a form."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Down'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphBack":{"id":"GlyphGlyphBack","title":"Back","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Back Glyph indicates a possible change of the view. The view change leads back to some previous view.","composition":"The chevron-left glyphicon is used.","effect":"The click on a Back Glyph leads back to a previous view.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Back and Next Buttons MUST be accompanied by the respective Back\/Next Glyph."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"If clicking on the Back\/Next GLYPH opens a new view of an object, the Next Glyph MUST be used.","2":"If clicking on the Back\/Next GLYPH opens a previous view of an object, the Back Glyph MUST be used."},"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Back'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphNext":{"id":"GlyphGlyphNext","title":"Next","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Next Glyph indicates a possible change of the view. The view change leads back to some previous view.","composition":"The chevron-right glyphicon is used.","effect":"The click on a Next Glyph opens a new view.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Back and Next Buttons MUST be accompanied by the respective Back\/Next Glyph."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"If clicking on the Back\/Next GLYPH opens a new view of an object, the Next Glyph MUST be used.","2":"If clicking on the Back\/Next GLYPH opens a previous view of an object, the Back Glyph MUST be used."},"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Next'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphSortAscending":{"id":"GlyphGlyphSortAscending","title":"Sort Ascending","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Sorting Glyphs indicate the sorting direction of a column in a table as ascending (up) or descending (down). It is a toggle reversing the ordering of a column.","composition":"The Sort Ascending Glyph uses glyphicon-arrow-up.","effect":"Clicking the Sort Ascending Glyph reverses the direction of ordering in a table.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Sort Ascending'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphSortDescending":{"id":"GlyphGlyphSortDescending","title":"Sort Descending","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Sorting Glyphs indicate the sorting direction of a column in a table as ascending (up) or descending (down). It is a toggle reversing the ordering of a column.","composition":"The Sort Descending Glyph uses glyphicon-arrow-descending.","effect":"Clicking the Sort Descending Glyph reverses the direction of ordering in a table.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Sort Descending'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphBriefcase":{"id":"GlyphGlyphBriefcase","title":"Briefcase","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The briefcase symbolize some ongoing work that is done. Momentarily in the background tasks.","composition":"The briefcase Glyph uses glyphicon-briefcase.","effect":"The click on the briefcase opens a popup to the background tasks.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Background Tasks'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphUser":{"id":"GlyphGlyphUser","title":"User","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The User Glyph triggers the \u201cWho is online?\u201d Popover in the Top Navigation. The User Glyph indicates the number of pending contact requests and users online via the the Novelty Counter and Status Counter respectively.","composition":"The User Glyph uses the glyphicon-user.","effect":"Clicking the User Glyph opens the \u201cWho is online?\u201d Popover.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Show who is online'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphMail":{"id":"GlyphGlyphMail","title":"Mail","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Mail Glyph provides a shortcut to the mail service. The Mail Glyph indicates the number of new mails received.","composition":"The Mail Glyph uses the glyphicon-envelope.","effect":"Upon clicking on the Mail Glyph the user is transferred to the full-screen mail service.","rivals":{"Mail Icon":"The Mail Icon is used to indicate the user is currently located in the Mail service The Mail Glyph acts as shortcut to the Mail service."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Mail'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphNotification":{"id":"GlyphGlyphNotification","title":"Notification","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Notification Glyph allows users to activate \/ deactivate the notification service for a specific object or sub-item. It is a toggle indicating by colour whether it is activated or not.","composition":"The Notification Glyph uses the glyphicon-bell in link-color if notifications are not active or brand-warning color if they are.","effect":"Upon clicking the notification activation is toggled: Clicking the Notification Glyph activates respectively deactivates the notification service for the current object or sub-item.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"The Notification Glyph MUST only be used in the Content Top Actions."},"composition":[],"interaction":{"1":"Clicking the Notification Glyph MUST toggle the activation of Notifications."},"wording":[],"ordering":[],"style":{"1":"If notifications are activated the Notification Glyph MUST use the brand-warning color."},"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Notifications'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphTag":{"id":"GlyphGlyphTag","title":"Tag","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Tag Glyph is used to indicate the possibility of adding tags to an object.","composition":"The Tag Glyph uses the glyphicon-tag.","effect":"Upon clicking the Round Trip Modal to add new Tags is opened.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Novelty and Status Counter MUST show the amount of tags that has been given for an specific object."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Tags'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphNote":{"id":"GlyphGlyphNote","title":"Note","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Note Glyph is used to indicate the possibilty of adding notes to an object.","composition":"The Note Glyph uses the glyphicon-pushpin.","effect":"Upon clicking the Round Trip Modal to add new notes is opened","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Novelty and Status Counter MUST show the amount of notes that has been given for an specific object."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Notes'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"GlyphGlyphComment":{"id":"GlyphGlyphComment","title":"Comment","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Comment Glyph is used to indicate the possibilty of adding comments to an object.","composition":"The Comment Glyph uses the glyphicon-comment.","effect":"Upon clicking the Round Trip Modal to add new comments is opened.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Novelty and Status Counter MUST show the amount of comments that has been given for an specific object."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The aria-label MUST be \u2018Comments'."}},"parent":"GlyphFactoryGlyph","children":[],"less_variables":[],"path":"src\/UI\/Component\/Glyph\/Glyph"},"ButtonStandardStandard":{"id":"ButtonStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The standard button is the default button to be used in ILIAS. If there is no good reason using another button instance in ILIAS, this is the one that should be used.","composition":"The standard button uses the primary color as background.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Standard buttons MUST be used if there is no good reason using another instance."},"composition":[],"interaction":[],"wording":[],"ordering":{"1":"The most important standard button SHOULD be first in reading direction if there are several buttons.","2":"In the toolbar and in forms special regulations for the ordering of the buttons MAY apply."},"style":[],"responsiveness":{"1":"The most important standard button in multi-action bars MUST be sticky (stay visible on small screens)."},"accessibility":{"1":"Standard buttons MAY define aria-label attribute. Use it in cases where a text label is not visible on the screen or when the label does not provide enough information about the action.","2":"Standard buttons MAY define aria-checked attribute. Use it to inform which is the currently active button."}},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Standard"},"ButtonPrimaryPrimary":{"id":"ButtonPrimaryPrimary","title":"Primary","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The primary button indicates the most important action on a screen. By definition there can only be one single \u201cmost important\u201d action on any given screen and thus only one single primary button per screen.","composition":"The background color is the btn-primary-color. This screen-unique button-color ensures that it stands out and attracts the user\u2019s attention while there are several buttons competing for attention.","effect":"In toolbars the primary button are required to be sticky, meaning they stay in view in the responsive view.","rivals":[]},"background ":"Tiddwell refers to the primary button as \u201cprominent done button\u201d and describes that \u201cthe button that finishes a transaction should be placed at the end of the visual flow; and is to be made big and well labeled.\u201d She explains that \u201cA well-understood, obvious last step gives your users a sense of closure. There\u2019s no doubt that the transaction will be done when that button is clicked; don\u2019t leave them hanging, wondering whether their work took effect\u201d. The GNOME Human Interface Guidelines -> Buttons also describes a button indicated as most important for dialogs.","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Most pages SHOULD NOT have any Primary Button at all.","2":"There MUST be no more than one Primary Button per page in ILIAS.","3":"The decision to make a Button a Primary Button MUST be confirmed by the JF."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Primary"},"ButtonCloseClose":{"id":"ButtonCloseClose","title":"Close","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The close button triggers the closing of some collection displayed temporarily such as an overlay.","composition":"The close button is displayed without border.","effect":"Clicking the close button closes the enclosing collection.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":{"1":"The Close Button MUST always be positioned in the top right of a collection."},"style":[],"responsiveness":[],"accessibility":{"1":"The functionality of the close button MUST be indicated for screen readers by an aria-label."}},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Close"},"ButtonShyShy":{"id":"ButtonShyShy","title":"Shy","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Shy buttons are used in contexts that need a less obtrusive presentation than usual buttons have, e.g. in UI collections like Dropdowns.","composition":"Shy buttons do not come with a separte background color.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Shy buttons MUST only be used, if a standard button presentation is not appropriate. E.g. if usual buttons destroy the presentation of an outer UI component or if there is not enough space for a standard button presentation."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Shy"},"ButtonMonthMonth":{"id":"ButtonMonthMonth","title":"Month","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Month Button enables to select a specific month to fire some action (probably a change of view).","composition":"The Month Button is composed of a Button showing the default month directly (probably the month currently rendered by some view). A dropdown contains an interface enabling the selection of a month from the future or the past.","effect":"Selecting a month from the dropdown directly fires the according action (e.g. switching the view to the selected month). Technically this is currently a Javascript event being fired.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":{"1":"Selecting a month from the dropdown MUST directly fire the according action."},"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Month"},"ButtonTagTag":{"id":"ButtonTagTag","title":"Tag","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Tags classify entities. Thus, their primary purpose is the visualization of those classifications for one entity. However, tags are usually clickable - either to edit associations or list related entities, i.e. objects with the same tag.","composition":"Tags are a colored area with text on it. When used in a tag-cloud (a list of tags), tags can be visually \"weighted\" according to the number of their occurences, be it with different (font-)sizes, different colors or all of them.","effect":"Tags may trigger an action or change the view when clicked. There is no visual difference (besides the cursor) between clickable tags and tags with unavailable action.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"Tags SHOULD be used with an additonal class to adjust colors.","2":"The font-color SHOULD be set with high contrast to the chosen background color."},"responsiveness":[],"accessibility":{"1":"The functionality of the tag button MUST be indicated for screen readers by an aria-label."}},"parent":"ButtonFactoryButton","children":[],"less_variables":[],"path":"src\/UI\/Component\/Button\/Tag"},"ListingUnorderedUnordered":{"id":"ListingUnorderedUnordered","title":"Unordered","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Unordered Lists are used to display a unordered set of textual elements.","composition":"Unordered Lists are composed of a set of bullets labeling the listed items.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ListingFactoryListing","children":[],"less_variables":[],"path":"src\/UI\/Component\/Listing\/Unordered"},"ListingOrderedOrdered":{"id":"ListingOrderedOrdered","title":"Ordered","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Ordered Lists are used to displayed a numbered set of textual elements. They are used if the order of the elements is relevant.","composition":"Ordered Lists are composed of a set of numbers labeling the items enumerated.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ListingFactoryListing","children":[],"less_variables":[],"path":"src\/UI\/Component\/Listing\/Ordered"},"ListingDescriptiveDescriptive":{"id":"ListingDescriptiveDescriptive","title":"Descriptive","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Descriptive Lists are used to display key-value doubles of textual-information.","composition":"Descriptive Lists are composed of a key acting as title describing the type of information being displayed underneath.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ListingFactoryListing","children":[],"less_variables":[],"path":"src\/UI\/Component\/Listing\/Descriptive"},"ImageImageStandard":{"id":"ImageImageStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The standard image is used if the image is to be rendered in it's the original size.","composition":"","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ImageFactoryImage","children":[],"less_variables":[],"path":"src\/UI\/Component\/Image\/Image"},"ImageImageResponsive":{"id":"ImageImageResponsive","title":"Responsive","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A responsive image is to be used if the image needs to adapt to changing amount of space available.","composition":"Responsive images scale nicely to the parent element.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ImageFactoryImage","children":[],"less_variables":[],"path":"src\/UI\/Component\/Image\/Image"},"PanelStandardStandard":{"id":"PanelStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Standard Panels are used in the Center Content section to group content.","composition":"Standard Panels consist of a title and a content section. The structure of this content might be varying from Standard Panel to Standard Panel. Standard Panels may contain Sub Panels.","effect":"","rivals":{"Cards":"Often Cards are used in Decks to display multiple uniformly structured chunks of Data horizontally and vertically."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"In Forms Standard Panels MUST be used to group different sections into Form Parts.","2":"Standard Panels SHOULD be used in the Center Content as primary Container for grouping content of varying content."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PanelFactoryPanel","children":[],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Standard"},"PanelSubSub":{"id":"PanelSubSub","title":"Sub","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Sub Panels are used to structure the content of Standard panels further into titled sections.","composition":"Sub Panels consist of a title and a content section. They may contain a Card on their right side to display meta information about the content displayed.","effect":"","rivals":{"Standard Panel":"The Standard Panel might contain a Sub Panel.","Card":"The Sub Panels may contain one Card."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Sub Panels MUST only be inside Standard Panels"},"composition":{"1":"Sub Panels MUST NOT contain Sub Panels or Standard Panels as content."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PanelFactoryPanel","children":[],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Sub"},"PanelReportReport":{"id":"PanelReportReport","title":"Report","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Report Panels display user-generated data combining text in lists, tables and sometimes charts. Report Panels always draw from two distinct sources: the structure \/ scaffolding of the Report Panels stems from user-generated content (i.e a question of a survey, a competence with levels) and is filled with user-generated content harvested by that very structure (i.e. participants\u2019 answers to the question, self-evaluation of competence).","composition":"They are composed of a Standard Panel which contains several Sub Panels. They might also contain a card to display information meta information in their first block.","effect":"Report Panels are predominantly used for displaying data. They may however comprise links or buttons.","rivals":{"Standard Panels":"The Report Panels contains sub panels used to structure information."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Report Panels SHOULD be used when user generated content of two sources (i.e results, guidelines in a template) is to be displayed alongside each other."},"composition":[],"interaction":{"1":"Links MAY open new views.","2":"Buttons MAY trigger actions or inline editing."},"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PanelFactoryPanel","children":[],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Report"},"PanelListingFactoryListing":{"id":"PanelListingFactoryListing","title":"Listing","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Listing Panels are used to list items following all one single template.","composition":"Listing Panels are composed of several titled Item Groups. They further may contain a filter.","effect":"The List Items of Listing Panels may contain a dropdown offering options to interact with the item. Further Listing Panels may be filtered and the number of sections or items to be displayed may be configurable.","rivals":{"Report Panels":"Report Panels contain sections as Sub Panels each displaying different aspects of one item."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Listing Panels SHOULD be used, if a large number of items using the same template are to be displayed in an inviting way not using a Table."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PanelFactoryPanel","children":["PanelListingStandardStandard"],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Listing\/Factory"},"PanelListingStandardStandard":{"id":"PanelListingStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Standard item lists present lists of items with similar presentation. All items are passed by using Item Groups.","composition":"This Listing is composed of title and a set of Item Groups. Additionally an optional dropdown to select the number\/types of items to be shown at the top of the Listing.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"PanelListingFactoryListing","children":[],"less_variables":[],"path":"src\/UI\/Component\/Panel\/Listing\/Standard"},"ModalInterruptiveInterruptive":{"id":"ModalInterruptiveInterruptive","title":"Interruptive","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"An Interruptive modal disrupts the user in critical situation, forcing him or her to focus on the task at hand.","composition":"The modal states why this situation needs attention and may point out consequences.","effect":"All controls of the original context are inaccessible until the modal is completed. Upon completion the user returns to the original context.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Due to the heavily disruptive nature of this type of modal it MUST be restricted to critical situations (e.g. loss of data).","2":"All actions where data is deleted from the system are considered to be critical situations and SHOULD be implemented as an Interruptive modal. Exceptions are possible if items from lists in forms are to be deleted or if the modal would heavily disrupt the workflow.","3":"Interruptive modals MUST contain a primary button continuing the action that initiated the modal (e.g. Delete the item) on the left side of the footer of the modal and a default button canceling the action on the right side of the footer.","4":"The cancel button in the footer and the close button in the header MUST NOT perform any additional action than closing the Interruptive modal."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ModalFactoryModal","children":[],"less_variables":[],"path":"src\/UI\/Component\/Modal\/Interruptive"},"InterruptiveItemInterruptiveItem":{"id":"InterruptiveItemInterruptiveItem","title":"Interruptive Item","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Interruptive items are displayed in an Interruptive modal and represent the object(s) being affected by the critical action, e.g. deleting.","composition":"An Interruptive item is composed of an Id, title, description and an icon.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"An interruptive item MUST have an ID and title.","2":"An interruptive item SHOULD have an icon representing the affected object.","3":"An interruptive item MAY have a description which helps to further identify the object. If an Interruptive modal displays multiple items having the the same title, the description MUST be used in order to distinct these objects from each other.","4":"If an interruptive item represents an ILIAS object, e.g. a course, then the Id, title, description and icon of the item MUST correspond to the Id, title, description and icon from the ILIAS object."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ModalFactoryModal","children":[],"less_variables":[],"path":"InterruptiveItem"},"ModalRoundTripRoundtrip":{"id":"ModalRoundTripRoundtrip","title":"Roundtrip","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Round-Trip modals are to be used if the context would be lost by performing this action otherwise. Round-Trip modals accommodate sub-workflows within an overriding workflow. The Round-Trip modal ensures that the user does not leave the trajectory of the overriding workflow. This is typically the case if an ILIAS service is being called while working in an object.","composition":"Round-Trip modals are completed by a well defined sequence of only a few steps that might be displayed on a sequence of different modals connected through some \"next\" button.","effect":"Round-Trip modals perform sub-workflow involving some kind of user input. Sub-workflow is completed and user is returned to starting point allowing for continuing the overriding workflow.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Round-Trip modals MUST contain at least two buttons at the bottom of the modals: a button to cancel (right) the workflow and a button to finish or reach the next step in the workflow (left).","2":"Round-Trip modals SHOULD be used, if the user would lose the context otherwise. If the action can be performed within the same context (e.g. add a post in a forum, edit a wiki page), a Round-Trip modal MUST NOT be used.","3":"When the workflow is completed, Round-Trip modals SHOULD show the same view that was displayed when initiating the modal.","4":"Round-Trip modals SHOULD NOT be used to add new items of any kind since adding item is a linear workflow redirecting to the newly added item setting- or content-tab.","5":"Round-Trip modals SHOULD NOT be used to perform complex workflows."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ModalFactoryModal","children":[],"less_variables":[],"path":"src\/UI\/Component\/Modal\/RoundTrip"},"ModalLightboxLightbox":{"id":"ModalLightboxLightbox","title":"Lightbox","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Lightbox modal displays media data such as images or videos.","composition":"A Lightbox modal consists of one or multiple lightbox pages representing the media together with a title and description.","effect":"Lightbox modals are activated by clicking the full view glyphicon, the title of the object or it's thumbnail. If multiple pages are to be displayed, they can flipped through.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Lightbox modals MUST contain a title above the presented item.","2":"Lightbox modals SHOULD contain a descriptional text below the presented items.","3":"Multiple media items inside a Lightbox modal MUST be presented in carousel like manner allowing to flickr through items."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ModalFactoryModal","children":[],"less_variables":[],"path":"src\/UI\/Component\/Modal\/Lightbox"},"LightboxImagePageLightboxImagePage":{"id":"LightboxImagePageLightboxImagePage","title":"Lightbox Image Page","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A Lightbox image page represents an image inside a Lightbox modal.","composition":"The page consists of the image, a title and optional description.","effect":"The image is displayed in the content section of the Lightbox modal and the title is used as modal title. If a description is present, it will be displayed below the image.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"2":"A Lighbox image page MUST have an image and a short title.","1":"A Lightbox image page SHOULD have short a description, describing the presented image. If the description is omitted, the Lightbox image page falls back to the alt tag of the image."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ModalFactoryModal","children":[],"less_variables":[],"path":"LightboxImagePage"},"PopoverStandardStandard":{"id":"PopoverStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Standard Popovers are used to display other components. Whenever you want to use the standard-popover, please hand in a PullRequest and discuss it.","composition":"The content of a Standard Popover displays the components together with an optional title.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Standard Popovers MUST NOT be used to render lists, use a Listing Popover for this purpose.","2":"Standard Popovers SHOULD NOT contain complex or large components.","3":"Usages of Standard Popovers MUST be accepted by JourFixe."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PopoverFactoryPopover","children":[],"less_variables":[],"path":"src\/UI\/Component\/Popover\/Standard"},"PopoverListingListing":{"id":"PopoverListingListing","title":"Listing","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Listing Popovers are used to display list items.","composition":"The content of a Listing Popover displays the list together with an optional title.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Listing Popovers MUST be used if one needs to display lists inside a Popover."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"PopoverFactoryPopover","children":[],"less_variables":[],"path":"src\/UI\/Component\/Popover\/Listing"},"DividerHorizontalHorizontal":{"id":"DividerHorizontalHorizontal","title":"Horizontal","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A Horizontal Divider is used to mark a thematic change in a sequence of elements that are stacked from top to bottom.","composition":"Horiztonal dividers consists of a horizontal ruler which may comprise a label.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Horizontal Dividers MUST only be used in container components that render a sequence of items from top to bottom."},"composition":[],"interaction":[],"wording":[],"ordering":{"1":"Horizontal Dividers MUST always have a succeeding element in a sequence of elments, which MUST NOT be another Horizontal Divider.","2":"Horizontal Dividers without label MUST always have a preceding element in a sequence of elments, which MUST NOT be another Horizontal Divider."},"style":[],"responsiveness":[],"accessibility":[]},"parent":"DividerFactoryDivider","children":[],"less_variables":[],"path":"src\/UI\/Component\/Divider\/Horizontal"},"LinkStandardStandard":{"id":"LinkStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A standard link is a link with a text label as content of the link.","composition":"The standard link uses the default link color as text color an no background.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Standard links MUST be used if there is no good reason using another instance."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"LinkFactoryLink","children":[],"less_variables":[],"path":"src\/UI\/Component\/Link\/Standard"},"DropzoneFileFactoryFile":{"id":"DropzoneFileFactoryFile","title":"File","abstract":1,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"File dropzones are used to drop files from outside the browser window. The dropped files are presented to the user and can be uploaded to the server. File dropzones offer additional convenience beside manually selecting files over the file browser.","composition":"File dropzones are areas to drop the files. They contain either a message (standard file dropzone) or other ILIAS UI components (wrapper file dropzone).","effect":"A dropzone is highlighted when the user drags files over it.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"There MUST be alternative ways in the system to upload the files due to the limited accessibility of file dropzones."}},"parent":"DropzoneFactoryDropzone","children":["DropzoneFileStandardStandard","DropzoneFileWrapperWrapper"],"less_variables":[],"path":"src\/UI\/Component\/Dropzone\/File\/Factory"},"DropzoneFileStandardStandard":{"id":"DropzoneFileStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The standard dropzone is used to drop files dragged from outside the browser window. The dropped files are presented to the user and can be uploaded to the server.","composition":"Standard dropzones consist of a visible area where files can be dropped. They MUST contain a message explaining that it is possible to drop files inside. The dropped files are presented to the user, optionally with some button to start the upload process.","effect":"A standard dropzone is highlighted when the user is dragging files over the dropzone. After dropping, the dropped files are presented to the user with some meta information of the files such the file name and file size.","rivals":{"Rival 1":"A wrapper dropzone can hold other ILIAS UI components instead of a message."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Standard dropzones MUST contain a message.","2":"The upload button MUST be disabled if there are no files to be uploaded. Only true if the dropzone is NOT used in a form containing other form elements.","3":"Standard dropzones MAY be used in forms."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"Standard dropzones MUST offer the possibility to select files manually from the computer."}},"parent":"DropzoneFileFactoryFile","children":[],"less_variables":[],"path":"src\/UI\/Component\/Dropzone\/File\/Standard"},"DropzoneFileWrapperWrapper":{"id":"DropzoneFileWrapperWrapper","title":"Wrapper","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"A wrapper dropzone is used to display other ILIAS UI components inside it. In contrast to the standard dropzone, the wrapper dropzone is not visible by default. Only the wrapped components are visible. Any wrapper dropzone gets highlighted once the user is dragging files over the browser window. Thus, a user needs to have the knowledge that there are wrapper dropzones present. They can be introduced to offer additional approaches to complete some workflow more conveniently. Especially in situation where space is scarce such as appointments in the calendar.","composition":"A wrapper dropzone contains one or multiple ILIAS UI components. A roundtrip modal is used to present the dropped files and to initialize the upload process.","effect":"All wrapper dropzones on the page are highlighted when the user dragging files over the browser window. After dropping the files, the roundtrip modal is opened showing all files. The modal contains a button to start the upload process.","rivals":{"Rival 1":"A standard dropzone displays a message instead of other ILIAS UI components."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Most pages SHOULD NOT contain a wrapper dropzone. Whenever you want to introduce a new usage of the Wrapper-Dropzone, propose it to the Jour Fixe.","2":"Wrapper dropzones MUST contain one or more ILIAS UI components.","3":"Wrapper dropzones MUST NOT contain any other file dropzones.","4":"Wrapper dropzones MUST NOT be used in modals.","5":"The upload button in the modal MUST be disabled if there are no files to be uploaded."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"DropzoneFileFactoryFile","children":[],"less_variables":[],"path":"src\/UI\/Component\/Dropzone\/File\/Wrapper"},"DropdownStandardStandard":{"id":"DropdownStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"The Standard Dropdown is the default Dropdown to be used in ILIAS. If there is no good reason using another Dropdown instance in ILIAS, this is the one that should be used.","composition":"The Standard Dropdown uses the primary color as background.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Standard Dropdown MUST be used if there is no good reason using another instance."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"DropdownFactoryDropdown","children":[],"less_variables":[],"path":"src\/UI\/Component\/Dropdown\/Standard"},"ItemStandardStandard":{"id":"ItemStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"This is a standard item to be used in lists or similar contexts.","composition":"A list item consists of a title and the following optional elements: description, action drop down, properties (name\/value), a text or image lead and a color.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"Information MUST NOT be provided by color alone. The same information could be presented, e.g. in a property to enable screen reader access."}},"parent":"ItemFactoryItem","children":[],"less_variables":[],"path":"src\/UI\/Component\/Item\/Standard"},"ItemGroupGroup":{"id":"ItemGroupGroup","title":"Group","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"An Item Group groups items of a certain type.","composition":"An Item Group consists of a header with an optional action Dropdown and a list if Items.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ItemFactoryItem","children":[],"less_variables":[],"path":"src\/UI\/Component\/Item\/Group"},"IconStandardStandard":{"id":"IconStandardStandard","title":"Standard","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Standard Icons represent ILIAS Objects.","composition":"A Standard Icon is displayed as a block-element with a background-graphic. By default, a fallback icon will be rendered; this is until a background image is defined in the icon's CSS-class.","effect":"","rivals":{"1":"Custom Icons are constructed with a path to an (uploaded) image."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"IconFactoryIcon","children":[],"less_variables":[],"path":"src\/UI\/Component\/Icon\/Standard"},"IconCustomCustom":{"id":"IconCustomCustom","title":"Custom","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"ILIAS allows users to upload icons for repository objects. Those, in opposite to the standard icons, need to be constructed with a path.","composition":"Instead of setting a background image via CSS-class, an image-tag is contained in the icons's div.","effect":"","rivals":{"1":"Standard Icons MUST be used for core-objects."}},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Custom Icons MAY still use an abbreviation."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":{"1":"Custom Icons MUST use SVG as graphic.","2":"Icons MUST have a transparent background so they could be put on all kinds of backgrounds.","3":"Images used for Custom Icons SHOULD have equal width and height (=be quadratic) in order not to be distorted."},"responsiveness":[],"accessibility":[]},"parent":"IconFactoryIcon","children":[],"less_variables":[],"path":"src\/UI\/Component\/Icon\/Custom"},"ViewControlModeMode":{"id":"ViewControlModeMode","title":"Mode","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Mode View Controls enable the switching between different aspects of some data. The different modes are mutually exclusive and can therefore not be activated at once.","composition":"Mode View Controls are composed of Buttons switching between active and inactive states.","effect":"Clicking on an inactive Button turns this button active and all other inactive. Clicking on an active button has no effect.","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":{"1":"Exactly one Button MUST always be active."},"composition":[],"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":{"1":"The HTML container enclosing the buttons of the Mode View Control MUST cary the role-attribute \"group\".","2":"The HTML container enclosing the buttons of the Mode View Control MUST set an aria-label describing the element. Eg. \"Mode View Control\"","3":"The Buttons of the Mode View Control MUST set an aria-label clearly describing what the button shows if clicked. E.g. \"List View\", \"Month View\", ...","4":"The currently active Button must be labeled by setting aria-checked to \"true\"."}},"parent":"ViewControlFactoryViewControl","children":[],"less_variables":[],"path":"src\/UI\/Component\/ViewControl\/Mode"},"ViewControlSectionSection":{"id":"ViewControlSectionSection","title":"Section","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Section View Controls enable the switching between different sections of some data. Examples are subsequent days\/weeks\/months in a calendar or entries in a blog.","composition":"Section View Controls are composed of three Buttons. The Button on the left caries a Back Glyph, the Button in the middle is either a Default- or Split Button labeling the data displayed below and the Button on the right carries a next Glyph.","effect":"Clicking on the Buttons left or right changes the selection of the displayed data by a fixed interval. Clicking the Button in the middle opens the sections hinted by the label of the button (e.g. \"Today\").","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":null,"parent":"ViewControlFactoryViewControl","children":[],"less_variables":[],"path":"src\/UI\/Component\/ViewControl\/Section"},"ChartScaleBarScaleBar":{"id":"ChartScaleBarScaleBar","title":"Scale Bar","abstract":0,"status_entry":"Proposed","status_implementation":"Partly implemented","description":{"purpose":"Scale Bars are used to display a set of items some of which especially highlighted. E.g. they can be used to inform about a score or target on a rank ordered scale.","composition":"Scale Bars are composed of of a set of bars of equal size. Each bar contains a title. The highlighted elements differ from the others through their darkened background.","effect":"","rivals":[]},"background ":"","selector":"","feature_wiki_references ":[],"rules":{"usage":[],"composition":{"1":"Each Bar of the Scale Bars MUST bear a title.","2":"The title of Scale Bars MUST NOT contain any other content than text."},"interaction":[],"wording":[],"ordering":[],"style":[],"responsiveness":[],"accessibility":[]},"parent":"ChartFactoryChart","children":[],"less_variables":[],"path":"src\/UI\/Component\/Chart\/ScaleBar"}} \ No newline at end of file diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index a6647ddde65b..de3325717de9 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -5311,6 +5311,7 @@ common#:#visitors#:#Besucher common#:#visits#:#Besuche common#:#web_resources#:#Weblinks common#:#webdav#:#WebDAV +common#:#edit_metadata#:#Metadaten bearbeiten common#:#webdav_pear_auth_http_needed#:#Sie benötigen PEAR-Auth_HTTP um diese Funktion zu verwenden. Geben Sie im Terminal-Fenster pear install Auth_HTTP ein. common#:#webdav_pwd_instruction#:#Um das ILIAS-Magazin als Webordner außerhalb der aktuellen Sitzung als Webordner zu öffnen, empfehlen wir ein lokales Passwort anzulegen.
Dieses Passwort wird nur für die Webordner-Funktionalitäten benötigt, die ILIAS-Anmeldung erfolgt weiterhin mit Ihrem gewohnten Passwort. common#:#webdav_pwd_instruction_success#:#Ein neues lokales Passwort wurde angelegt. Sie können nun das Magazin als Webordner öffnen. @@ -12680,7 +12681,7 @@ trac#:#trac_lp_list_gui#:#Persönlicher Schreibtisch, Magazin, Suche trac#:#trac_lp_list_gui_info#:#Wenn aktiviert, wird der Lernfortschrittsstatus in Objekt-Listen angezeigt. common#:#hide_all_details#:#Alle Details ausblenden common#:#show_all_details#:#Alle Details anzeigen -common#:#select_files_from_computer#:#Wählen Sie die Dateien von Ihrem Computer aus +common#:#select_files_from_computer#:#Dateien wählen common#:#drag_files_here#:#Ziehen Sie die Dateien in diesen Bereich common#:#selected_files#:#Ausgewählte Dateien common#:#num_of_selected_files#:#%s Datei(en) ausgewählt diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 460bb611a7d7..567478ebb9f5 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -2790,6 +2790,7 @@ common#:#notes_and_comments#:#Notes and Comments common#:#notes#:#Notes common#:#notifications#:#Notifications common#:#num_users#:#Number of Users +common#:#edit_metadata#:#Edit Metadata rbac#:#nwss_edit_permission#:#User can change permission settings in News and Web Feeds administration rbac#:#nwss_read#:#User has read access to ews and Web Feeds administration rbac#:#nwss_visible#:#News and Web Feeds administration is visible @@ -10426,7 +10427,7 @@ trac#:#trac_lp_list_gui#:#Personal Desktop, Repository, Search trac#:#trac_lp_list_gui_info#:#If activated, the learning progress status is included in objects lists. common#:#hide_all_details#:#Hide all details common#:#show_all_details#:#Show all details -common#:#select_files_from_computer#:#Select the files from your computer +common#:#select_files_from_computer#:#Select Files common#:#drag_files_here#:#Drag-and-drop your files here common#:#selected_files#:#Selected files common#:#num_of_selected_files#:#%s file(s) selected diff --git a/libs/README.md b/libs/README.md index 18a5bae9ef0c..5838f62945d9 100644 --- a/libs/README.md +++ b/libs/README.md @@ -1,6 +1,7 @@ # Dependency Management in ILIAS The are currently three type of external libraries: -- PHP dependencies installed using composer in /libs/composer, see [composer-readme](libs/composer/README.md). +- PHP dependencies installed using composer in /libs/composer, see [composer-readme](composer/README.md). - PHP dependencies installad manually, located in some /Services/\*/libs and /Modules/\*/libs -- JS- and client-side Frameworks, currently also located in /Services\/* and /Modules\/* \ No newline at end of file +- JS- and client-side Frameworks installed using bower, see [bower-readme](bower/README.md) +- JS- and client-side Frameworks installed manually, currently also located in /Services\/* and /Modules\/* \ No newline at end of file diff --git a/libs/bower/README.md b/libs/bower/README.md new file mode 100644 index 000000000000..3a3bb00334d3 --- /dev/null +++ b/libs/bower/README.md @@ -0,0 +1,21 @@ +# How to add dependencies with bower in ILIAS + +**New dependencies need to be approved by the Jour Fixe of the ILIAS society.** + +## Dependencies for production +- Comment all lines in libs/.gitignore which begin with bower/ +- Add a new library using bower, e.g. "bower install bootstrap@3.3.7 --save" +- Add a section in "extra" with the following metadata": +```json + "jquery": { + "introduction": "03.08.2017", + "introduced-by": "fschmid" + }, +``` +- Run "bower install --no-dev" +- Add all files to ILIAS git-repository and commit + +## Dependencies for development +- Add a new library using bower, e.g. "bower install mocha --save-dev" +- Ignore all directories which are added by installation (uncomment existing) +- commit changes in gitignore and bower.json. diff --git a/libs/bower/bower.json b/libs/bower/bower.json new file mode 100644 index 000000000000..491eed1a6e0f --- /dev/null +++ b/libs/bower/bower.json @@ -0,0 +1,32 @@ +{ + "name": "ilias", + "homepage": "https://ilias.de", + "authors": [ + "ILIAS " + ], + "description": "", + "main": "", + "license": "GNUv3", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "fine-uploader": "5.13.0", + "jquery-dragster": "^1.0.3" + }, + "extra": { + "fine-uploader": { + "introduction": "03.08.2017", + "introduced-by": "fschmid" + }, + "jquery-dragster": { + "introduction": "03.08.2017", + "introduced-by": "fschmid" + } + } +} diff --git a/libs/bower/bower_components/fine-uploader/.bower.json b/libs/bower/bower_components/fine-uploader/.bower.json new file mode 100644 index 000000000000..5ca8af7c77ef --- /dev/null +++ b/libs/bower/bower_components/fine-uploader/.bower.json @@ -0,0 +1,55 @@ +{ + "name": "fine-uploader", + "version": "5.13.0", + "author": "Widen Enterprises, Inc.", + "description": "A official bower build for FineUploader/fine-uploader", + "license": "https://raw.githubusercontent.com/FineUploader/fine-uploader/master/LICENSE", + "keywords": [ + "amazon", + "api", + "aws", + "azure", + "chunk", + "chunking", + "cross-domain", + "cross-site", + "drag", + "drop", + "file", + "file-input", + "file-uploader", + "input", + "jquery", + "jquery-plugin", + "multiple", + "preview", + "progress", + "resume", + "s3", + "selection", + "upload", + "widget" + ], + "repository": { + "type": "git", + "url": "git://github.com/FineUploader/bower-dist.git" + }, + "main": [ + "dist/" + ], + "ignore": [ + ".*", + "build.sh", + "src" + ], + "homepage": "https://github.com/FineUploader/bower-dist", + "_release": "5.13.0", + "_resolution": { + "type": "version", + "tag": "5.13.0", + "commit": "0edcb461548b73bc211427284aaeed4c1dd451b6" + }, + "_source": "https://github.com/FineUploader/bower-dist.git", + "_target": "5.13.0", + "_originalSource": "fine-uploader" +} \ No newline at end of file diff --git a/libs/bower/bower_components/fine-uploader/LICENSE b/libs/bower/bower_components/fine-uploader/LICENSE new file mode 100644 index 000000000000..3814391a38a0 --- /dev/null +++ b/libs/bower/bower_components/fine-uploader/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-present, Widen Enterprises, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/bower/bower_components/fine-uploader/README.md b/libs/bower/bower_components/fine-uploader/README.md new file mode 100644 index 000000000000..46dcefa82258 --- /dev/null +++ b/libs/bower/bower_components/fine-uploader/README.md @@ -0,0 +1,72 @@ +[![Fine Uploader](http://fineuploader.com/img/FineUploader_logo.png)](http://fineuploader.com) + +![Bower](https://img.shields.io/bower/v/fine-uploader.svg?style=flat-square) +[![license](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) + +[**Documentation**](http://docs.fineuploader.com) | +[**Examples**](http://fineuploader.com/demos) | +[**Support**](http://fineuploader.com/support.html) | +[**Blog**](http://blog.fineuploader.com/) | +[**Changelog**](http://blog.fineuploader.com/category/changelog/) + +--- + +## [Bower](http://bower.io) distribution build of [Fine Uploader](http://fineuploader.com) + +### Usage + +First, download Fine Uploader: + +```bash +bower install fine-uploader --save +``` + +Then, simply reference the necessary JavaScript files on your HTML page: + +```html + + +``` + +__Enjoy__ + +---- + +### Updating or building manually + +You normally should not have to do this, but you can _also_ build this distribution yourself by following the steps in this section. + +#### Prepping (getting fine-uploader) + +```bash +$ git clone --recursive https://github.com/FineUploader/bower-dist.git +``` + +OR, if you already cloned this repo; + +```bash +$ cd fineuploader-dist +$ git pull origin +``` + +#### Building + +In your terminal please navigate to where the project is cloned + +```bash +$ ./build.sh # e.g: ./build.sh 5.11.8 +``` + +**NOTE:** + +- The build will automaticaly install node dependencies if the node_modules directory does not exist. +- If for some reason you would like to reinstall the dependencies use `--reinstall-dep` to remove existing `node_modules` directory first. After that execute the following command: + + ```bash + $ ./build.sh --reinstall-dep + ``` + +### Credits + +* [Fery Wardiyanto](https://github.com/feryardiant) as original author of this distribution. [Buy him a coffee](https://gratipay.com/~feryardiant/). +* **Fine Uploader** is a code library sponsored by [Widen Enterprises, Inc.](http://www.widen.com/) diff --git a/libs/bower/bower_components/fine-uploader/bower.json b/libs/bower/bower_components/fine-uploader/bower.json new file mode 100644 index 000000000000..3652c57a4f96 --- /dev/null +++ b/libs/bower/bower_components/fine-uploader/bower.json @@ -0,0 +1,45 @@ +{ + "name": "fine-uploader", + "version": "5.11.8", + "author": "Widen Enterprises, Inc.", + "description": "A official bower build for FineUploader/fine-uploader", + "license": "https://raw.githubusercontent.com/FineUploader/fine-uploader/master/LICENSE", + "keywords": [ + "amazon", + "api", + "aws", + "azure", + "chunk", + "chunking", + "cross-domain", + "cross-site", + "drag", + "drop", + "file", + "file-input", + "file-uploader", + "input", + "jquery", + "jquery-plugin", + "multiple", + "preview", + "progress", + "resume", + "s3", + "selection", + "upload", + "widget" + ], + "repository": { + "type": "git", + "url": "git://github.com/FineUploader/bower-dist.git" + }, + "main": [ + "dist/" + ], + "ignore": [ + ".*", + "build.sh", + "src" + ] +} diff --git a/libs/bower/bower_components/fine-uploader/dist/LICENSE b/libs/bower/bower_components/fine-uploader/dist/LICENSE new file mode 100644 index 000000000000..3814391a38a0 --- /dev/null +++ b/libs/bower/bower_components/fine-uploader/dist/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-present, Widen Enterprises, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/bower/bower_components/fine-uploader/dist/all.fine-uploader.core.js b/libs/bower/bower_components/fine-uploader/dist/all.fine-uploader.core.js new file mode 100644 index 000000000000..c8b88534350e --- /dev/null +++ b/libs/bower/bower_components/fine-uploader/dist/all.fine-uploader.core.js @@ -0,0 +1,8699 @@ +// Fine Uploader 5.13.0 - (c) 2013-present Widen Enterprises, Inc. MIT licensed. http://fineuploader.com +(function(global) { + var qq = function(element) { + "use strict"; + return { + hide: function() { + element.style.display = "none"; + return this; + }, + attach: function(type, fn) { + if (element.addEventListener) { + element.addEventListener(type, fn, false); + } else if (element.attachEvent) { + element.attachEvent("on" + type, fn); + } + return function() { + qq(element).detach(type, fn); + }; + }, + detach: function(type, fn) { + if (element.removeEventListener) { + element.removeEventListener(type, fn, false); + } else if (element.attachEvent) { + element.detachEvent("on" + type, fn); + } + return this; + }, + contains: function(descendant) { + if (!descendant) { + return false; + } + if (element === descendant) { + return true; + } + if (element.contains) { + return element.contains(descendant); + } else { + return !!(descendant.compareDocumentPosition(element) & 8); + } + }, + insertBefore: function(elementB) { + elementB.parentNode.insertBefore(element, elementB); + return this; + }, + remove: function() { + element.parentNode.removeChild(element); + return this; + }, + css: function(styles) { + if (element.style == null) { + throw new qq.Error("Can't apply style to node as it is not on the HTMLElement prototype chain!"); + } + if (styles.opacity != null) { + if (typeof element.style.opacity !== "string" && typeof element.filters !== "undefined") { + styles.filter = "alpha(opacity=" + Math.round(100 * styles.opacity) + ")"; + } + } + qq.extend(element.style, styles); + return this; + }, + hasClass: function(name, considerParent) { + var re = new RegExp("(^| )" + name + "( |$)"); + return re.test(element.className) || !!(considerParent && re.test(element.parentNode.className)); + }, + addClass: function(name) { + if (!qq(element).hasClass(name)) { + element.className += " " + name; + } + return this; + }, + removeClass: function(name) { + var re = new RegExp("(^| )" + name + "( |$)"); + element.className = element.className.replace(re, " ").replace(/^\s+|\s+$/g, ""); + return this; + }, + getByClass: function(className, first) { + var candidates, result = []; + if (first && element.querySelector) { + return element.querySelector("." + className); + } else if (element.querySelectorAll) { + return element.querySelectorAll("." + className); + } + candidates = element.getElementsByTagName("*"); + qq.each(candidates, function(idx, val) { + if (qq(val).hasClass(className)) { + result.push(val); + } + }); + return first ? result[0] : result; + }, + getFirstByClass: function(className) { + return qq(element).getByClass(className, true); + }, + children: function() { + var children = [], child = element.firstChild; + while (child) { + if (child.nodeType === 1) { + children.push(child); + } + child = child.nextSibling; + } + return children; + }, + setText: function(text) { + element.innerText = text; + element.textContent = text; + return this; + }, + clearText: function() { + return qq(element).setText(""); + }, + hasAttribute: function(attrName) { + var attrVal; + if (element.hasAttribute) { + if (!element.hasAttribute(attrName)) { + return false; + } + return /^false$/i.exec(element.getAttribute(attrName)) == null; + } else { + attrVal = element[attrName]; + if (attrVal === undefined) { + return false; + } + return /^false$/i.exec(attrVal) == null; + } + } + }; + }; + (function() { + "use strict"; + qq.canvasToBlob = function(canvas, mime, quality) { + return qq.dataUriToBlob(canvas.toDataURL(mime, quality)); + }; + qq.dataUriToBlob = function(dataUri) { + var arrayBuffer, byteString, createBlob = function(data, mime) { + var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder, blobBuilder = BlobBuilder && new BlobBuilder(); + if (blobBuilder) { + blobBuilder.append(data); + return blobBuilder.getBlob(mime); + } else { + return new Blob([ data ], { + type: mime + }); + } + }, intArray, mimeString; + if (dataUri.split(",")[0].indexOf("base64") >= 0) { + byteString = atob(dataUri.split(",")[1]); + } else { + byteString = decodeURI(dataUri.split(",")[1]); + } + mimeString = dataUri.split(",")[0].split(":")[1].split(";")[0]; + arrayBuffer = new ArrayBuffer(byteString.length); + intArray = new Uint8Array(arrayBuffer); + qq.each(byteString, function(idx, character) { + intArray[idx] = character.charCodeAt(0); + }); + return createBlob(arrayBuffer, mimeString); + }; + qq.log = function(message, level) { + if (window.console) { + if (!level || level === "info") { + window.console.log(message); + } else { + if (window.console[level]) { + window.console[level](message); + } else { + window.console.log("<" + level + "> " + message); + } + } + } + }; + qq.isObject = function(variable) { + return variable && !variable.nodeType && Object.prototype.toString.call(variable) === "[object Object]"; + }; + qq.isFunction = function(variable) { + return typeof variable === "function"; + }; + qq.isArray = function(value) { + return Object.prototype.toString.call(value) === "[object Array]" || value && window.ArrayBuffer && value.buffer && value.buffer.constructor === ArrayBuffer; + }; + qq.isItemList = function(maybeItemList) { + return Object.prototype.toString.call(maybeItemList) === "[object DataTransferItemList]"; + }; + qq.isNodeList = function(maybeNodeList) { + return Object.prototype.toString.call(maybeNodeList) === "[object NodeList]" || maybeNodeList.item && maybeNodeList.namedItem; + }; + qq.isString = function(maybeString) { + return Object.prototype.toString.call(maybeString) === "[object String]"; + }; + qq.trimStr = function(string) { + if (String.prototype.trim) { + return string.trim(); + } + return string.replace(/^\s+|\s+$/g, ""); + }; + qq.format = function(str) { + var args = Array.prototype.slice.call(arguments, 1), newStr = str, nextIdxToReplace = newStr.indexOf("{}"); + qq.each(args, function(idx, val) { + var strBefore = newStr.substring(0, nextIdxToReplace), strAfter = newStr.substring(nextIdxToReplace + 2); + newStr = strBefore + val + strAfter; + nextIdxToReplace = newStr.indexOf("{}", nextIdxToReplace + val.length); + if (nextIdxToReplace < 0) { + return false; + } + }); + return newStr; + }; + qq.isFile = function(maybeFile) { + return window.File && Object.prototype.toString.call(maybeFile) === "[object File]"; + }; + qq.isFileList = function(maybeFileList) { + return window.FileList && Object.prototype.toString.call(maybeFileList) === "[object FileList]"; + }; + qq.isFileOrInput = function(maybeFileOrInput) { + return qq.isFile(maybeFileOrInput) || qq.isInput(maybeFileOrInput); + }; + qq.isInput = function(maybeInput, notFile) { + var evaluateType = function(type) { + var normalizedType = type.toLowerCase(); + if (notFile) { + return normalizedType !== "file"; + } + return normalizedType === "file"; + }; + if (window.HTMLInputElement) { + if (Object.prototype.toString.call(maybeInput) === "[object HTMLInputElement]") { + if (maybeInput.type && evaluateType(maybeInput.type)) { + return true; + } + } + } + if (maybeInput.tagName) { + if (maybeInput.tagName.toLowerCase() === "input") { + if (maybeInput.type && evaluateType(maybeInput.type)) { + return true; + } + } + } + return false; + }; + qq.isBlob = function(maybeBlob) { + if (window.Blob && Object.prototype.toString.call(maybeBlob) === "[object Blob]") { + return true; + } + }; + qq.isXhrUploadSupported = function() { + var input = document.createElement("input"); + input.type = "file"; + return input.multiple !== undefined && typeof File !== "undefined" && typeof FormData !== "undefined" && typeof qq.createXhrInstance().upload !== "undefined"; + }; + qq.createXhrInstance = function() { + if (window.XMLHttpRequest) { + return new XMLHttpRequest(); + } + try { + return new ActiveXObject("MSXML2.XMLHTTP.3.0"); + } catch (error) { + qq.log("Neither XHR or ActiveX are supported!", "error"); + return null; + } + }; + qq.isFolderDropSupported = function(dataTransfer) { + return dataTransfer.items && dataTransfer.items.length > 0 && dataTransfer.items[0].webkitGetAsEntry; + }; + qq.isFileChunkingSupported = function() { + return !qq.androidStock() && qq.isXhrUploadSupported() && (File.prototype.slice !== undefined || File.prototype.webkitSlice !== undefined || File.prototype.mozSlice !== undefined); + }; + qq.sliceBlob = function(fileOrBlob, start, end) { + var slicer = fileOrBlob.slice || fileOrBlob.mozSlice || fileOrBlob.webkitSlice; + return slicer.call(fileOrBlob, start, end); + }; + qq.arrayBufferToHex = function(buffer) { + var bytesAsHex = "", bytes = new Uint8Array(buffer); + qq.each(bytes, function(idx, byt) { + var byteAsHexStr = byt.toString(16); + if (byteAsHexStr.length < 2) { + byteAsHexStr = "0" + byteAsHexStr; + } + bytesAsHex += byteAsHexStr; + }); + return bytesAsHex; + }; + qq.readBlobToHex = function(blob, startOffset, length) { + var initialBlob = qq.sliceBlob(blob, startOffset, startOffset + length), fileReader = new FileReader(), promise = new qq.Promise(); + fileReader.onload = function() { + promise.success(qq.arrayBufferToHex(fileReader.result)); + }; + fileReader.onerror = promise.failure; + fileReader.readAsArrayBuffer(initialBlob); + return promise; + }; + qq.extend = function(first, second, extendNested) { + qq.each(second, function(prop, val) { + if (extendNested && qq.isObject(val)) { + if (first[prop] === undefined) { + first[prop] = {}; + } + qq.extend(first[prop], val, true); + } else { + first[prop] = val; + } + }); + return first; + }; + qq.override = function(target, sourceFn) { + var super_ = {}, source = sourceFn(super_); + qq.each(source, function(srcPropName, srcPropVal) { + if (target[srcPropName] !== undefined) { + super_[srcPropName] = target[srcPropName]; + } + target[srcPropName] = srcPropVal; + }); + return target; + }; + qq.indexOf = function(arr, elt, from) { + if (arr.indexOf) { + return arr.indexOf(elt, from); + } + from = from || 0; + var len = arr.length; + if (from < 0) { + from += len; + } + for (;from < len; from += 1) { + if (arr.hasOwnProperty(from) && arr[from] === elt) { + return from; + } + } + return -1; + }; + qq.getUniqueId = function() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == "x" ? r : r & 3 | 8; + return v.toString(16); + }); + }; + qq.ie = function() { + return navigator.userAgent.indexOf("MSIE") !== -1 || navigator.userAgent.indexOf("Trident") !== -1; + }; + qq.ie7 = function() { + return navigator.userAgent.indexOf("MSIE 7") !== -1; + }; + qq.ie8 = function() { + return navigator.userAgent.indexOf("MSIE 8") !== -1; + }; + qq.ie10 = function() { + return navigator.userAgent.indexOf("MSIE 10") !== -1; + }; + qq.ie11 = function() { + return qq.ie() && navigator.userAgent.indexOf("rv:11") !== -1; + }; + qq.edge = function() { + return navigator.userAgent.indexOf("Edge") >= 0; + }; + qq.safari = function() { + return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1; + }; + qq.chrome = function() { + return navigator.vendor !== undefined && navigator.vendor.indexOf("Google") !== -1; + }; + qq.opera = function() { + return navigator.vendor !== undefined && navigator.vendor.indexOf("Opera") !== -1; + }; + qq.firefox = function() { + return !qq.edge() && !qq.ie11() && navigator.userAgent.indexOf("Mozilla") !== -1 && navigator.vendor !== undefined && navigator.vendor === ""; + }; + qq.windows = function() { + return navigator.platform === "Win32"; + }; + qq.android = function() { + return navigator.userAgent.toLowerCase().indexOf("android") !== -1; + }; + qq.androidStock = function() { + return qq.android() && navigator.userAgent.toLowerCase().indexOf("chrome") < 0; + }; + qq.ios6 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 6_") !== -1; + }; + qq.ios7 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 7_") !== -1; + }; + qq.ios8 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 8_") !== -1; + }; + qq.ios800 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 8_0 ") !== -1; + }; + qq.ios = function() { + return navigator.userAgent.indexOf("iPad") !== -1 || navigator.userAgent.indexOf("iPod") !== -1 || navigator.userAgent.indexOf("iPhone") !== -1; + }; + qq.iosChrome = function() { + return qq.ios() && navigator.userAgent.indexOf("CriOS") !== -1; + }; + qq.iosSafari = function() { + return qq.ios() && !qq.iosChrome() && navigator.userAgent.indexOf("Safari") !== -1; + }; + qq.iosSafariWebView = function() { + return qq.ios() && !qq.iosChrome() && !qq.iosSafari(); + }; + qq.preventDefault = function(e) { + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + } + }; + qq.toElement = function() { + var div = document.createElement("div"); + return function(html) { + div.innerHTML = html; + var element = div.firstChild; + div.removeChild(element); + return element; + }; + }(); + qq.each = function(iterableItem, callback) { + var keyOrIndex, retVal; + if (iterableItem) { + if (window.Storage && iterableItem.constructor === window.Storage) { + for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { + retVal = callback(iterableItem.key(keyOrIndex), iterableItem.getItem(iterableItem.key(keyOrIndex))); + if (retVal === false) { + break; + } + } + } else if (qq.isArray(iterableItem) || qq.isItemList(iterableItem) || qq.isNodeList(iterableItem)) { + for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { + retVal = callback(keyOrIndex, iterableItem[keyOrIndex]); + if (retVal === false) { + break; + } + } + } else if (qq.isString(iterableItem)) { + for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { + retVal = callback(keyOrIndex, iterableItem.charAt(keyOrIndex)); + if (retVal === false) { + break; + } + } + } else { + for (keyOrIndex in iterableItem) { + if (Object.prototype.hasOwnProperty.call(iterableItem, keyOrIndex)) { + retVal = callback(keyOrIndex, iterableItem[keyOrIndex]); + if (retVal === false) { + break; + } + } + } + } + } + }; + qq.bind = function(oldFunc, context) { + if (qq.isFunction(oldFunc)) { + var args = Array.prototype.slice.call(arguments, 2); + return function() { + var newArgs = qq.extend([], args); + if (arguments.length) { + newArgs = newArgs.concat(Array.prototype.slice.call(arguments)); + } + return oldFunc.apply(context, newArgs); + }; + } + throw new Error("first parameter must be a function!"); + }; + qq.obj2url = function(obj, temp, prefixDone) { + var uristrings = [], prefix = "&", add = function(nextObj, i) { + var nextTemp = temp ? /\[\]$/.test(temp) ? temp : temp + "[" + i + "]" : i; + if (nextTemp !== "undefined" && i !== "undefined") { + uristrings.push(typeof nextObj === "object" ? qq.obj2url(nextObj, nextTemp, true) : Object.prototype.toString.call(nextObj) === "[object Function]" ? encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj()) : encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj)); + } + }; + if (!prefixDone && temp) { + prefix = /\?/.test(temp) ? /\?$/.test(temp) ? "" : "&" : "?"; + uristrings.push(temp); + uristrings.push(qq.obj2url(obj)); + } else if (Object.prototype.toString.call(obj) === "[object Array]" && typeof obj !== "undefined") { + qq.each(obj, function(idx, val) { + add(val, idx); + }); + } else if (typeof obj !== "undefined" && obj !== null && typeof obj === "object") { + qq.each(obj, function(prop, val) { + add(val, prop); + }); + } else { + uristrings.push(encodeURIComponent(temp) + "=" + encodeURIComponent(obj)); + } + if (temp) { + return uristrings.join(prefix); + } else { + return uristrings.join(prefix).replace(/^&/, "").replace(/%20/g, "+"); + } + }; + qq.obj2FormData = function(obj, formData, arrayKeyName) { + if (!formData) { + formData = new FormData(); + } + qq.each(obj, function(key, val) { + key = arrayKeyName ? arrayKeyName + "[" + key + "]" : key; + if (qq.isObject(val)) { + qq.obj2FormData(val, formData, key); + } else if (qq.isFunction(val)) { + formData.append(key, val()); + } else { + formData.append(key, val); + } + }); + return formData; + }; + qq.obj2Inputs = function(obj, form) { + var input; + if (!form) { + form = document.createElement("form"); + } + qq.obj2FormData(obj, { + append: function(key, val) { + input = document.createElement("input"); + input.setAttribute("name", key); + input.setAttribute("value", val); + form.appendChild(input); + } + }); + return form; + }; + qq.parseJson = function(json) { + if (window.JSON && qq.isFunction(JSON.parse)) { + return JSON.parse(json); + } else { + return eval("(" + json + ")"); + } + }; + qq.getExtension = function(filename) { + var extIdx = filename.lastIndexOf(".") + 1; + if (extIdx > 0) { + return filename.substr(extIdx, filename.length - extIdx); + } + }; + qq.getFilename = function(blobOrFileInput) { + if (qq.isInput(blobOrFileInput)) { + return blobOrFileInput.value.replace(/.*(\/|\\)/, ""); + } else if (qq.isFile(blobOrFileInput)) { + if (blobOrFileInput.fileName !== null && blobOrFileInput.fileName !== undefined) { + return blobOrFileInput.fileName; + } + } + return blobOrFileInput.name; + }; + qq.DisposeSupport = function() { + var disposers = []; + return { + dispose: function() { + var disposer; + do { + disposer = disposers.shift(); + if (disposer) { + disposer(); + } + } while (disposer); + }, + attach: function() { + var args = arguments; + this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1))); + }, + addDisposer: function(disposeFunction) { + disposers.push(disposeFunction); + } + }; + }; + })(); + (function() { + "use strict"; + if (typeof define === "function" && define.amd) { + define(function() { + return qq; + }); + } else if (typeof module !== "undefined" && module.exports) { + module.exports = qq; + } else { + global.qq = qq; + } + })(); + (function() { + "use strict"; + qq.Error = function(message) { + this.message = "[Fine Uploader " + qq.version + "] " + message; + }; + qq.Error.prototype = new Error(); + })(); + qq.version = "5.13.0"; + qq.supportedFeatures = function() { + "use strict"; + var supportsUploading, supportsUploadingBlobs, supportsFileDrop, supportsAjaxFileUploading, supportsFolderDrop, supportsChunking, supportsResume, supportsUploadViaPaste, supportsUploadCors, supportsDeleteFileXdr, supportsDeleteFileCorsXhr, supportsDeleteFileCors, supportsFolderSelection, supportsImagePreviews, supportsUploadProgress; + function testSupportsFileInputElement() { + var supported = true, tempInput; + try { + tempInput = document.createElement("input"); + tempInput.type = "file"; + qq(tempInput).hide(); + if (tempInput.disabled) { + supported = false; + } + } catch (ex) { + supported = false; + } + return supported; + } + function isChrome21OrHigher() { + return (qq.chrome() || qq.opera()) && navigator.userAgent.match(/Chrome\/[2][1-9]|Chrome\/[3-9][0-9]/) !== undefined; + } + function isChrome14OrHigher() { + return (qq.chrome() || qq.opera()) && navigator.userAgent.match(/Chrome\/[1][4-9]|Chrome\/[2-9][0-9]/) !== undefined; + } + function isCrossOriginXhrSupported() { + if (window.XMLHttpRequest) { + var xhr = qq.createXhrInstance(); + return xhr.withCredentials !== undefined; + } + return false; + } + function isXdrSupported() { + return window.XDomainRequest !== undefined; + } + function isCrossOriginAjaxSupported() { + if (isCrossOriginXhrSupported()) { + return true; + } + return isXdrSupported(); + } + function isFolderSelectionSupported() { + return document.createElement("input").webkitdirectory !== undefined; + } + function isLocalStorageSupported() { + try { + return !!window.localStorage && qq.isFunction(window.localStorage.setItem); + } catch (error) { + return false; + } + } + function isDragAndDropSupported() { + var span = document.createElement("span"); + return ("draggable" in span || "ondragstart" in span && "ondrop" in span) && !qq.android() && !qq.ios(); + } + supportsUploading = testSupportsFileInputElement(); + supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported(); + supportsUploadingBlobs = supportsAjaxFileUploading && !qq.androidStock(); + supportsFileDrop = supportsAjaxFileUploading && isDragAndDropSupported(); + supportsFolderDrop = supportsFileDrop && isChrome21OrHigher(); + supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported(); + supportsResume = supportsAjaxFileUploading && supportsChunking && isLocalStorageSupported(); + supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher(); + supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading); + supportsDeleteFileCorsXhr = isCrossOriginXhrSupported(); + supportsDeleteFileXdr = isXdrSupported(); + supportsDeleteFileCors = isCrossOriginAjaxSupported(); + supportsFolderSelection = isFolderSelectionSupported(); + supportsImagePreviews = supportsAjaxFileUploading && window.FileReader !== undefined; + supportsUploadProgress = function() { + if (supportsAjaxFileUploading) { + return !qq.androidStock() && !qq.iosChrome(); + } + return false; + }(); + return { + ajaxUploading: supportsAjaxFileUploading, + blobUploading: supportsUploadingBlobs, + canDetermineSize: supportsAjaxFileUploading, + chunking: supportsChunking, + deleteFileCors: supportsDeleteFileCors, + deleteFileCorsXdr: supportsDeleteFileXdr, + deleteFileCorsXhr: supportsDeleteFileCorsXhr, + dialogElement: !!window.HTMLDialogElement, + fileDrop: supportsFileDrop, + folderDrop: supportsFolderDrop, + folderSelection: supportsFolderSelection, + imagePreviews: supportsImagePreviews, + imageValidation: supportsImagePreviews, + itemSizeValidation: supportsAjaxFileUploading, + pause: supportsChunking, + progressBar: supportsUploadProgress, + resume: supportsResume, + scaling: supportsImagePreviews && supportsUploadingBlobs, + tiffPreviews: qq.safari(), + unlimitedScaledImageSize: !qq.ios(), + uploading: supportsUploading, + uploadCors: supportsUploadCors, + uploadCustomHeaders: supportsAjaxFileUploading, + uploadNonMultipart: supportsAjaxFileUploading, + uploadViaPaste: supportsUploadViaPaste + }; + }(); + qq.isGenericPromise = function(maybePromise) { + "use strict"; + return !!(maybePromise && maybePromise.then && qq.isFunction(maybePromise.then)); + }; + qq.Promise = function() { + "use strict"; + var successArgs, failureArgs, successCallbacks = [], failureCallbacks = [], doneCallbacks = [], state = 0; + qq.extend(this, { + then: function(onSuccess, onFailure) { + if (state === 0) { + if (onSuccess) { + successCallbacks.push(onSuccess); + } + if (onFailure) { + failureCallbacks.push(onFailure); + } + } else if (state === -1) { + onFailure && onFailure.apply(null, failureArgs); + } else if (onSuccess) { + onSuccess.apply(null, successArgs); + } + return this; + }, + done: function(callback) { + if (state === 0) { + doneCallbacks.push(callback); + } else { + callback.apply(null, failureArgs === undefined ? successArgs : failureArgs); + } + return this; + }, + success: function() { + state = 1; + successArgs = arguments; + if (successCallbacks.length) { + qq.each(successCallbacks, function(idx, callback) { + callback.apply(null, successArgs); + }); + } + if (doneCallbacks.length) { + qq.each(doneCallbacks, function(idx, callback) { + callback.apply(null, successArgs); + }); + } + return this; + }, + failure: function() { + state = -1; + failureArgs = arguments; + if (failureCallbacks.length) { + qq.each(failureCallbacks, function(idx, callback) { + callback.apply(null, failureArgs); + }); + } + if (doneCallbacks.length) { + qq.each(doneCallbacks, function(idx, callback) { + callback.apply(null, failureArgs); + }); + } + return this; + } + }); + }; + qq.BlobProxy = function(referenceBlob, onCreate) { + "use strict"; + qq.extend(this, { + referenceBlob: referenceBlob, + create: function() { + return onCreate(referenceBlob); + } + }); + }; + qq.UploadButton = function(o) { + "use strict"; + var self = this, disposeSupport = new qq.DisposeSupport(), options = { + acceptFiles: null, + element: null, + focusClass: "qq-upload-button-focus", + folders: false, + hoverClass: "qq-upload-button-hover", + ios8BrowserCrashWorkaround: false, + multiple: false, + name: "qqfile", + onChange: function(input) {}, + title: null + }, input, buttonId; + qq.extend(options, o); + buttonId = qq.getUniqueId(); + function createInput() { + var input = document.createElement("input"); + input.setAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME, buttonId); + input.setAttribute("title", options.title); + self.setMultiple(options.multiple, input); + if (options.folders && qq.supportedFeatures.folderSelection) { + input.setAttribute("webkitdirectory", ""); + } + if (options.acceptFiles) { + input.setAttribute("accept", options.acceptFiles); + } + input.setAttribute("type", "file"); + input.setAttribute("name", options.name); + qq(input).css({ + position: "absolute", + right: 0, + top: 0, + fontFamily: "Arial", + fontSize: qq.ie() && !qq.ie8() ? "3500px" : "118px", + margin: 0, + padding: 0, + cursor: "pointer", + opacity: 0 + }); + !qq.ie7() && qq(input).css({ + height: "100%" + }); + options.element.appendChild(input); + disposeSupport.attach(input, "change", function() { + options.onChange(input); + }); + disposeSupport.attach(input, "mouseover", function() { + qq(options.element).addClass(options.hoverClass); + }); + disposeSupport.attach(input, "mouseout", function() { + qq(options.element).removeClass(options.hoverClass); + }); + disposeSupport.attach(input, "focus", function() { + qq(options.element).addClass(options.focusClass); + }); + disposeSupport.attach(input, "blur", function() { + qq(options.element).removeClass(options.focusClass); + }); + return input; + } + qq(options.element).css({ + position: "relative", + overflow: "hidden", + direction: "ltr" + }); + qq.extend(this, { + getInput: function() { + return input; + }, + getButtonId: function() { + return buttonId; + }, + setMultiple: function(isMultiple, optInput) { + var input = optInput || this.getInput(); + if (options.ios8BrowserCrashWorkaround && qq.ios8() && (qq.iosChrome() || qq.iosSafariWebView())) { + input.setAttribute("multiple", ""); + } else { + if (isMultiple) { + input.setAttribute("multiple", ""); + } else { + input.removeAttribute("multiple"); + } + } + }, + setAcceptFiles: function(acceptFiles) { + if (acceptFiles !== options.acceptFiles) { + input.setAttribute("accept", acceptFiles); + } + }, + reset: function() { + if (input.parentNode) { + qq(input).remove(); + } + qq(options.element).removeClass(options.focusClass); + input = null; + input = createInput(); + } + }); + input = createInput(); + }; + qq.UploadButton.BUTTON_ID_ATTR_NAME = "qq-button-id"; + qq.UploadData = function(uploaderProxy) { + "use strict"; + var data = [], byUuid = {}, byStatus = {}, byProxyGroupId = {}, byBatchId = {}; + function getDataByIds(idOrIds) { + if (qq.isArray(idOrIds)) { + var entries = []; + qq.each(idOrIds, function(idx, id) { + entries.push(data[id]); + }); + return entries; + } + return data[idOrIds]; + } + function getDataByUuids(uuids) { + if (qq.isArray(uuids)) { + var entries = []; + qq.each(uuids, function(idx, uuid) { + entries.push(data[byUuid[uuid]]); + }); + return entries; + } + return data[byUuid[uuids]]; + } + function getDataByStatus(status) { + var statusResults = [], statuses = [].concat(status); + qq.each(statuses, function(index, statusEnum) { + var statusResultIndexes = byStatus[statusEnum]; + if (statusResultIndexes !== undefined) { + qq.each(statusResultIndexes, function(i, dataIndex) { + statusResults.push(data[dataIndex]); + }); + } + }); + return statusResults; + } + qq.extend(this, { + addFile: function(spec) { + var status = spec.status || qq.status.SUBMITTING, id = data.push({ + name: spec.name, + originalName: spec.name, + uuid: spec.uuid, + size: spec.size == null ? -1 : spec.size, + status: status + }) - 1; + if (spec.batchId) { + data[id].batchId = spec.batchId; + if (byBatchId[spec.batchId] === undefined) { + byBatchId[spec.batchId] = []; + } + byBatchId[spec.batchId].push(id); + } + if (spec.proxyGroupId) { + data[id].proxyGroupId = spec.proxyGroupId; + if (byProxyGroupId[spec.proxyGroupId] === undefined) { + byProxyGroupId[spec.proxyGroupId] = []; + } + byProxyGroupId[spec.proxyGroupId].push(id); + } + data[id].id = id; + byUuid[spec.uuid] = id; + if (byStatus[status] === undefined) { + byStatus[status] = []; + } + byStatus[status].push(id); + uploaderProxy.onStatusChange(id, null, status); + return id; + }, + retrieve: function(optionalFilter) { + if (qq.isObject(optionalFilter) && data.length) { + if (optionalFilter.id !== undefined) { + return getDataByIds(optionalFilter.id); + } else if (optionalFilter.uuid !== undefined) { + return getDataByUuids(optionalFilter.uuid); + } else if (optionalFilter.status) { + return getDataByStatus(optionalFilter.status); + } + } else { + return qq.extend([], data, true); + } + }, + reset: function() { + data = []; + byUuid = {}; + byStatus = {}; + byBatchId = {}; + }, + setStatus: function(id, newStatus) { + var oldStatus = data[id].status, byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], id); + byStatus[oldStatus].splice(byStatusOldStatusIndex, 1); + data[id].status = newStatus; + if (byStatus[newStatus] === undefined) { + byStatus[newStatus] = []; + } + byStatus[newStatus].push(id); + uploaderProxy.onStatusChange(id, oldStatus, newStatus); + }, + uuidChanged: function(id, newUuid) { + var oldUuid = data[id].uuid; + data[id].uuid = newUuid; + byUuid[newUuid] = id; + delete byUuid[oldUuid]; + }, + updateName: function(id, newName) { + data[id].name = newName; + }, + updateSize: function(id, newSize) { + data[id].size = newSize; + }, + setParentId: function(targetId, parentId) { + data[targetId].parentId = parentId; + }, + getIdsInProxyGroup: function(id) { + var proxyGroupId = data[id].proxyGroupId; + if (proxyGroupId) { + return byProxyGroupId[proxyGroupId]; + } + return []; + }, + getIdsInBatch: function(id) { + var batchId = data[id].batchId; + return byBatchId[batchId]; + } + }); + }; + qq.status = { + SUBMITTING: "submitting", + SUBMITTED: "submitted", + REJECTED: "rejected", + QUEUED: "queued", + CANCELED: "canceled", + PAUSED: "paused", + UPLOADING: "uploading", + UPLOAD_RETRYING: "retrying upload", + UPLOAD_SUCCESSFUL: "upload successful", + UPLOAD_FAILED: "upload failed", + DELETE_FAILED: "delete failed", + DELETING: "deleting", + DELETED: "deleted" + }; + (function() { + "use strict"; + qq.basePublicApi = { + addBlobs: function(blobDataOrArray, params, endpoint) { + this.addFiles(blobDataOrArray, params, endpoint); + }, + addInitialFiles: function(cannedFileList) { + var self = this; + qq.each(cannedFileList, function(index, cannedFile) { + self._addCannedFile(cannedFile); + }); + }, + addFiles: function(data, params, endpoint) { + this._maybeHandleIos8SafariWorkaround(); + var batchId = this._storedIds.length === 0 ? qq.getUniqueId() : this._currentBatchId, processBlob = qq.bind(function(blob) { + this._handleNewFile({ + blob: blob, + name: this._options.blobs.defaultName + }, batchId, verifiedFiles); + }, this), processBlobData = qq.bind(function(blobData) { + this._handleNewFile(blobData, batchId, verifiedFiles); + }, this), processCanvas = qq.bind(function(canvas) { + var blob = qq.canvasToBlob(canvas); + this._handleNewFile({ + blob: blob, + name: this._options.blobs.defaultName + ".png" + }, batchId, verifiedFiles); + }, this), processCanvasData = qq.bind(function(canvasData) { + var normalizedQuality = canvasData.quality && canvasData.quality / 100, blob = qq.canvasToBlob(canvasData.canvas, canvasData.type, normalizedQuality); + this._handleNewFile({ + blob: blob, + name: canvasData.name + }, batchId, verifiedFiles); + }, this), processFileOrInput = qq.bind(function(fileOrInput) { + if (qq.isInput(fileOrInput) && qq.supportedFeatures.ajaxUploading) { + var files = Array.prototype.slice.call(fileOrInput.files), self = this; + qq.each(files, function(idx, file) { + self._handleNewFile(file, batchId, verifiedFiles); + }); + } else { + this._handleNewFile(fileOrInput, batchId, verifiedFiles); + } + }, this), normalizeData = function() { + if (qq.isFileList(data)) { + data = Array.prototype.slice.call(data); + } + data = [].concat(data); + }, self = this, verifiedFiles = []; + this._currentBatchId = batchId; + if (data) { + normalizeData(); + qq.each(data, function(idx, fileContainer) { + if (qq.isFileOrInput(fileContainer)) { + processFileOrInput(fileContainer); + } else if (qq.isBlob(fileContainer)) { + processBlob(fileContainer); + } else if (qq.isObject(fileContainer)) { + if (fileContainer.blob && fileContainer.name) { + processBlobData(fileContainer); + } else if (fileContainer.canvas && fileContainer.name) { + processCanvasData(fileContainer); + } + } else if (fileContainer.tagName && fileContainer.tagName.toLowerCase() === "canvas") { + processCanvas(fileContainer); + } else { + self.log(fileContainer + " is not a valid file container! Ignoring!", "warn"); + } + }); + this.log("Received " + verifiedFiles.length + " files."); + this._prepareItemsForUpload(verifiedFiles, params, endpoint); + } + }, + cancel: function(id) { + this._handler.cancel(id); + }, + cancelAll: function() { + var storedIdsCopy = [], self = this; + qq.extend(storedIdsCopy, this._storedIds); + qq.each(storedIdsCopy, function(idx, storedFileId) { + self.cancel(storedFileId); + }); + this._handler.cancelAll(); + }, + clearStoredFiles: function() { + this._storedIds = []; + }, + continueUpload: function(id) { + var uploadData = this._uploadData.retrieve({ + id: id + }); + if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) { + return false; + } + if (uploadData.status === qq.status.PAUSED) { + this.log(qq.format("Paused file ID {} ({}) will be continued. Not paused.", id, this.getName(id))); + this._uploadFile(id); + return true; + } else { + this.log(qq.format("Ignoring continue for file ID {} ({}). Not paused.", id, this.getName(id)), "error"); + } + return false; + }, + deleteFile: function(id) { + return this._onSubmitDelete(id); + }, + doesExist: function(fileOrBlobId) { + return this._handler.isValid(fileOrBlobId); + }, + drawThumbnail: function(fileId, imgOrCanvas, maxSize, fromServer, customResizeFunction) { + var promiseToReturn = new qq.Promise(), fileOrUrl, options; + if (this._imageGenerator) { + fileOrUrl = this._thumbnailUrls[fileId]; + options = { + customResizeFunction: customResizeFunction, + maxSize: maxSize > 0 ? maxSize : null, + scale: maxSize > 0 + }; + if (!fromServer && qq.supportedFeatures.imagePreviews) { + fileOrUrl = this.getFile(fileId); + } + if (fileOrUrl == null) { + promiseToReturn.failure({ + container: imgOrCanvas, + error: "File or URL not found." + }); + } else { + this._imageGenerator.generate(fileOrUrl, imgOrCanvas, options).then(function success(modifiedContainer) { + promiseToReturn.success(modifiedContainer); + }, function failure(container, reason) { + promiseToReturn.failure({ + container: container, + error: reason || "Problem generating thumbnail" + }); + }); + } + } else { + promiseToReturn.failure({ + container: imgOrCanvas, + error: "Missing image generator module" + }); + } + return promiseToReturn; + }, + getButton: function(fileId) { + return this._getButton(this._buttonIdsForFileIds[fileId]); + }, + getEndpoint: function(fileId) { + return this._endpointStore.get(fileId); + }, + getFile: function(fileOrBlobId) { + return this._handler.getFile(fileOrBlobId) || null; + }, + getInProgress: function() { + return this._uploadData.retrieve({ + status: [ qq.status.UPLOADING, qq.status.UPLOAD_RETRYING, qq.status.QUEUED ] + }).length; + }, + getName: function(id) { + return this._uploadData.retrieve({ + id: id + }).name; + }, + getParentId: function(id) { + var uploadDataEntry = this.getUploads({ + id: id + }), parentId = null; + if (uploadDataEntry) { + if (uploadDataEntry.parentId !== undefined) { + parentId = uploadDataEntry.parentId; + } + } + return parentId; + }, + getResumableFilesData: function() { + return this._handler.getResumableFilesData(); + }, + getSize: function(id) { + return this._uploadData.retrieve({ + id: id + }).size; + }, + getNetUploads: function() { + return this._netUploaded; + }, + getRemainingAllowedItems: function() { + var allowedItems = this._currentItemLimit; + if (allowedItems > 0) { + return allowedItems - this._netUploadedOrQueued; + } + return null; + }, + getUploads: function(optionalFilter) { + return this._uploadData.retrieve(optionalFilter); + }, + getUuid: function(id) { + return this._uploadData.retrieve({ + id: id + }).uuid; + }, + log: function(str, level) { + if (this._options.debug && (!level || level === "info")) { + qq.log("[Fine Uploader " + qq.version + "] " + str); + } else if (level && level !== "info") { + qq.log("[Fine Uploader " + qq.version + "] " + str, level); + } + }, + pauseUpload: function(id) { + var uploadData = this._uploadData.retrieve({ + id: id + }); + if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) { + return false; + } + if (qq.indexOf([ qq.status.UPLOADING, qq.status.UPLOAD_RETRYING ], uploadData.status) >= 0) { + if (this._handler.pause(id)) { + this._uploadData.setStatus(id, qq.status.PAUSED); + return true; + } else { + this.log(qq.format("Unable to pause file ID {} ({}).", id, this.getName(id)), "error"); + } + } else { + this.log(qq.format("Ignoring pause for file ID {} ({}). Not in progress.", id, this.getName(id)), "error"); + } + return false; + }, + reset: function() { + this.log("Resetting uploader..."); + this._handler.reset(); + this._storedIds = []; + this._autoRetries = []; + this._retryTimeouts = []; + this._preventRetries = []; + this._thumbnailUrls = []; + qq.each(this._buttons, function(idx, button) { + button.reset(); + }); + this._paramsStore.reset(); + this._endpointStore.reset(); + this._netUploadedOrQueued = 0; + this._netUploaded = 0; + this._uploadData.reset(); + this._buttonIdsForFileIds = []; + this._pasteHandler && this._pasteHandler.reset(); + this._options.session.refreshOnReset && this._refreshSessionData(); + this._succeededSinceLastAllComplete = []; + this._failedSinceLastAllComplete = []; + this._totalProgress && this._totalProgress.reset(); + }, + retry: function(id) { + return this._manualRetry(id); + }, + scaleImage: function(id, specs) { + var self = this; + return qq.Scaler.prototype.scaleImage(id, specs, { + log: qq.bind(self.log, self), + getFile: qq.bind(self.getFile, self), + uploadData: self._uploadData + }); + }, + setCustomHeaders: function(headers, id) { + this._customHeadersStore.set(headers, id); + }, + setDeleteFileCustomHeaders: function(headers, id) { + this._deleteFileCustomHeadersStore.set(headers, id); + }, + setDeleteFileEndpoint: function(endpoint, id) { + this._deleteFileEndpointStore.set(endpoint, id); + }, + setDeleteFileParams: function(params, id) { + this._deleteFileParamsStore.set(params, id); + }, + setEndpoint: function(endpoint, id) { + this._endpointStore.set(endpoint, id); + }, + setForm: function(elementOrId) { + this._updateFormSupportAndParams(elementOrId); + }, + setItemLimit: function(newItemLimit) { + this._currentItemLimit = newItemLimit; + }, + setName: function(id, newName) { + this._uploadData.updateName(id, newName); + }, + setParams: function(params, id) { + this._paramsStore.set(params, id); + }, + setUuid: function(id, newUuid) { + return this._uploadData.uuidChanged(id, newUuid); + }, + uploadStoredFiles: function() { + if (this._storedIds.length === 0) { + this._itemError("noFilesError"); + } else { + this._uploadStoredFiles(); + } + } + }; + qq.basePrivateApi = { + _addCannedFile: function(sessionData) { + var id = this._uploadData.addFile({ + uuid: sessionData.uuid, + name: sessionData.name, + size: sessionData.size, + status: qq.status.UPLOAD_SUCCESSFUL + }); + sessionData.deleteFileEndpoint && this.setDeleteFileEndpoint(sessionData.deleteFileEndpoint, id); + sessionData.deleteFileParams && this.setDeleteFileParams(sessionData.deleteFileParams, id); + if (sessionData.thumbnailUrl) { + this._thumbnailUrls[id] = sessionData.thumbnailUrl; + } + this._netUploaded++; + this._netUploadedOrQueued++; + return id; + }, + _annotateWithButtonId: function(file, associatedInput) { + if (qq.isFile(file)) { + file.qqButtonId = this._getButtonId(associatedInput); + } + }, + _batchError: function(message) { + this._options.callbacks.onError(null, null, message, undefined); + }, + _createDeleteHandler: function() { + var self = this; + return new qq.DeleteFileAjaxRequester({ + method: this._options.deleteFile.method.toUpperCase(), + maxConnections: this._options.maxConnections, + uuidParamName: this._options.request.uuidName, + customHeaders: this._deleteFileCustomHeadersStore, + paramsStore: this._deleteFileParamsStore, + endpointStore: this._deleteFileEndpointStore, + cors: this._options.cors, + log: qq.bind(self.log, self), + onDelete: function(id) { + self._onDelete(id); + self._options.callbacks.onDelete(id); + }, + onDeleteComplete: function(id, xhrOrXdr, isError) { + self._onDeleteComplete(id, xhrOrXdr, isError); + self._options.callbacks.onDeleteComplete(id, xhrOrXdr, isError); + } + }); + }, + _createPasteHandler: function() { + var self = this; + return new qq.PasteSupport({ + targetElement: this._options.paste.targetElement, + callbacks: { + log: qq.bind(self.log, self), + pasteReceived: function(blob) { + self._handleCheckedCallback({ + name: "onPasteReceived", + callback: qq.bind(self._options.callbacks.onPasteReceived, self, blob), + onSuccess: qq.bind(self._handlePasteSuccess, self, blob), + identifier: "pasted image" + }); + } + } + }); + }, + _createStore: function(initialValue, _readOnlyValues_) { + var store = {}, catchall = initialValue, perIdReadOnlyValues = {}, readOnlyValues = _readOnlyValues_, copy = function(orig) { + if (qq.isObject(orig)) { + return qq.extend({}, orig); + } + return orig; + }, getReadOnlyValues = function() { + if (qq.isFunction(readOnlyValues)) { + return readOnlyValues(); + } + return readOnlyValues; + }, includeReadOnlyValues = function(id, existing) { + if (readOnlyValues && qq.isObject(existing)) { + qq.extend(existing, getReadOnlyValues()); + } + if (perIdReadOnlyValues[id]) { + qq.extend(existing, perIdReadOnlyValues[id]); + } + }; + return { + set: function(val, id) { + if (id == null) { + store = {}; + catchall = copy(val); + } else { + store[id] = copy(val); + } + }, + get: function(id) { + var values; + if (id != null && store[id]) { + values = store[id]; + } else { + values = copy(catchall); + } + includeReadOnlyValues(id, values); + return copy(values); + }, + addReadOnly: function(id, values) { + if (qq.isObject(store)) { + if (id === null) { + if (qq.isFunction(values)) { + readOnlyValues = values; + } else { + readOnlyValues = readOnlyValues || {}; + qq.extend(readOnlyValues, values); + } + } else { + perIdReadOnlyValues[id] = perIdReadOnlyValues[id] || {}; + qq.extend(perIdReadOnlyValues[id], values); + } + } + }, + remove: function(fileId) { + return delete store[fileId]; + }, + reset: function() { + store = {}; + perIdReadOnlyValues = {}; + catchall = initialValue; + } + }; + }, + _createUploadDataTracker: function() { + var self = this; + return new qq.UploadData({ + getName: function(id) { + return self.getName(id); + }, + getUuid: function(id) { + return self.getUuid(id); + }, + getSize: function(id) { + return self.getSize(id); + }, + onStatusChange: function(id, oldStatus, newStatus) { + self._onUploadStatusChange(id, oldStatus, newStatus); + self._options.callbacks.onStatusChange(id, oldStatus, newStatus); + self._maybeAllComplete(id, newStatus); + if (self._totalProgress) { + setTimeout(function() { + self._totalProgress.onStatusChange(id, oldStatus, newStatus); + }, 0); + } + } + }); + }, + _createUploadButton: function(spec) { + var self = this, acceptFiles = spec.accept || this._options.validation.acceptFiles, allowedExtensions = spec.allowedExtensions || this._options.validation.allowedExtensions, button; + function allowMultiple() { + if (qq.supportedFeatures.ajaxUploading) { + if (self._options.workarounds.iosEmptyVideos && qq.ios() && !qq.ios6() && self._isAllowedExtension(allowedExtensions, ".mov")) { + return false; + } + if (spec.multiple === undefined) { + return self._options.multiple; + } + return spec.multiple; + } + return false; + } + button = new qq.UploadButton({ + acceptFiles: acceptFiles, + element: spec.element, + focusClass: this._options.classes.buttonFocus, + folders: spec.folders, + hoverClass: this._options.classes.buttonHover, + ios8BrowserCrashWorkaround: this._options.workarounds.ios8BrowserCrash, + multiple: allowMultiple(), + name: this._options.request.inputName, + onChange: function(input) { + self._onInputChange(input); + }, + title: spec.title == null ? this._options.text.fileInputTitle : spec.title + }); + this._disposeSupport.addDisposer(function() { + button.dispose(); + }); + self._buttons.push(button); + return button; + }, + _createUploadHandler: function(additionalOptions, namespace) { + var self = this, lastOnProgress = {}, options = { + debug: this._options.debug, + maxConnections: this._options.maxConnections, + cors: this._options.cors, + paramsStore: this._paramsStore, + endpointStore: this._endpointStore, + chunking: this._options.chunking, + resume: this._options.resume, + blobs: this._options.blobs, + log: qq.bind(self.log, self), + preventRetryParam: this._options.retry.preventRetryResponseProperty, + onProgress: function(id, name, loaded, total) { + if (loaded < 0 || total < 0) { + return; + } + if (lastOnProgress[id]) { + if (lastOnProgress[id].loaded !== loaded || lastOnProgress[id].total !== total) { + self._onProgress(id, name, loaded, total); + self._options.callbacks.onProgress(id, name, loaded, total); + } + } else { + self._onProgress(id, name, loaded, total); + self._options.callbacks.onProgress(id, name, loaded, total); + } + lastOnProgress[id] = { + loaded: loaded, + total: total + }; + }, + onComplete: function(id, name, result, xhr) { + delete lastOnProgress[id]; + var status = self.getUploads({ + id: id + }).status, retVal; + if (status === qq.status.UPLOAD_SUCCESSFUL || status === qq.status.UPLOAD_FAILED) { + return; + } + retVal = self._onComplete(id, name, result, xhr); + if (retVal instanceof qq.Promise) { + retVal.done(function() { + self._options.callbacks.onComplete(id, name, result, xhr); + }); + } else { + self._options.callbacks.onComplete(id, name, result, xhr); + } + }, + onCancel: function(id, name, cancelFinalizationEffort) { + var promise = new qq.Promise(); + self._handleCheckedCallback({ + name: "onCancel", + callback: qq.bind(self._options.callbacks.onCancel, self, id, name), + onFailure: promise.failure, + onSuccess: function() { + cancelFinalizationEffort.then(function() { + self._onCancel(id, name); + }); + promise.success(); + }, + identifier: id + }); + return promise; + }, + onUploadPrep: qq.bind(this._onUploadPrep, this), + onUpload: function(id, name) { + self._onUpload(id, name); + self._options.callbacks.onUpload(id, name); + }, + onUploadChunk: function(id, name, chunkData) { + self._onUploadChunk(id, chunkData); + self._options.callbacks.onUploadChunk(id, name, chunkData); + }, + onUploadChunkSuccess: function(id, chunkData, result, xhr) { + self._options.callbacks.onUploadChunkSuccess.apply(self, arguments); + }, + onResume: function(id, name, chunkData) { + return self._options.callbacks.onResume(id, name, chunkData); + }, + onAutoRetry: function(id, name, responseJSON, xhr) { + return self._onAutoRetry.apply(self, arguments); + }, + onUuidChanged: function(id, newUuid) { + self.log("Server requested UUID change from '" + self.getUuid(id) + "' to '" + newUuid + "'"); + self.setUuid(id, newUuid); + }, + getName: qq.bind(self.getName, self), + getUuid: qq.bind(self.getUuid, self), + getSize: qq.bind(self.getSize, self), + setSize: qq.bind(self._setSize, self), + getDataByUuid: function(uuid) { + return self.getUploads({ + uuid: uuid + }); + }, + isQueued: function(id) { + var status = self.getUploads({ + id: id + }).status; + return status === qq.status.QUEUED || status === qq.status.SUBMITTED || status === qq.status.UPLOAD_RETRYING || status === qq.status.PAUSED; + }, + getIdsInProxyGroup: self._uploadData.getIdsInProxyGroup, + getIdsInBatch: self._uploadData.getIdsInBatch + }; + qq.each(this._options.request, function(prop, val) { + options[prop] = val; + }); + options.customHeaders = this._customHeadersStore; + if (additionalOptions) { + qq.each(additionalOptions, function(key, val) { + options[key] = val; + }); + } + return new qq.UploadHandlerController(options, namespace); + }, + _fileOrBlobRejected: function(id) { + this._netUploadedOrQueued--; + this._uploadData.setStatus(id, qq.status.REJECTED); + }, + _formatSize: function(bytes) { + if (bytes === 0) { + return bytes + this._options.text.sizeSymbols[0]; + } + var i = -1; + do { + bytes = bytes / 1e3; + i++; + } while (bytes > 999); + return Math.max(bytes, .1).toFixed(1) + this._options.text.sizeSymbols[i]; + }, + _generateExtraButtonSpecs: function() { + var self = this; + this._extraButtonSpecs = {}; + qq.each(this._options.extraButtons, function(idx, extraButtonOptionEntry) { + var multiple = extraButtonOptionEntry.multiple, validation = qq.extend({}, self._options.validation, true), extraButtonSpec = qq.extend({}, extraButtonOptionEntry); + if (multiple === undefined) { + multiple = self._options.multiple; + } + if (extraButtonSpec.validation) { + qq.extend(validation, extraButtonOptionEntry.validation, true); + } + qq.extend(extraButtonSpec, { + multiple: multiple, + validation: validation + }, true); + self._initExtraButton(extraButtonSpec); + }); + }, + _getButton: function(buttonId) { + var extraButtonsSpec = this._extraButtonSpecs[buttonId]; + if (extraButtonsSpec) { + return extraButtonsSpec.element; + } else if (buttonId === this._defaultButtonId) { + return this._options.button; + } + }, + _getButtonId: function(buttonOrFileInputOrFile) { + var inputs, fileInput, fileBlobOrInput = buttonOrFileInputOrFile; + if (fileBlobOrInput instanceof qq.BlobProxy) { + fileBlobOrInput = fileBlobOrInput.referenceBlob; + } + if (fileBlobOrInput && !qq.isBlob(fileBlobOrInput)) { + if (qq.isFile(fileBlobOrInput)) { + return fileBlobOrInput.qqButtonId; + } else if (fileBlobOrInput.tagName.toLowerCase() === "input" && fileBlobOrInput.type.toLowerCase() === "file") { + return fileBlobOrInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME); + } + inputs = fileBlobOrInput.getElementsByTagName("input"); + qq.each(inputs, function(idx, input) { + if (input.getAttribute("type") === "file") { + fileInput = input; + return false; + } + }); + if (fileInput) { + return fileInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME); + } + } + }, + _getNotFinished: function() { + return this._uploadData.retrieve({ + status: [ qq.status.UPLOADING, qq.status.UPLOAD_RETRYING, qq.status.QUEUED, qq.status.SUBMITTING, qq.status.SUBMITTED, qq.status.PAUSED ] + }).length; + }, + _getValidationBase: function(buttonId) { + var extraButtonSpec = this._extraButtonSpecs[buttonId]; + return extraButtonSpec ? extraButtonSpec.validation : this._options.validation; + }, + _getValidationDescriptor: function(fileWrapper) { + if (fileWrapper.file instanceof qq.BlobProxy) { + return { + name: qq.getFilename(fileWrapper.file.referenceBlob), + size: fileWrapper.file.referenceBlob.size + }; + } + return { + name: this.getUploads({ + id: fileWrapper.id + }).name, + size: this.getUploads({ + id: fileWrapper.id + }).size + }; + }, + _getValidationDescriptors: function(fileWrappers) { + var self = this, fileDescriptors = []; + qq.each(fileWrappers, function(idx, fileWrapper) { + fileDescriptors.push(self._getValidationDescriptor(fileWrapper)); + }); + return fileDescriptors; + }, + _handleCameraAccess: function() { + if (this._options.camera.ios && qq.ios()) { + var acceptIosCamera = "image/*;capture=camera", button = this._options.camera.button, buttonId = button ? this._getButtonId(button) : this._defaultButtonId, optionRoot = this._options; + if (buttonId && buttonId !== this._defaultButtonId) { + optionRoot = this._extraButtonSpecs[buttonId]; + } + optionRoot.multiple = false; + if (optionRoot.validation.acceptFiles === null) { + optionRoot.validation.acceptFiles = acceptIosCamera; + } else { + optionRoot.validation.acceptFiles += "," + acceptIosCamera; + } + qq.each(this._buttons, function(idx, button) { + if (button.getButtonId() === buttonId) { + button.setMultiple(optionRoot.multiple); + button.setAcceptFiles(optionRoot.acceptFiles); + return false; + } + }); + } + }, + _handleCheckedCallback: function(details) { + var self = this, callbackRetVal = details.callback(); + if (qq.isGenericPromise(callbackRetVal)) { + this.log(details.name + " - waiting for " + details.name + " promise to be fulfilled for " + details.identifier); + return callbackRetVal.then(function(successParam) { + self.log(details.name + " promise success for " + details.identifier); + details.onSuccess(successParam); + }, function() { + if (details.onFailure) { + self.log(details.name + " promise failure for " + details.identifier); + details.onFailure(); + } else { + self.log(details.name + " promise failure for " + details.identifier); + } + }); + } + if (callbackRetVal !== false) { + details.onSuccess(callbackRetVal); + } else { + if (details.onFailure) { + this.log(details.name + " - return value was 'false' for " + details.identifier + ". Invoking failure callback."); + details.onFailure(); + } else { + this.log(details.name + " - return value was 'false' for " + details.identifier + ". Will not proceed."); + } + } + return callbackRetVal; + }, + _handleNewFile: function(file, batchId, newFileWrapperList) { + var self = this, uuid = qq.getUniqueId(), size = -1, name = qq.getFilename(file), actualFile = file.blob || file, handler = this._customNewFileHandler ? this._customNewFileHandler : qq.bind(self._handleNewFileGeneric, self); + if (!qq.isInput(actualFile) && actualFile.size >= 0) { + size = actualFile.size; + } + handler(actualFile, name, uuid, size, newFileWrapperList, batchId, this._options.request.uuidName, { + uploadData: self._uploadData, + paramsStore: self._paramsStore, + addFileToHandler: function(id, file) { + self._handler.add(id, file); + self._netUploadedOrQueued++; + self._trackButton(id); + } + }); + }, + _handleNewFileGeneric: function(file, name, uuid, size, fileList, batchId) { + var id = this._uploadData.addFile({ + uuid: uuid, + name: name, + size: size, + batchId: batchId + }); + this._handler.add(id, file); + this._trackButton(id); + this._netUploadedOrQueued++; + fileList.push({ + id: id, + file: file + }); + }, + _handlePasteSuccess: function(blob, extSuppliedName) { + var extension = blob.type.split("/")[1], name = extSuppliedName; + if (name == null) { + name = this._options.paste.defaultName; + } + name += "." + extension; + this.addFiles({ + name: name, + blob: blob + }); + }, + _initExtraButton: function(spec) { + var button = this._createUploadButton({ + accept: spec.validation.acceptFiles, + allowedExtensions: spec.validation.allowedExtensions, + element: spec.element, + folders: spec.folders, + multiple: spec.multiple, + title: spec.fileInputTitle + }); + this._extraButtonSpecs[button.getButtonId()] = spec; + }, + _initFormSupportAndParams: function() { + this._formSupport = qq.FormSupport && new qq.FormSupport(this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this)); + if (this._formSupport && this._formSupport.attachedToForm) { + this._paramsStore = this._createStore(this._options.request.params, this._formSupport.getFormInputsAsObject); + this._options.autoUpload = this._formSupport.newAutoUpload; + if (this._formSupport.newEndpoint) { + this._options.request.endpoint = this._formSupport.newEndpoint; + } + } else { + this._paramsStore = this._createStore(this._options.request.params); + } + }, + _isDeletePossible: function() { + if (!qq.DeleteFileAjaxRequester || !this._options.deleteFile.enabled) { + return false; + } + if (this._options.cors.expected) { + if (qq.supportedFeatures.deleteFileCorsXhr) { + return true; + } + if (qq.supportedFeatures.deleteFileCorsXdr && this._options.cors.allowXdr) { + return true; + } + return false; + } + return true; + }, + _isAllowedExtension: function(allowed, fileName) { + var valid = false; + if (!allowed.length) { + return true; + } + qq.each(allowed, function(idx, allowedExt) { + if (qq.isString(allowedExt)) { + var extRegex = new RegExp("\\." + allowedExt + "$", "i"); + if (fileName.match(extRegex) != null) { + valid = true; + return false; + } + } + }); + return valid; + }, + _itemError: function(code, maybeNameOrNames, item) { + var message = this._options.messages[code], allowedExtensions = [], names = [].concat(maybeNameOrNames), name = names[0], buttonId = this._getButtonId(item), validationBase = this._getValidationBase(buttonId), extensionsForMessage, placeholderMatch; + function r(name, replacement) { + message = message.replace(name, replacement); + } + qq.each(validationBase.allowedExtensions, function(idx, allowedExtension) { + if (qq.isString(allowedExtension)) { + allowedExtensions.push(allowedExtension); + } + }); + extensionsForMessage = allowedExtensions.join(", ").toLowerCase(); + r("{file}", this._options.formatFileName(name)); + r("{extensions}", extensionsForMessage); + r("{sizeLimit}", this._formatSize(validationBase.sizeLimit)); + r("{minSizeLimit}", this._formatSize(validationBase.minSizeLimit)); + placeholderMatch = message.match(/(\{\w+\})/g); + if (placeholderMatch !== null) { + qq.each(placeholderMatch, function(idx, placeholder) { + r(placeholder, names[idx]); + }); + } + this._options.callbacks.onError(null, name, message, undefined); + return message; + }, + _manualRetry: function(id, callback) { + if (this._onBeforeManualRetry(id)) { + this._netUploadedOrQueued++; + this._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING); + if (callback) { + callback(id); + } else { + this._handler.retry(id); + } + return true; + } + }, + _maybeAllComplete: function(id, status) { + var self = this, notFinished = this._getNotFinished(); + if (status === qq.status.UPLOAD_SUCCESSFUL) { + this._succeededSinceLastAllComplete.push(id); + } else if (status === qq.status.UPLOAD_FAILED) { + this._failedSinceLastAllComplete.push(id); + } + if (notFinished === 0 && (this._succeededSinceLastAllComplete.length || this._failedSinceLastAllComplete.length)) { + setTimeout(function() { + self._onAllComplete(self._succeededSinceLastAllComplete, self._failedSinceLastAllComplete); + }, 0); + } + }, + _maybeHandleIos8SafariWorkaround: function() { + var self = this; + if (this._options.workarounds.ios8SafariUploads && qq.ios800() && qq.iosSafari()) { + setTimeout(function() { + window.alert(self._options.messages.unsupportedBrowserIos8Safari); + }, 0); + throw new qq.Error(this._options.messages.unsupportedBrowserIos8Safari); + } + }, + _maybeParseAndSendUploadError: function(id, name, response, xhr) { + if (!response.success) { + if (xhr && xhr.status !== 200 && !response.error) { + this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status, xhr); + } else { + var errorReason = response.error ? response.error : this._options.text.defaultResponseError; + this._options.callbacks.onError(id, name, errorReason, xhr); + } + } + }, + _maybeProcessNextItemAfterOnValidateCallback: function(validItem, items, index, params, endpoint) { + var self = this; + if (items.length > index) { + if (validItem || !this._options.validation.stopOnFirstInvalidFile) { + setTimeout(function() { + var validationDescriptor = self._getValidationDescriptor(items[index]), buttonId = self._getButtonId(items[index].file), button = self._getButton(buttonId); + self._handleCheckedCallback({ + name: "onValidate", + callback: qq.bind(self._options.callbacks.onValidate, self, validationDescriptor, button), + onSuccess: qq.bind(self._onValidateCallbackSuccess, self, items, index, params, endpoint), + onFailure: qq.bind(self._onValidateCallbackFailure, self, items, index, params, endpoint), + identifier: "Item '" + validationDescriptor.name + "', size: " + validationDescriptor.size + }); + }, 0); + } else if (!validItem) { + for (;index < items.length; index++) { + self._fileOrBlobRejected(items[index].id); + } + } + } + }, + _onAllComplete: function(successful, failed) { + this._totalProgress && this._totalProgress.onAllComplete(successful, failed, this._preventRetries); + this._options.callbacks.onAllComplete(qq.extend([], successful), qq.extend([], failed)); + this._succeededSinceLastAllComplete = []; + this._failedSinceLastAllComplete = []; + }, + _onAutoRetry: function(id, name, responseJSON, xhr, callback) { + var self = this; + self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty]; + if (self._shouldAutoRetry(id, name, responseJSON)) { + var retryWaitPeriod = self._options.retry.autoAttemptDelay * 1e3; + self._maybeParseAndSendUploadError.apply(self, arguments); + self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id]); + self._onBeforeAutoRetry(id, name); + self._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING); + self._retryTimeouts[id] = setTimeout(function() { + self.log("Starting retry for " + name + "..."); + if (callback) { + callback(id); + } else { + self._handler.retry(id); + } + }, retryWaitPeriod); + return true; + } + }, + _onBeforeAutoRetry: function(id, name) { + this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "..."); + }, + _onBeforeManualRetry: function(id) { + var itemLimit = this._currentItemLimit, fileName; + if (this._preventRetries[id]) { + this.log("Retries are forbidden for id " + id, "warn"); + return false; + } else if (this._handler.isValid(id)) { + fileName = this.getName(id); + if (this._options.callbacks.onManualRetry(id, fileName) === false) { + return false; + } + if (itemLimit > 0 && this._netUploadedOrQueued + 1 > itemLimit) { + this._itemError("retryFailTooManyItems"); + return false; + } + this.log("Retrying upload for '" + fileName + "' (id: " + id + ")..."); + return true; + } else { + this.log("'" + id + "' is not a valid file ID", "error"); + return false; + } + }, + _onCancel: function(id, name) { + this._netUploadedOrQueued--; + clearTimeout(this._retryTimeouts[id]); + var storedItemIndex = qq.indexOf(this._storedIds, id); + if (!this._options.autoUpload && storedItemIndex >= 0) { + this._storedIds.splice(storedItemIndex, 1); + } + this._uploadData.setStatus(id, qq.status.CANCELED); + }, + _onComplete: function(id, name, result, xhr) { + if (!result.success) { + this._netUploadedOrQueued--; + this._uploadData.setStatus(id, qq.status.UPLOAD_FAILED); + if (result[this._options.retry.preventRetryResponseProperty] === true) { + this._preventRetries[id] = true; + } + } else { + if (result.thumbnailUrl) { + this._thumbnailUrls[id] = result.thumbnailUrl; + } + this._netUploaded++; + this._uploadData.setStatus(id, qq.status.UPLOAD_SUCCESSFUL); + } + this._maybeParseAndSendUploadError(id, name, result, xhr); + return result.success ? true : false; + }, + _onDelete: function(id) { + this._uploadData.setStatus(id, qq.status.DELETING); + }, + _onDeleteComplete: function(id, xhrOrXdr, isError) { + var name = this.getName(id); + if (isError) { + this._uploadData.setStatus(id, qq.status.DELETE_FAILED); + this.log("Delete request for '" + name + "' has failed.", "error"); + if (xhrOrXdr.withCredentials === undefined) { + this._options.callbacks.onError(id, name, "Delete request failed", xhrOrXdr); + } else { + this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhrOrXdr.status, xhrOrXdr); + } + } else { + this._netUploadedOrQueued--; + this._netUploaded--; + this._handler.expunge(id); + this._uploadData.setStatus(id, qq.status.DELETED); + this.log("Delete request for '" + name + "' has succeeded."); + } + }, + _onInputChange: function(input) { + var fileIndex; + if (qq.supportedFeatures.ajaxUploading) { + for (fileIndex = 0; fileIndex < input.files.length; fileIndex++) { + this._annotateWithButtonId(input.files[fileIndex], input); + } + this.addFiles(input.files); + } else if (input.value.length > 0) { + this.addFiles(input); + } + qq.each(this._buttons, function(idx, button) { + button.reset(); + }); + }, + _onProgress: function(id, name, loaded, total) { + this._totalProgress && this._totalProgress.onIndividualProgress(id, loaded, total); + }, + _onSubmit: function(id, name) {}, + _onSubmitCallbackSuccess: function(id, name) { + this._onSubmit.apply(this, arguments); + this._uploadData.setStatus(id, qq.status.SUBMITTED); + this._onSubmitted.apply(this, arguments); + if (this._options.autoUpload) { + this._options.callbacks.onSubmitted.apply(this, arguments); + this._uploadFile(id); + } else { + this._storeForLater(id); + this._options.callbacks.onSubmitted.apply(this, arguments); + } + }, + _onSubmitDelete: function(id, onSuccessCallback, additionalMandatedParams) { + var uuid = this.getUuid(id), adjustedOnSuccessCallback; + if (onSuccessCallback) { + adjustedOnSuccessCallback = qq.bind(onSuccessCallback, this, id, uuid, additionalMandatedParams); + } + if (this._isDeletePossible()) { + this._handleCheckedCallback({ + name: "onSubmitDelete", + callback: qq.bind(this._options.callbacks.onSubmitDelete, this, id), + onSuccess: adjustedOnSuccessCallback || qq.bind(this._deleteHandler.sendDelete, this, id, uuid, additionalMandatedParams), + identifier: id + }); + return true; + } else { + this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " + "due to CORS on a user agent that does not support pre-flighting.", "warn"); + return false; + } + }, + _onSubmitted: function(id) {}, + _onTotalProgress: function(loaded, total) { + this._options.callbacks.onTotalProgress(loaded, total); + }, + _onUploadPrep: function(id) {}, + _onUpload: function(id, name) { + this._uploadData.setStatus(id, qq.status.UPLOADING); + }, + _onUploadChunk: function(id, chunkData) {}, + _onUploadStatusChange: function(id, oldStatus, newStatus) { + if (newStatus === qq.status.PAUSED) { + clearTimeout(this._retryTimeouts[id]); + } + }, + _onValidateBatchCallbackFailure: function(fileWrappers) { + var self = this; + qq.each(fileWrappers, function(idx, fileWrapper) { + self._fileOrBlobRejected(fileWrapper.id); + }); + }, + _onValidateBatchCallbackSuccess: function(validationDescriptors, items, params, endpoint, button) { + var errorMessage, itemLimit = this._currentItemLimit, proposedNetFilesUploadedOrQueued = this._netUploadedOrQueued; + if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) { + if (items.length > 0) { + this._handleCheckedCallback({ + name: "onValidate", + callback: qq.bind(this._options.callbacks.onValidate, this, validationDescriptors[0], button), + onSuccess: qq.bind(this._onValidateCallbackSuccess, this, items, 0, params, endpoint), + onFailure: qq.bind(this._onValidateCallbackFailure, this, items, 0, params, endpoint), + identifier: "Item '" + items[0].file.name + "', size: " + items[0].file.size + }); + } else { + this._itemError("noFilesError"); + } + } else { + this._onValidateBatchCallbackFailure(items); + errorMessage = this._options.messages.tooManyItemsError.replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued).replace(/\{itemLimit\}/g, itemLimit); + this._batchError(errorMessage); + } + }, + _onValidateCallbackFailure: function(items, index, params, endpoint) { + var nextIndex = index + 1; + this._fileOrBlobRejected(items[index].id, items[index].file.name); + this._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint); + }, + _onValidateCallbackSuccess: function(items, index, params, endpoint) { + var self = this, nextIndex = index + 1, validationDescriptor = this._getValidationDescriptor(items[index]); + this._validateFileOrBlobData(items[index], validationDescriptor).then(function() { + self._upload(items[index].id, params, endpoint); + self._maybeProcessNextItemAfterOnValidateCallback(true, items, nextIndex, params, endpoint); + }, function() { + self._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint); + }); + }, + _prepareItemsForUpload: function(items, params, endpoint) { + if (items.length === 0) { + this._itemError("noFilesError"); + return; + } + var validationDescriptors = this._getValidationDescriptors(items), buttonId = this._getButtonId(items[0].file), button = this._getButton(buttonId); + this._handleCheckedCallback({ + name: "onValidateBatch", + callback: qq.bind(this._options.callbacks.onValidateBatch, this, validationDescriptors, button), + onSuccess: qq.bind(this._onValidateBatchCallbackSuccess, this, validationDescriptors, items, params, endpoint, button), + onFailure: qq.bind(this._onValidateBatchCallbackFailure, this, items), + identifier: "batch validation" + }); + }, + _preventLeaveInProgress: function() { + var self = this; + this._disposeSupport.attach(window, "beforeunload", function(e) { + if (self.getInProgress()) { + e = e || window.event; + e.returnValue = self._options.messages.onLeave; + return self._options.messages.onLeave; + } + }); + }, + _refreshSessionData: function() { + var self = this, options = this._options.session; + if (qq.Session && this._options.session.endpoint != null) { + if (!this._session) { + qq.extend(options, { + cors: this._options.cors + }); + options.log = qq.bind(this.log, this); + options.addFileRecord = qq.bind(this._addCannedFile, this); + this._session = new qq.Session(options); + } + setTimeout(function() { + self._session.refresh().then(function(response, xhrOrXdr) { + self._sessionRequestComplete(); + self._options.callbacks.onSessionRequestComplete(response, true, xhrOrXdr); + }, function(response, xhrOrXdr) { + self._options.callbacks.onSessionRequestComplete(response, false, xhrOrXdr); + }); + }, 0); + } + }, + _sessionRequestComplete: function() {}, + _setSize: function(id, newSize) { + this._uploadData.updateSize(id, newSize); + this._totalProgress && this._totalProgress.onNewSize(id); + }, + _shouldAutoRetry: function(id, name, responseJSON) { + var uploadData = this._uploadData.retrieve({ + id: id + }); + if (!this._preventRetries[id] && this._options.retry.enableAuto && uploadData.status !== qq.status.PAUSED) { + if (this._autoRetries[id] === undefined) { + this._autoRetries[id] = 0; + } + if (this._autoRetries[id] < this._options.retry.maxAutoAttempts) { + this._autoRetries[id] += 1; + return true; + } + } + return false; + }, + _storeForLater: function(id) { + this._storedIds.push(id); + }, + _trackButton: function(id) { + var buttonId; + if (qq.supportedFeatures.ajaxUploading) { + buttonId = this._handler.getFile(id).qqButtonId; + } else { + buttonId = this._getButtonId(this._handler.getInput(id)); + } + if (buttonId) { + this._buttonIdsForFileIds[id] = buttonId; + } + }, + _updateFormSupportAndParams: function(formElementOrId) { + this._options.form.element = formElementOrId; + this._formSupport = qq.FormSupport && new qq.FormSupport(this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this)); + if (this._formSupport && this._formSupport.attachedToForm) { + this._paramsStore.addReadOnly(null, this._formSupport.getFormInputsAsObject); + this._options.autoUpload = this._formSupport.newAutoUpload; + if (this._formSupport.newEndpoint) { + this.setEndpoint(this._formSupport.newEndpoint); + } + } + }, + _upload: function(id, params, endpoint) { + var name = this.getName(id); + if (params) { + this.setParams(params, id); + } + if (endpoint) { + this.setEndpoint(endpoint, id); + } + this._handleCheckedCallback({ + name: "onSubmit", + callback: qq.bind(this._options.callbacks.onSubmit, this, id, name), + onSuccess: qq.bind(this._onSubmitCallbackSuccess, this, id, name), + onFailure: qq.bind(this._fileOrBlobRejected, this, id, name), + identifier: id + }); + }, + _uploadFile: function(id) { + if (!this._handler.upload(id)) { + this._uploadData.setStatus(id, qq.status.QUEUED); + } + }, + _uploadStoredFiles: function() { + var idToUpload, stillSubmitting, self = this; + while (this._storedIds.length) { + idToUpload = this._storedIds.shift(); + this._uploadFile(idToUpload); + } + stillSubmitting = this.getUploads({ + status: qq.status.SUBMITTING + }).length; + if (stillSubmitting) { + qq.log("Still waiting for " + stillSubmitting + " files to clear submit queue. Will re-parse stored IDs array shortly."); + setTimeout(function() { + self._uploadStoredFiles(); + }, 1e3); + } + }, + _validateFileOrBlobData: function(fileWrapper, validationDescriptor) { + var self = this, file = function() { + if (fileWrapper.file instanceof qq.BlobProxy) { + return fileWrapper.file.referenceBlob; + } + return fileWrapper.file; + }(), name = validationDescriptor.name, size = validationDescriptor.size, buttonId = this._getButtonId(fileWrapper.file), validationBase = this._getValidationBase(buttonId), validityChecker = new qq.Promise(); + validityChecker.then(function() {}, function() { + self._fileOrBlobRejected(fileWrapper.id, name); + }); + if (qq.isFileOrInput(file) && !this._isAllowedExtension(validationBase.allowedExtensions, name)) { + this._itemError("typeError", name, file); + return validityChecker.failure(); + } + if (!this._options.validation.allowEmpty && size === 0) { + this._itemError("emptyError", name, file); + return validityChecker.failure(); + } + if (size > 0 && validationBase.sizeLimit && size > validationBase.sizeLimit) { + this._itemError("sizeError", name, file); + return validityChecker.failure(); + } + if (size > 0 && size < validationBase.minSizeLimit) { + this._itemError("minSizeError", name, file); + return validityChecker.failure(); + } + if (qq.ImageValidation && qq.supportedFeatures.imagePreviews && qq.isFile(file)) { + new qq.ImageValidation(file, qq.bind(self.log, self)).validate(validationBase.image).then(validityChecker.success, function(errorCode) { + self._itemError(errorCode + "ImageError", name, file); + validityChecker.failure(); + }); + } else { + validityChecker.success(); + } + return validityChecker; + }, + _wrapCallbacks: function() { + var self, safeCallback, prop; + self = this; + safeCallback = function(name, callback, args) { + var errorMsg; + try { + return callback.apply(self, args); + } catch (exception) { + errorMsg = exception.message || exception.toString(); + self.log("Caught exception in '" + name + "' callback - " + errorMsg, "error"); + } + }; + for (prop in this._options.callbacks) { + (function() { + var callbackName, callbackFunc; + callbackName = prop; + callbackFunc = self._options.callbacks[callbackName]; + self._options.callbacks[callbackName] = function() { + return safeCallback(callbackName, callbackFunc, arguments); + }; + })(); + } + } + }; + })(); + (function() { + "use strict"; + qq.FineUploaderBasic = function(o) { + var self = this; + this._options = { + debug: false, + button: null, + multiple: true, + maxConnections: 3, + disableCancelForFormUploads: false, + autoUpload: true, + request: { + customHeaders: {}, + endpoint: "/server/upload", + filenameParam: "qqfilename", + forceMultipart: true, + inputName: "qqfile", + method: "POST", + params: {}, + paramsInBody: true, + totalFileSizeName: "qqtotalfilesize", + uuidName: "qquuid" + }, + validation: { + allowedExtensions: [], + sizeLimit: 0, + minSizeLimit: 0, + itemLimit: 0, + stopOnFirstInvalidFile: true, + acceptFiles: null, + image: { + maxHeight: 0, + maxWidth: 0, + minHeight: 0, + minWidth: 0 + }, + allowEmpty: false + }, + callbacks: { + onSubmit: function(id, name) {}, + onSubmitted: function(id, name) {}, + onComplete: function(id, name, responseJSON, maybeXhr) {}, + onAllComplete: function(successful, failed) {}, + onCancel: function(id, name) {}, + onUpload: function(id, name) {}, + onUploadChunk: function(id, name, chunkData) {}, + onUploadChunkSuccess: function(id, chunkData, responseJSON, xhr) {}, + onResume: function(id, fileName, chunkData) {}, + onProgress: function(id, name, loaded, total) {}, + onTotalProgress: function(loaded, total) {}, + onError: function(id, name, reason, maybeXhrOrXdr) {}, + onAutoRetry: function(id, name, attemptNumber) {}, + onManualRetry: function(id, name) {}, + onValidateBatch: function(fileOrBlobData) {}, + onValidate: function(fileOrBlobData) {}, + onSubmitDelete: function(id) {}, + onDelete: function(id) {}, + onDeleteComplete: function(id, xhrOrXdr, isError) {}, + onPasteReceived: function(blob) {}, + onStatusChange: function(id, oldStatus, newStatus) {}, + onSessionRequestComplete: function(response, success, xhrOrXdr) {} + }, + messages: { + typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.", + sizeError: "{file} is too large, maximum file size is {sizeLimit}.", + minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.", + emptyError: "{file} is empty, please select files again without it.", + noFilesError: "No files to upload.", + tooManyItemsError: "Too many items ({netItems}) would be uploaded. Item limit is {itemLimit}.", + maxHeightImageError: "Image is too tall.", + maxWidthImageError: "Image is too wide.", + minHeightImageError: "Image is not tall enough.", + minWidthImageError: "Image is not wide enough.", + retryFailTooManyItems: "Retry failed - you have reached your file limit.", + onLeave: "The files are being uploaded, if you leave now the upload will be canceled.", + unsupportedBrowserIos8Safari: "Unrecoverable error - this browser does not permit file uploading of any kind due to serious bugs in iOS8 Safari. Please use iOS8 Chrome until Apple fixes these issues." + }, + retry: { + enableAuto: false, + maxAutoAttempts: 3, + autoAttemptDelay: 5, + preventRetryResponseProperty: "preventRetry" + }, + classes: { + buttonHover: "qq-upload-button-hover", + buttonFocus: "qq-upload-button-focus" + }, + chunking: { + enabled: false, + concurrent: { + enabled: false + }, + mandatory: false, + paramNames: { + partIndex: "qqpartindex", + partByteOffset: "qqpartbyteoffset", + chunkSize: "qqchunksize", + totalFileSize: "qqtotalfilesize", + totalParts: "qqtotalparts" + }, + partSize: 2e6, + success: { + endpoint: null + } + }, + resume: { + enabled: false, + recordsExpireIn: 7, + paramNames: { + resuming: "qqresume" + } + }, + formatFileName: function(fileOrBlobName) { + return fileOrBlobName; + }, + text: { + defaultResponseError: "Upload failure reason unknown", + fileInputTitle: "file input", + sizeSymbols: [ "kB", "MB", "GB", "TB", "PB", "EB" ] + }, + deleteFile: { + enabled: false, + method: "DELETE", + endpoint: "/server/upload", + customHeaders: {}, + params: {} + }, + cors: { + expected: false, + sendCredentials: false, + allowXdr: false + }, + blobs: { + defaultName: "misc_data" + }, + paste: { + targetElement: null, + defaultName: "pasted_image" + }, + camera: { + ios: false, + button: null + }, + extraButtons: [], + session: { + endpoint: null, + params: {}, + customHeaders: {}, + refreshOnReset: true + }, + form: { + element: "qq-form", + autoUpload: false, + interceptSubmit: true + }, + scaling: { + customResizer: null, + sendOriginal: true, + orient: true, + defaultType: null, + defaultQuality: 80, + failureText: "Failed to scale", + includeExif: false, + sizes: [] + }, + workarounds: { + iosEmptyVideos: true, + ios8SafariUploads: true, + ios8BrowserCrash: false + } + }; + qq.extend(this._options, o, true); + this._buttons = []; + this._extraButtonSpecs = {}; + this._buttonIdsForFileIds = []; + this._wrapCallbacks(); + this._disposeSupport = new qq.DisposeSupport(); + this._storedIds = []; + this._autoRetries = []; + this._retryTimeouts = []; + this._preventRetries = []; + this._thumbnailUrls = []; + this._netUploadedOrQueued = 0; + this._netUploaded = 0; + this._uploadData = this._createUploadDataTracker(); + this._initFormSupportAndParams(); + this._customHeadersStore = this._createStore(this._options.request.customHeaders); + this._deleteFileCustomHeadersStore = this._createStore(this._options.deleteFile.customHeaders); + this._deleteFileParamsStore = this._createStore(this._options.deleteFile.params); + this._endpointStore = this._createStore(this._options.request.endpoint); + this._deleteFileEndpointStore = this._createStore(this._options.deleteFile.endpoint); + this._handler = this._createUploadHandler(); + this._deleteHandler = qq.DeleteFileAjaxRequester && this._createDeleteHandler(); + if (this._options.button) { + this._defaultButtonId = this._createUploadButton({ + element: this._options.button, + title: this._options.text.fileInputTitle + }).getButtonId(); + } + this._generateExtraButtonSpecs(); + this._handleCameraAccess(); + if (this._options.paste.targetElement) { + if (qq.PasteSupport) { + this._pasteHandler = this._createPasteHandler(); + } else { + this.log("Paste support module not found", "error"); + } + } + this._preventLeaveInProgress(); + this._imageGenerator = qq.ImageGenerator && new qq.ImageGenerator(qq.bind(this.log, this)); + this._refreshSessionData(); + this._succeededSinceLastAllComplete = []; + this._failedSinceLastAllComplete = []; + this._scaler = qq.Scaler && new qq.Scaler(this._options.scaling, qq.bind(this.log, this)) || {}; + if (this._scaler.enabled) { + this._customNewFileHandler = qq.bind(this._scaler.handleNewFile, this._scaler); + } + if (qq.TotalProgress && qq.supportedFeatures.progressBar) { + this._totalProgress = new qq.TotalProgress(qq.bind(this._onTotalProgress, this), function(id) { + var entry = self._uploadData.retrieve({ + id: id + }); + return entry && entry.size || 0; + }); + } + this._currentItemLimit = this._options.validation.itemLimit; + }; + qq.FineUploaderBasic.prototype = qq.basePublicApi; + qq.extend(qq.FineUploaderBasic.prototype, qq.basePrivateApi); + })(); + qq.AjaxRequester = function(o) { + "use strict"; + var log, shouldParamsBeInQueryString, queue = [], requestData = {}, options = { + acceptHeader: null, + validMethods: [ "PATCH", "POST", "PUT" ], + method: "POST", + contentType: "application/x-www-form-urlencoded", + maxConnections: 3, + customHeaders: {}, + endpointStore: {}, + paramsStore: {}, + mandatedParams: {}, + allowXRequestedWithAndCacheControl: true, + successfulResponseCodes: { + DELETE: [ 200, 202, 204 ], + PATCH: [ 200, 201, 202, 203, 204 ], + POST: [ 200, 201, 202, 203, 204 ], + PUT: [ 200, 201, 202, 203, 204 ], + GET: [ 200 ] + }, + cors: { + expected: false, + sendCredentials: false + }, + log: function(str, level) {}, + onSend: function(id) {}, + onComplete: function(id, xhrOrXdr, isError) {}, + onProgress: null + }; + qq.extend(options, o); + log = options.log; + if (qq.indexOf(options.validMethods, options.method) < 0) { + throw new Error("'" + options.method + "' is not a supported method for this type of request!"); + } + function isSimpleMethod() { + return qq.indexOf([ "GET", "POST", "HEAD" ], options.method) >= 0; + } + function containsNonSimpleHeaders(headers) { + var containsNonSimple = false; + qq.each(containsNonSimple, function(idx, header) { + if (qq.indexOf([ "Accept", "Accept-Language", "Content-Language", "Content-Type" ], header) < 0) { + containsNonSimple = true; + return false; + } + }); + return containsNonSimple; + } + function isXdr(xhr) { + return options.cors.expected && xhr.withCredentials === undefined; + } + function getCorsAjaxTransport() { + var xhrOrXdr; + if (window.XMLHttpRequest || window.ActiveXObject) { + xhrOrXdr = qq.createXhrInstance(); + if (xhrOrXdr.withCredentials === undefined) { + xhrOrXdr = new XDomainRequest(); + xhrOrXdr.onload = function() {}; + xhrOrXdr.onerror = function() {}; + xhrOrXdr.ontimeout = function() {}; + xhrOrXdr.onprogress = function() {}; + } + } + return xhrOrXdr; + } + function getXhrOrXdr(id, suppliedXhr) { + var xhrOrXdr = requestData[id].xhr; + if (!xhrOrXdr) { + if (suppliedXhr) { + xhrOrXdr = suppliedXhr; + } else { + if (options.cors.expected) { + xhrOrXdr = getCorsAjaxTransport(); + } else { + xhrOrXdr = qq.createXhrInstance(); + } + } + requestData[id].xhr = xhrOrXdr; + } + return xhrOrXdr; + } + function dequeue(id) { + var i = qq.indexOf(queue, id), max = options.maxConnections, nextId; + delete requestData[id]; + queue.splice(i, 1); + if (queue.length >= max && i < max) { + nextId = queue[max - 1]; + sendRequest(nextId); + } + } + function onComplete(id, xdrError) { + var xhr = getXhrOrXdr(id), method = options.method, isError = xdrError === true; + dequeue(id); + if (isError) { + log(method + " request for " + id + " has failed", "error"); + } else if (!isXdr(xhr) && !isResponseSuccessful(xhr.status)) { + isError = true; + log(method + " request for " + id + " has failed - response code " + xhr.status, "error"); + } + options.onComplete(id, xhr, isError); + } + function getParams(id) { + var onDemandParams = requestData[id].additionalParams, mandatedParams = options.mandatedParams, params; + if (options.paramsStore.get) { + params = options.paramsStore.get(id); + } + if (onDemandParams) { + qq.each(onDemandParams, function(name, val) { + params = params || {}; + params[name] = val; + }); + } + if (mandatedParams) { + qq.each(mandatedParams, function(name, val) { + params = params || {}; + params[name] = val; + }); + } + return params; + } + function sendRequest(id, optXhr) { + var xhr = getXhrOrXdr(id, optXhr), method = options.method, params = getParams(id), payload = requestData[id].payload, url; + options.onSend(id); + url = createUrl(id, params, requestData[id].additionalQueryParams); + if (isXdr(xhr)) { + xhr.onload = getXdrLoadHandler(id); + xhr.onerror = getXdrErrorHandler(id); + } else { + xhr.onreadystatechange = getXhrReadyStateChangeHandler(id); + } + registerForUploadProgress(id); + xhr.open(method, url, true); + if (options.cors.expected && options.cors.sendCredentials && !isXdr(xhr)) { + xhr.withCredentials = true; + } + setHeaders(id); + log("Sending " + method + " request for " + id); + if (payload) { + xhr.send(payload); + } else if (shouldParamsBeInQueryString || !params) { + xhr.send(); + } else if (params && options.contentType && options.contentType.toLowerCase().indexOf("application/x-www-form-urlencoded") >= 0) { + xhr.send(qq.obj2url(params, "")); + } else if (params && options.contentType && options.contentType.toLowerCase().indexOf("application/json") >= 0) { + xhr.send(JSON.stringify(params)); + } else { + xhr.send(params); + } + return xhr; + } + function createUrl(id, params, additionalQueryParams) { + var endpoint = options.endpointStore.get(id), addToPath = requestData[id].addToPath; + if (addToPath != undefined) { + endpoint += "/" + addToPath; + } + if (shouldParamsBeInQueryString && params) { + endpoint = qq.obj2url(params, endpoint); + } + if (additionalQueryParams) { + endpoint = qq.obj2url(additionalQueryParams, endpoint); + } + return endpoint; + } + function getXhrReadyStateChangeHandler(id) { + return function() { + if (getXhrOrXdr(id).readyState === 4) { + onComplete(id); + } + }; + } + function registerForUploadProgress(id) { + var onProgress = options.onProgress; + if (onProgress) { + getXhrOrXdr(id).upload.onprogress = function(e) { + if (e.lengthComputable) { + onProgress(id, e.loaded, e.total); + } + }; + } + } + function getXdrLoadHandler(id) { + return function() { + onComplete(id); + }; + } + function getXdrErrorHandler(id) { + return function() { + onComplete(id, true); + }; + } + function setHeaders(id) { + var xhr = getXhrOrXdr(id), customHeaders = options.customHeaders, onDemandHeaders = requestData[id].additionalHeaders || {}, method = options.method, allHeaders = {}; + if (!isXdr(xhr)) { + options.acceptHeader && xhr.setRequestHeader("Accept", options.acceptHeader); + if (options.allowXRequestedWithAndCacheControl) { + if (!options.cors.expected || (!isSimpleMethod() || containsNonSimpleHeaders(customHeaders))) { + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.setRequestHeader("Cache-Control", "no-cache"); + } + } + if (options.contentType && (method === "POST" || method === "PUT")) { + xhr.setRequestHeader("Content-Type", options.contentType); + } + qq.extend(allHeaders, qq.isFunction(customHeaders) ? customHeaders(id) : customHeaders); + qq.extend(allHeaders, onDemandHeaders); + qq.each(allHeaders, function(name, val) { + xhr.setRequestHeader(name, val); + }); + } + } + function isResponseSuccessful(responseCode) { + return qq.indexOf(options.successfulResponseCodes[options.method], responseCode) >= 0; + } + function prepareToSend(id, optXhr, addToPath, additionalParams, additionalQueryParams, additionalHeaders, payload) { + requestData[id] = { + addToPath: addToPath, + additionalParams: additionalParams, + additionalQueryParams: additionalQueryParams, + additionalHeaders: additionalHeaders, + payload: payload + }; + var len = queue.push(id); + if (len <= options.maxConnections) { + return sendRequest(id, optXhr); + } + } + shouldParamsBeInQueryString = options.method === "GET" || options.method === "DELETE"; + qq.extend(this, { + initTransport: function(id) { + var path, params, headers, payload, cacheBuster, additionalQueryParams; + return { + withPath: function(appendToPath) { + path = appendToPath; + return this; + }, + withParams: function(additionalParams) { + params = additionalParams; + return this; + }, + withQueryParams: function(_additionalQueryParams_) { + additionalQueryParams = _additionalQueryParams_; + return this; + }, + withHeaders: function(additionalHeaders) { + headers = additionalHeaders; + return this; + }, + withPayload: function(thePayload) { + payload = thePayload; + return this; + }, + withCacheBuster: function() { + cacheBuster = true; + return this; + }, + send: function(optXhr) { + if (cacheBuster && qq.indexOf([ "GET", "DELETE" ], options.method) >= 0) { + params.qqtimestamp = new Date().getTime(); + } + return prepareToSend(id, optXhr, path, params, additionalQueryParams, headers, payload); + } + }; + }, + canceled: function(id) { + dequeue(id); + } + }); + }; + qq.UploadHandler = function(spec) { + "use strict"; + var proxy = spec.proxy, fileState = {}, onCancel = proxy.onCancel, getName = proxy.getName; + qq.extend(this, { + add: function(id, fileItem) { + fileState[id] = fileItem; + fileState[id].temp = {}; + }, + cancel: function(id) { + var self = this, cancelFinalizationEffort = new qq.Promise(), onCancelRetVal = onCancel(id, getName(id), cancelFinalizationEffort); + onCancelRetVal.then(function() { + if (self.isValid(id)) { + fileState[id].canceled = true; + self.expunge(id); + } + cancelFinalizationEffort.success(); + }); + }, + expunge: function(id) { + delete fileState[id]; + }, + getThirdPartyFileId: function(id) { + return fileState[id].key; + }, + isValid: function(id) { + return fileState[id] !== undefined; + }, + reset: function() { + fileState = {}; + }, + _getFileState: function(id) { + return fileState[id]; + }, + _setThirdPartyFileId: function(id, thirdPartyFileId) { + fileState[id].key = thirdPartyFileId; + }, + _wasCanceled: function(id) { + return !!fileState[id].canceled; + } + }); + }; + qq.UploadHandlerController = function(o, namespace) { + "use strict"; + var controller = this, chunkingPossible = false, concurrentChunkingPossible = false, chunking, preventRetryResponse, log, handler, options = { + paramsStore: {}, + maxConnections: 3, + chunking: { + enabled: false, + multiple: { + enabled: false + } + }, + log: function(str, level) {}, + onProgress: function(id, fileName, loaded, total) {}, + onComplete: function(id, fileName, response, xhr) {}, + onCancel: function(id, fileName) {}, + onUploadPrep: function(id) {}, + onUpload: function(id, fileName) {}, + onUploadChunk: function(id, fileName, chunkData) {}, + onUploadChunkSuccess: function(id, chunkData, response, xhr) {}, + onAutoRetry: function(id, fileName, response, xhr) {}, + onResume: function(id, fileName, chunkData) {}, + onUuidChanged: function(id, newUuid) {}, + getName: function(id) {}, + setSize: function(id, newSize) {}, + isQueued: function(id) {}, + getIdsInProxyGroup: function(id) {}, + getIdsInBatch: function(id) {} + }, chunked = { + done: function(id, chunkIdx, response, xhr) { + var chunkData = handler._getChunkData(id, chunkIdx); + handler._getFileState(id).attemptingResume = false; + delete handler._getFileState(id).temp.chunkProgress[chunkIdx]; + handler._getFileState(id).loaded += chunkData.size; + options.onUploadChunkSuccess(id, handler._getChunkDataForCallback(chunkData), response, xhr); + }, + finalize: function(id) { + var size = options.getSize(id), name = options.getName(id); + log("All chunks have been uploaded for " + id + " - finalizing...."); + handler.finalizeChunks(id).then(function(response, xhr) { + log("Finalize successful for " + id); + var normaizedResponse = upload.normalizeResponse(response, true); + options.onProgress(id, name, size, size); + handler._maybeDeletePersistedChunkData(id); + upload.cleanup(id, normaizedResponse, xhr); + }, function(response, xhr) { + var normaizedResponse = upload.normalizeResponse(response, false); + log("Problem finalizing chunks for file ID " + id + " - " + normaizedResponse.error, "error"); + if (normaizedResponse.reset) { + chunked.reset(id); + } + if (!options.onAutoRetry(id, name, normaizedResponse, xhr)) { + upload.cleanup(id, normaizedResponse, xhr); + } + }); + }, + handleFailure: function(chunkIdx, id, response, xhr) { + var name = options.getName(id); + log("Chunked upload request failed for " + id + ", chunk " + chunkIdx); + handler.clearCachedChunk(id, chunkIdx); + var responseToReport = upload.normalizeResponse(response, false), inProgressIdx; + if (responseToReport.reset) { + chunked.reset(id); + } else { + inProgressIdx = qq.indexOf(handler._getFileState(id).chunking.inProgress, chunkIdx); + if (inProgressIdx >= 0) { + handler._getFileState(id).chunking.inProgress.splice(inProgressIdx, 1); + handler._getFileState(id).chunking.remaining.unshift(chunkIdx); + } + } + if (!handler._getFileState(id).temp.ignoreFailure) { + if (concurrentChunkingPossible) { + handler._getFileState(id).temp.ignoreFailure = true; + log(qq.format("Going to attempt to abort these chunks: {}. These are currently in-progress: {}.", JSON.stringify(Object.keys(handler._getXhrs(id))), JSON.stringify(handler._getFileState(id).chunking.inProgress))); + qq.each(handler._getXhrs(id), function(ckid, ckXhr) { + log(qq.format("Attempting to abort file {}.{}. XHR readyState {}. ", id, ckid, ckXhr.readyState)); + ckXhr.abort(); + ckXhr._cancelled = true; + }); + handler.moveInProgressToRemaining(id); + connectionManager.free(id, true); + } + if (!options.onAutoRetry(id, name, responseToReport, xhr)) { + upload.cleanup(id, responseToReport, xhr); + } + } + }, + hasMoreParts: function(id) { + return !!handler._getFileState(id).chunking.remaining.length; + }, + nextPart: function(id) { + var nextIdx = handler._getFileState(id).chunking.remaining.shift(); + if (nextIdx >= handler._getTotalChunks(id)) { + nextIdx = null; + } + return nextIdx; + }, + reset: function(id) { + log("Server or callback has ordered chunking effort to be restarted on next attempt for item ID " + id, "error"); + handler._maybeDeletePersistedChunkData(id); + handler.reevaluateChunking(id); + handler._getFileState(id).loaded = 0; + }, + sendNext: function(id) { + var size = options.getSize(id), name = options.getName(id), chunkIdx = chunked.nextPart(id), chunkData = handler._getChunkData(id, chunkIdx), resuming = handler._getFileState(id).attemptingResume, inProgressChunks = handler._getFileState(id).chunking.inProgress || []; + if (handler._getFileState(id).loaded == null) { + handler._getFileState(id).loaded = 0; + } + if (resuming && options.onResume(id, name, chunkData) === false) { + chunked.reset(id); + chunkIdx = chunked.nextPart(id); + chunkData = handler._getChunkData(id, chunkIdx); + resuming = false; + } + if (chunkIdx == null && inProgressChunks.length === 0) { + chunked.finalize(id); + } else { + log(qq.format("Sending chunked upload request for item {}.{}, bytes {}-{} of {}.", id, chunkIdx, chunkData.start + 1, chunkData.end, size)); + options.onUploadChunk(id, name, handler._getChunkDataForCallback(chunkData)); + inProgressChunks.push(chunkIdx); + handler._getFileState(id).chunking.inProgress = inProgressChunks; + if (concurrentChunkingPossible) { + connectionManager.open(id, chunkIdx); + } + if (concurrentChunkingPossible && connectionManager.available() && handler._getFileState(id).chunking.remaining.length) { + chunked.sendNext(id); + } + if (chunkData.blob.size === 0) { + log(qq.format("Chunk {} for file {} will not be uploaded, zero sized chunk.", chunkIdx, id), "error"); + chunked.handleFailure(chunkIdx, id, "File is no longer available", null); + } else { + handler.uploadChunk(id, chunkIdx, resuming).then(function success(response, xhr) { + log("Chunked upload request succeeded for " + id + ", chunk " + chunkIdx); + handler.clearCachedChunk(id, chunkIdx); + var inProgressChunks = handler._getFileState(id).chunking.inProgress || [], responseToReport = upload.normalizeResponse(response, true), inProgressChunkIdx = qq.indexOf(inProgressChunks, chunkIdx); + log(qq.format("Chunk {} for file {} uploaded successfully.", chunkIdx, id)); + chunked.done(id, chunkIdx, responseToReport, xhr); + if (inProgressChunkIdx >= 0) { + inProgressChunks.splice(inProgressChunkIdx, 1); + } + handler._maybePersistChunkedState(id); + if (!chunked.hasMoreParts(id) && inProgressChunks.length === 0) { + chunked.finalize(id); + } else if (chunked.hasMoreParts(id)) { + chunked.sendNext(id); + } else { + log(qq.format("File ID {} has no more chunks to send and these chunk indexes are still marked as in-progress: {}", id, JSON.stringify(inProgressChunks))); + } + }, function failure(response, xhr) { + chunked.handleFailure(chunkIdx, id, response, xhr); + }).done(function() { + handler.clearXhr(id, chunkIdx); + }); + } + } + } + }, connectionManager = { + _open: [], + _openChunks: {}, + _waiting: [], + available: function() { + var max = options.maxConnections, openChunkEntriesCount = 0, openChunksCount = 0; + qq.each(connectionManager._openChunks, function(fileId, openChunkIndexes) { + openChunkEntriesCount++; + openChunksCount += openChunkIndexes.length; + }); + return max - (connectionManager._open.length - openChunkEntriesCount + openChunksCount); + }, + free: function(id, dontAllowNext) { + var allowNext = !dontAllowNext, waitingIndex = qq.indexOf(connectionManager._waiting, id), connectionsIndex = qq.indexOf(connectionManager._open, id), nextId; + delete connectionManager._openChunks[id]; + if (upload.getProxyOrBlob(id) instanceof qq.BlobProxy) { + log("Generated blob upload has ended for " + id + ", disposing generated blob."); + delete handler._getFileState(id).file; + } + if (waitingIndex >= 0) { + connectionManager._waiting.splice(waitingIndex, 1); + } else if (allowNext && connectionsIndex >= 0) { + connectionManager._open.splice(connectionsIndex, 1); + nextId = connectionManager._waiting.shift(); + if (nextId >= 0) { + connectionManager._open.push(nextId); + upload.start(nextId); + } + } + }, + getWaitingOrConnected: function() { + var waitingOrConnected = []; + qq.each(connectionManager._openChunks, function(fileId, chunks) { + if (chunks && chunks.length) { + waitingOrConnected.push(parseInt(fileId)); + } + }); + qq.each(connectionManager._open, function(idx, fileId) { + if (!connectionManager._openChunks[fileId]) { + waitingOrConnected.push(parseInt(fileId)); + } + }); + waitingOrConnected = waitingOrConnected.concat(connectionManager._waiting); + return waitingOrConnected; + }, + isUsingConnection: function(id) { + return qq.indexOf(connectionManager._open, id) >= 0; + }, + open: function(id, chunkIdx) { + if (chunkIdx == null) { + connectionManager._waiting.push(id); + } + if (connectionManager.available()) { + if (chunkIdx == null) { + connectionManager._waiting.pop(); + connectionManager._open.push(id); + } else { + (function() { + var openChunksEntry = connectionManager._openChunks[id] || []; + openChunksEntry.push(chunkIdx); + connectionManager._openChunks[id] = openChunksEntry; + })(); + } + return true; + } + return false; + }, + reset: function() { + connectionManager._waiting = []; + connectionManager._open = []; + } + }, simple = { + send: function(id, name) { + handler._getFileState(id).loaded = 0; + log("Sending simple upload request for " + id); + handler.uploadFile(id).then(function(response, optXhr) { + log("Simple upload request succeeded for " + id); + var responseToReport = upload.normalizeResponse(response, true), size = options.getSize(id); + options.onProgress(id, name, size, size); + upload.maybeNewUuid(id, responseToReport); + upload.cleanup(id, responseToReport, optXhr); + }, function(response, optXhr) { + log("Simple upload request failed for " + id); + var responseToReport = upload.normalizeResponse(response, false); + if (!options.onAutoRetry(id, name, responseToReport, optXhr)) { + upload.cleanup(id, responseToReport, optXhr); + } + }); + } + }, upload = { + cancel: function(id) { + log("Cancelling " + id); + options.paramsStore.remove(id); + connectionManager.free(id); + }, + cleanup: function(id, response, optXhr) { + var name = options.getName(id); + options.onComplete(id, name, response, optXhr); + if (handler._getFileState(id)) { + handler._clearXhrs && handler._clearXhrs(id); + } + connectionManager.free(id); + }, + getProxyOrBlob: function(id) { + return handler.getProxy && handler.getProxy(id) || handler.getFile && handler.getFile(id); + }, + initHandler: function() { + var handlerType = namespace ? qq[namespace] : qq.traditional, handlerModuleSubtype = qq.supportedFeatures.ajaxUploading ? "Xhr" : "Form"; + handler = new handlerType[handlerModuleSubtype + "UploadHandler"](options, { + getDataByUuid: options.getDataByUuid, + getName: options.getName, + getSize: options.getSize, + getUuid: options.getUuid, + log: log, + onCancel: options.onCancel, + onProgress: options.onProgress, + onUuidChanged: options.onUuidChanged + }); + if (handler._removeExpiredChunkingRecords) { + handler._removeExpiredChunkingRecords(); + } + }, + isDeferredEligibleForUpload: function(id) { + return options.isQueued(id); + }, + maybeDefer: function(id, blob) { + if (blob && !handler.getFile(id) && blob instanceof qq.BlobProxy) { + options.onUploadPrep(id); + log("Attempting to generate a blob on-demand for " + id); + blob.create().then(function(generatedBlob) { + log("Generated an on-demand blob for " + id); + handler.updateBlob(id, generatedBlob); + options.setSize(id, generatedBlob.size); + handler.reevaluateChunking(id); + upload.maybeSendDeferredFiles(id); + }, function(errorMessage) { + var errorResponse = {}; + if (errorMessage) { + errorResponse.error = errorMessage; + } + log(qq.format("Failed to generate blob for ID {}. Error message: {}.", id, errorMessage), "error"); + options.onComplete(id, options.getName(id), qq.extend(errorResponse, preventRetryResponse), null); + upload.maybeSendDeferredFiles(id); + connectionManager.free(id); + }); + } else { + return upload.maybeSendDeferredFiles(id); + } + return false; + }, + maybeSendDeferredFiles: function(id) { + var idsInGroup = options.getIdsInProxyGroup(id), uploadedThisId = false; + if (idsInGroup && idsInGroup.length) { + log("Maybe ready to upload proxy group file " + id); + qq.each(idsInGroup, function(idx, idInGroup) { + if (upload.isDeferredEligibleForUpload(idInGroup) && !!handler.getFile(idInGroup)) { + uploadedThisId = idInGroup === id; + upload.now(idInGroup); + } else if (upload.isDeferredEligibleForUpload(idInGroup)) { + return false; + } + }); + } else { + uploadedThisId = true; + upload.now(id); + } + return uploadedThisId; + }, + maybeNewUuid: function(id, response) { + if (response.newUuid !== undefined) { + options.onUuidChanged(id, response.newUuid); + } + }, + normalizeResponse: function(originalResponse, successful) { + var response = originalResponse; + if (!qq.isObject(originalResponse)) { + response = {}; + if (qq.isString(originalResponse) && !successful) { + response.error = originalResponse; + } + } + response.success = successful; + return response; + }, + now: function(id) { + var name = options.getName(id); + if (!controller.isValid(id)) { + throw new qq.Error(id + " is not a valid file ID to upload!"); + } + options.onUpload(id, name); + if (chunkingPossible && handler._shouldChunkThisFile(id)) { + chunked.sendNext(id); + } else { + simple.send(id, name); + } + }, + start: function(id) { + var blobToUpload = upload.getProxyOrBlob(id); + if (blobToUpload) { + return upload.maybeDefer(id, blobToUpload); + } else { + upload.now(id); + return true; + } + } + }; + qq.extend(this, { + add: function(id, file) { + handler.add.apply(this, arguments); + }, + upload: function(id) { + if (connectionManager.open(id)) { + return upload.start(id); + } + return false; + }, + retry: function(id) { + if (concurrentChunkingPossible) { + handler._getFileState(id).temp.ignoreFailure = false; + } + if (connectionManager.isUsingConnection(id)) { + return upload.start(id); + } else { + return controller.upload(id); + } + }, + cancel: function(id) { + var cancelRetVal = handler.cancel(id); + if (qq.isGenericPromise(cancelRetVal)) { + cancelRetVal.then(function() { + upload.cancel(id); + }); + } else if (cancelRetVal !== false) { + upload.cancel(id); + } + }, + cancelAll: function() { + var waitingOrConnected = connectionManager.getWaitingOrConnected(), i; + if (waitingOrConnected.length) { + for (i = waitingOrConnected.length - 1; i >= 0; i--) { + controller.cancel(waitingOrConnected[i]); + } + } + connectionManager.reset(); + }, + getFile: function(id) { + if (handler.getProxy && handler.getProxy(id)) { + return handler.getProxy(id).referenceBlob; + } + return handler.getFile && handler.getFile(id); + }, + isProxied: function(id) { + return !!(handler.getProxy && handler.getProxy(id)); + }, + getInput: function(id) { + if (handler.getInput) { + return handler.getInput(id); + } + }, + reset: function() { + log("Resetting upload handler"); + controller.cancelAll(); + connectionManager.reset(); + handler.reset(); + }, + expunge: function(id) { + if (controller.isValid(id)) { + return handler.expunge(id); + } + }, + isValid: function(id) { + return handler.isValid(id); + }, + getResumableFilesData: function() { + if (handler.getResumableFilesData) { + return handler.getResumableFilesData(); + } + return []; + }, + getThirdPartyFileId: function(id) { + if (controller.isValid(id)) { + return handler.getThirdPartyFileId(id); + } + }, + pause: function(id) { + if (controller.isResumable(id) && handler.pause && controller.isValid(id) && handler.pause(id)) { + connectionManager.free(id); + handler.moveInProgressToRemaining(id); + return true; + } + return false; + }, + isResumable: function(id) { + return !!handler.isResumable && handler.isResumable(id); + } + }); + qq.extend(options, o); + log = options.log; + chunkingPossible = options.chunking.enabled && qq.supportedFeatures.chunking; + concurrentChunkingPossible = chunkingPossible && options.chunking.concurrent.enabled; + preventRetryResponse = function() { + var response = {}; + response[options.preventRetryParam] = true; + return response; + }(); + upload.initHandler(); + }; + qq.WindowReceiveMessage = function(o) { + "use strict"; + var options = { + log: function(message, level) {} + }, callbackWrapperDetachers = {}; + qq.extend(options, o); + qq.extend(this, { + receiveMessage: function(id, callback) { + var onMessageCallbackWrapper = function(event) { + callback(event.data); + }; + if (window.postMessage) { + callbackWrapperDetachers[id] = qq(window).attach("message", onMessageCallbackWrapper); + } else { + log("iframe message passing not supported in this browser!", "error"); + } + }, + stopReceivingMessages: function(id) { + if (window.postMessage) { + var detacher = callbackWrapperDetachers[id]; + if (detacher) { + detacher(); + } + } + } + }); + }; + qq.FormUploadHandler = function(spec) { + "use strict"; + var options = spec.options, handler = this, proxy = spec.proxy, formHandlerInstanceId = qq.getUniqueId(), onloadCallbacks = {}, detachLoadEvents = {}, postMessageCallbackTimers = {}, isCors = options.isCors, inputName = options.inputName, getUuid = proxy.getUuid, log = proxy.log, corsMessageReceiver = new qq.WindowReceiveMessage({ + log: log + }); + function expungeFile(id) { + delete detachLoadEvents[id]; + if (isCors) { + clearTimeout(postMessageCallbackTimers[id]); + delete postMessageCallbackTimers[id]; + corsMessageReceiver.stopReceivingMessages(id); + } + var iframe = document.getElementById(handler._getIframeName(id)); + if (iframe) { + iframe.setAttribute("src", "javascript:false;"); + qq(iframe).remove(); + } + } + function getFileIdForIframeName(iframeName) { + return iframeName.split("_")[0]; + } + function initIframeForUpload(name) { + var iframe = qq.toElement("