diff --git a/Gruntfile.js b/Gruntfile.js index be9a25b3..00856d9d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,45 +1,43 @@ /* eslint-env node */ module.exports = function ( grunt ) { grunt.loadNpmTasks( 'grunt-eslint' ); grunt.loadNpmTasks( 'grunt-jsonlint' ); grunt.loadNpmTasks( 'grunt-banana-checker' ); grunt.initConfig( { eslint: { options: { cache: true }, all: [ '**/*.js', '!node_modules/**', '!vendor/**', '!libs/jquery.browser.js', - '!libs/jquery.fancytree.js', '!libs/jquery.rateyo.js', '!libs/FullCalendar-2.9.1/fullcalendar.js', '!libs/FullCalendar-3.9.0/fullcalendar.js', '!libs/FullCalendar-3.9.0/locale-all.js', '!libs/FancyBox/jquery.fancybox.1.3.4.js', '!libs/FancyBox/jquery.fancybox.3.2.10.js', - '!libs/jquery.fancytree.ui-deps.js', - '!libs/jquery.fancytree.js', + '!libs/jstree.js', '!libs/jsgrid.js', '!libs/select2.js', '!libs/Sortable.js' ] }, banana: { all: 'i18n/' }, jsonlint: { all: [ '**/*.json', '!node_modules/**', '!vendor/**' ] } } ); grunt.registerTask( 'test', [ 'eslint', 'jsonlint', 'banana' ] ); grunt.registerTask( 'default', 'test' ); }; diff --git a/extension.json b/extension.json index dbe77fe2..1bcaaec5 100644 --- a/extension.json +++ b/extension.json @@ -1,551 +1,542 @@ { "name": "PageForms", "namemsg": "pageforms-name", "version": "4.9.5", "author": [ "Yaron Koren", "Stephan Gambke", "..." ], "url": "https://www.mediawiki.org/wiki/Extension:Page_Forms", "descriptionmsg": "pageforms-desc", "license-name": "GPL-2.0-or-later", "type": "specialpage", "namespaces": [ { "id": 106, "constant": "PF_NS_FORM", "name": "Form", "conditional": true }, { "id": 107, "constant": "PF_NS_FORM_TALK", "name": "Form_talk", "conditional": true } ], "callback": "PFHooks::registerExtension", "ExtensionFunctions": [ "PFHooks::initialize" ], "requires": { "MediaWiki": ">= 1.29.0" }, "GroupPermissions": { "*": { "viewedittab": true }, "sysop": { "editrestrictedfields": true }, "user": { "createclass": true, "multipageedit": true } }, "AvailableRights": [ "viewedittab", "editrestrictedfields", "createclass", "multipageedit" ], "Actions": { "formedit": "PFFormEditAction", "formcreate": "PFHelperFormAction" }, "SpecialPages": { "Forms": "PFForms", "CreateForm": "PFCreateForm", "Templates": "PFTemplates", "MultiPageEdit": "PFMultiPageEdit", "CreateTemplate": "PFCreateTemplate", "CreateClass": "PFCreateClass", "CreateCategory": "PFCreateCategory", "FormStart": "PFFormStart", "FormEdit": "PFFormEdit", "RunQuery": "PFRunQuery", "UploadWindow": "PFUploadWindow" }, "JobClasses": { "pageFormsCreatePage": "PFCreatePageJob" }, "APIModules": { "pfautocomplete": "PFAutocompleteAPI", "pfautoedit": "PFAutoeditAPI" }, "MessagesDirs": { "PageForms": [ "i18n" ] }, "ExtensionMessagesFiles": { "PageFormsAlias": "languages/PF_Aliases.php", "PageFormsMagic": "languages/PF_Magic.php", "PageFormsNS": "languages/PF_Namespaces.php" }, "AutoloadClasses": { "PFForms": "specials/PF_Forms.php", "PFCreateForm": "specials/PF_CreateForm.php", "PFTemplates": "specials/PF_Templates.php", "PFMultiPageEdit": "specials/PF_MultiPageEdit.php", "PFCreateTemplate": "specials/PF_CreateTemplate.php", "PFCreateClass": "specials/PF_CreateClass.php", "PFCreateCategory": "specials/PF_CreateCategory.php", "PFFormStart": "specials/PF_FormStart.php", "PFFormEdit": "specials/PF_FormEdit.php", "PFRunQuery": "specials/PF_RunQuery.php", "PFUploadForm": "specials/PF_UploadForm.php", "PFUploadSourceField": "specials/PF_UploadSourceField.php", "PFUploadWindow": "specials/PF_UploadWindow.php", "PFTemplateField": "includes/PF_TemplateField.php", "PFForm": "includes/PF_Form.php", "PFTemplate": "includes/PF_Template.php", "PFTemplateInForm": "includes/PF_TemplateInForm.php", "PFFormField": "includes/PF_FormField.php", "PFFormPrinter": "includes/PF_FormPrinter.php", "PFFormUtils": "includes/PF_FormUtils.php", "PFUtils": "includes/PF_Utils.php", "PFValuesUtils": "includes/PF_ValuesUtils.php", "PFHooks": "includes/PF_Hooks.php", "PFFormLinker": "includes/PF_FormLinker.php", "PFPageSchemas": "includes/PF_PageSchemas.php", "PFParserFunctions": "includes/PF_ParserFunctions.php", "PFAutocompleteAPI": "includes/PF_AutocompleteAPI.php", "PFAutoeditAPI": "includes/PF_AutoeditAPI.php", "PFFormEditAction": "includes/PF_FormEditAction.php", "PFHelperFormAction": "includes/PF_HelperFormAction.php", "PFPageSection": "includes/PF_PageSection.php", "PFFormInput": "includes/forminputs/PF_FormInput.php", "PFTextInput": "includes/forminputs/PF_TextInput.php", "PFTextWithAutocompleteInput": "includes/forminputs/PF_TextWithAutocompleteInput.php", "PFTextAreaInput": "includes/forminputs/PF_TextAreaInput.php", "PFTextAreaWithAutocompleteInput": "includes/forminputs/PF_TextAreaWithAutocompleteInput.php", "PFEnumInput": "includes/forminputs/PF_EnumInput.php", "PFMultiEnumInput": "includes/forminputs/PF_MultiEnumInput.php", "PFCheckboxInput": "includes/forminputs/PF_CheckboxInput.php", "PFCheckboxesInput": "includes/forminputs/PF_CheckboxesInput.php", "PFRadioButtonInput": "includes/forminputs/PF_RadioButtonInput.php", "PFDropdownInput": "includes/forminputs/PF_DropdownInput.php", "PFListBoxInput": "includes/forminputs/PF_ListBoxInput.php", "PFComboBoxInput": "includes/forminputs/PF_ComboBoxInput.php", "PFDateInput": "includes/forminputs/PF_DateInput.php", "PFDatePickerInput": "includes/forminputs/PF_DatePickerInput.php", "PFTimePickerInput": "includes/forminputs/PF_TimePickerInput.php", "PFDateTimePicker": "includes/forminputs/PF_DateTimePicker.php", "PFDateTimeInput": "includes/forminputs/PF_DateTimeInput.php", "PFYearInput": "includes/forminputs/PF_YearInput.php", "PFTreeInput": "includes/forminputs/PF_TreeInput.php", "PFTree": "includes/forminputs/PF_Tree.php", "PFTokensInput": "includes/forminputs/PF_TokensInput.php", "PFGoogleMapsInput": "includes/forminputs/PF_GoogleMapsInput.php", "PFOpenLayersInput": "includes/forminputs/PF_OpenLayersInput.php", "PFLeafletInput": "includes/forminputs/PF_LeafletInput.php", "PFRegExpInput": "includes/forminputs/PF_RegExpInput.php", "PFRatingInput": "includes/forminputs/PF_RatingInput.php", "PFWikiPage": "includes/wikipage/PF_WikiPage.php", "PFWikiPageTemplate": "includes/wikipage/PF_WikiPageTemplate.php", "PFWikiPageTemplateParam": "includes/wikipage/PF_WikiPageTemplateParam.php", "PFWikiPageSection": "includes/wikipage/PF_WikiPageSection.php", "PFWikiPageFreeText": "includes/wikipage/PF_WikiPageFreeText.php", "PFCreatePageJob": "includes/PF_CreatePageJob.php" }, "ResourceModules": { "ext.pageforms.main": { "scripts": [ "libs/PageForms.js", "libs/PF_preview.js" ], "styles": [ "skins/PageForms.css", "skins/PF_jquery_ui_overrides.css" ], "dependencies": [ "ext.pageforms.jqui.autocomplete", "ext.pageforms.sortable", "ext.pageforms.autogrow", "mediawiki.util", "mediawiki.api", "ext.pageforms.select2", "ext.pageforms.wikieditor", "ext.pageforms.editwarning" ], "messages": [ "pf_formerrors_header", "pf_too_few_instances_error", "pf_too_many_instances_error", "pf_blank_error", "pf_not_unique_error", "pf_bad_url_error", "pf_bad_email_error", "pf_bad_number_error", "pf_bad_integer_error", "pf_bad_date_error", "pf_modified_input_error", "pf_pipe_error" ] }, "ext.pageforms.browser": { "scripts": [ "libs/jquery.browser.js" ] }, "ext.pageforms.fancybox.jquery1": { "scripts": "libs/FancyBox/jquery.fancybox.1.3.4.js", "styles": "skins/FancyBox/jquery.fancybox.1.3.4.css", "dependencies": [ "ext.pageforms.browser" ] }, "ext.pageforms.fancybox.jquery3": { "scripts": "libs/FancyBox/jquery.fancybox.3.2.10.js", "styles": "skins/FancyBox/jquery.fancybox.3.2.10.css", "dependencies": [ "ext.pageforms.browser" ] }, - "ext.pageforms.fancytree.dep": { + "ext.pageforms.jstree": { "scripts": [ - "libs/jquery.fancytree.ui-deps.js" - ], - "styles": "skins/skin-win8/ui.fancytree.css" - - }, - "ext.pageforms.fancytree": { - "scripts": [ - "libs/jquery.fancytree.js", + "libs/jstree.js", "libs/PF_tree.js" ], - "styles": "skins/skin-win8/ui.fancytree.css", - "dependencies": [ - "ext.pageforms.fancytree.dep", - "ext.pageforms.jqui.fancytree.deps" + "styles": [ + "skins/jstree/jstree.css" ] }, "ext.pageforms.sortable": { "scripts": [ "libs/Sortable.js" ] }, "ext.pageforms.autogrow": { "scripts": [ "libs/PF_autogrow.js" ] }, "ext.pageforms.popupformedit": { "scripts": "libs/PF_popupform.js", "styles": "skins/PF_popupform.css", "dependencies": [ "ext.pageforms.browser" ] }, "ext.pageforms.autoedit": { "scripts": "libs/PF_autoedit.js", "styles": "skins/PF_autoedit.css", "messages": [ "pf-autoedit-wait", "pf_autoedit_anoneditwarning" ] }, "ext.pageforms.submit": { "scripts": [ "libs/PF_submit.js" ], "styles": [ "skins/PF_submit.css" ], "messages": [ "pf_formedit_saveandcontinue_summary", "pf_formedit_saveandcontinueediting" ] }, "ext.pageforms.collapsible": { "scripts": [ "libs/PF_collapsible.js" ], "styles": [ "skins/PF_collapsible.css" ] }, "ext.pageforms.imagepreview": { "scripts": [ "libs/PF_imagePreview.js" ] }, "ext.pageforms.checkboxes": { "scripts": [ "libs/PF_checkboxes.js" ], "styles": [ "skins/PF_checkboxes.css" ], "messages": [ "pf_forminputs_checkboxes_select_all", "pf_forminputs_checkboxes_select_none" ] }, "ext.pageforms.datepicker": { "scripts": [ "libs/PF_datepicker.js" ], "dependencies": [ "ext.pageforms.jqui.datepicker", "ext.pageforms.main" ] }, "ext.pageforms.timepicker": { "scripts": "libs/PF_timepicker.js", "styles": "skins/PF_Timepicker.css" }, "ext.pageforms.datetimepicker": { "scripts": "libs/PF_datetimepicker.js", "dependencies": [ "ext.pageforms.datepicker", "ext.pageforms.timepicker" ] }, "ext.pageforms.regexp": { "scripts": "libs/PF_regexp.js", "dependencies": [ "ext.pageforms.main" ] }, "ext.pageforms.rating": { "scripts": [ "libs/jquery.rateyo.js", "libs/PF_rating.js" ], "styles": "skins/jquery.rateyo.css" }, "ext.pageforms.simpleupload": { "scripts": [ "libs/PF_simpleupload.js" ], "messages": [ "pf_forminputs_change_file", "pf-simpleupload" ] }, "ext.pageforms.select2": { "scripts": [ "libs/select2.js", "libs/ext.pf.select2.base.js", "libs/ext.pf.select2.combobox.js", "libs/ext.pf.select2.tokens.js" ], "styles": [ "skins/select2/select2.css", "skins/select2/select2-bootstrap.css", "skins/ext.pf.select2.css" ], "dependencies": [ "ext.pageforms", "ext.pageforms.jqui.sortable", "mediawiki.jqueryMsg" ], "messages": [ "pf-select2-no-matches", "pf-select2-searching", "pf-select2-input-too-short", "pf-select2-selection-too-big" ] }, "ext.pageforms.fullcalendar.jquery1": { "scripts": [ "libs/FullCalendar-2.9.1/fullcalendar.js", "libs/PF_FullCalendar.js" ], "styles": [ "skins/FullCalendar-2.9.1/fullcalendar.css", "skins/PF_FullCalendar.css" ], "dependencies": [ "ext.pageforms.select2", "ext.pageforms.jqui.sortable", - "ext.pageforms.fancytree", + "ext.pageforms.jstree", "ext.pageforms", "moment", "mediawiki.jqueryMsg" ], "messages": [ "pf-calendar-createevent", "pf-calendar-deleteevent", "pf-calendar-updateevent" ] }, "ext.pageforms.fullcalendar.jquery3": { "scripts": [ "libs/FullCalendar-3.9.0/fullcalendar.js", "libs/PF_FullCalendar.js" ], "styles": [ "skins/FullCalendar-3.9.0/fullcalendar.css", "skins/PF_FullCalendar.css" ], "dependencies": [ "ext.pageforms.select2", "ext.pageforms.jqui.sortable", - "ext.pageforms.fancytree", + "ext.pageforms.jstree", "ext.pageforms", "moment", "mediawiki.jqueryMsg" ], "messages": [ "pf-calendar-createevent", "pf-calendar-deleteevent", "pf-calendar-updateevent" ] }, "ext.pageforms.jsgrid": { "scripts": [ "libs/jsgrid.js", "libs/PF_jsGrid.js" ], "styles": [ "skins/jsgrid/jsgrid.css", "skins/jsgrid/theme.css", "skins/PF_jsGrid.css" ], "dependencies": [ "ext.pageforms.select2", "ext.pageforms.jqui.sortable", "mediawiki.language.months" ], "messages": [ "pf-resultstoshow", "htmlform-yes", "htmlform-no" ] }, "ext.pageforms.balloon": { "styles": [ "skins/balloon.css" ] }, "ext.pageforms.wikieditor": { "scripts": "libs/PF_wikieditor.js", "styles": "skins/PF_wikieditor.css" }, "ext.pageforms": { "scripts": [ "libs/ext.pf.js" ] }, "ext.pageforms.editwarning": { "scripts": "libs/PF_editWarning.js", "dependencies": [ "mediawiki.confirmCloseWindow", "jquery.textSelection" ] }, "ext.pageforms.PF_CreateProperty": { "scripts": [ "libs/PF_CreateProperty.js" ] }, "ext.pageforms.PF_PageSchemas": { "scripts": [ "libs/PF_PageSchemas.js" ] }, "ext.pageforms.PF_CreateTemplate": { "scripts": [ "libs/PF_CreateTemplate.js" ], "messages": [ "pf_blank_error", "pf_createtemplate_hierarchystructureplaceholder" ] }, "ext.pageforms.PF_CreateClass": { "scripts": [ "libs/PF_CreateClass.js" ], "messages": [ "pf_createtemplate_hierarchystructureplaceholder" ] }, "ext.pageforms.PF_CreateForm": { "scripts": [ "libs/PF_CreateForm.js" ], "messages": [ "pf_blank_error" ] } }, "ResourceFileModulePaths": { "localBasePath": "", "remoteExtPath": "PageForms" }, "Hooks": { "SkinTemplateNavigation": [ "PFFormEditAction::displayTab", "PFHelperFormAction::displayTab" ], "ArticlePurge": "PFFormUtils::purgeCache", "PageContentSave": "PFFormUtils::purgeCache", "ParserFirstCallInit": "PFHooks::registerFunctions", "MakeGlobalVariablesScript": "PFHooks::setGlobalJSVariables", "PageSchemasRegisterHandlers": "PFPageSchemas::registerClass", "EditPage::importFormData": "PFHooks::showFormPreview", "HtmlPageLinkRendererEnd": "PFFormLinker::setBrokenLink", "CargoTablesActionLinks": "PFHooks::addToCargoTablesLinks", "CargoTablesSetAllowedActions": "PFHooks::addToCargoTablesColumns", "CargoTablesSetActionLinks": "PFHooks::addToCargoTablesRow", "TinyMCEDisable": "PFHooks::disableTinyMCE", "CanonicalNamespaces": "PFHooks::registerNamespaces", "ResourceLoaderRegisterModules": "PFHooks::registerModules" }, "config": { "PageFormsVisualEditorMaxHeight": 400, "PageFormsUseDisplayTitle": true, "PageFormsShowExpandAllLink": false, "PageFormsSimpleUpload": false, "PageFormsMaxAutocompleteValues": 1000, "PageFormsMaxLocalAutocompleteValues": 100, "PageFormsAutocompleteOnAllChars": false, "PageFormsCacheAutocompleteValues": false, "PageFormsAutocompleteCacheTimeout": null, "PageFormsRenameEditTabs": false, "PageFormsRenameMainEditTab": false, "PageFormsListSeparator": ",", "PageForms24HourTime": false, "PageFormsCacheFormDefinitions": false, "PageFormsFormCacheType": null, "PageFormsDisableOutsideServices": false, "PageFormsLinkAllRedLinksToForms": false, "PageFormsShowTabsForAllHelperForms": true, "PageFormsRunQueryFormAtTop": false, "PageFormsGoogleMapsKey": null, "PageFormsShowOnSelect": [], "PageFormsAutocompleteValues": [], "PageFormsCalendarParams": [], "PageFormsCalendarValues": [], "PageFormsGridValues": [], "PageFormsGridParams": [], "PageFormsContLangYes": null, "PageFormsContLangNo": null, "PageFormsContLangMonths": [], "PageFormsHeightForMinimizingInstances": 800, "PageFormsFieldProperties": [], "PageFormsCargoFields": [], "PageFormsDependentFields": [], "PageFormsCheckboxesSelectAllMinimum": 10, "PageFormsMapsWithFeeders": [], "PageFormsDatePickerSettings": { "@note": "See PF_DefaultInputSettings.php for allowed values", "FirstDate": null, "LastDate": null, "DateFormat": "SHORT", "WeekStart": null, "DisabledDaysOfWeek": null, "HighlightedDaysOfWeek": null, "DisabledDates": null, "HighlightedDates": null, "_merge_strategy": "array_plus" }, "PageFormsAutoeditNamespaces": [ 0 ] }, "manifest_version": 1 } diff --git a/includes/forminputs/PF_Tree.php b/includes/forminputs/PF_Tree.php index 709b6d7c..927e245e 100644 --- a/includes/forminputs/PF_Tree.php +++ b/includes/forminputs/PF_Tree.php @@ -1,126 +1,250 @@ title = $curTitle; + public function __construct( $depth, $cur_values ) { + $this->depth = $depth; + $this->current_values = $cur_values; + $this->title = ""; $this->children = []; + $this->cur_value = ""; } public function addChild( $child ) { $this->children[] = $child; } /** - * Turn a manually-created "structure", defined as a bulleted list - * in wikitext, into a tree. This is based on the concept originated - * by the "menuselect" input type in the Semantic Forms Inputs - * extension - the difference here is that the text is manually - * parsed, instead of being run through the MediaWiki parser. + * This Function takes the wikitext-styled bullets as a parameter, and converts it into + * an array which is used within the class to modify the data passed to JS. * @param string $wikitext - * @return self */ - public static function newFromWikiText( $wikitext ) { - // The top node, called "Top", will be ignored, because - // we'll set "hideroot" to true. - $fullTree = new PFTree( 'Top' ); + public function getTreeFromWikiText( $wikitext ) { $lines = explode( "\n", $wikitext ); + $full_tree = []; + $temporary_values = []; foreach ( $lines as $line ) { $numBullets = 0; for ( $i = 0; $i < strlen( $line ) && $line[$i] == '*'; $i++ ) { $numBullets++; } - if ( $numBullets == 0 ) { - continue; - } $lineText = trim( substr( $line, $numBullets ) ); - $curParentNode = $fullTree->getLastNodeForLevel( $numBullets ); - $curParentNode->addChild( new PFTree( $lineText ) ); + $full_tree[] = [ 'level' => $numBullets, "text" => $lineText ]; + + if ( in_array( $lineText, $this->current_values ) && !in_array( $lineText, $temporary_values ) ) { + $temporary_values[] = $lineText; + } } - return $fullTree; + $this->tree_array = $full_tree; + $this->current_values = $temporary_values; + $this->configArray(); + $this->setParentsId(); + $this->setChildren(); } - public function getLastNodeForLevel( $level ) { - if ( $level <= 1 || count( $this->children ) == 0 ) { - return $this; + /** + * This function sets an ID for each element to be used in the function setParentsId() + * so that every child can know its parent + * This function also determine whether or not the node will be opened, depending on + * the attribute $depth. + * This function also determine whether or not the node is selected. + */ + private function configArray() { + for ( $i = 0; $i < count( $this->tree_array ); $i++ ) { + $this->tree_array[$i]['node_id'] = $i; + if ( $this->tree_array[$i]['level'] <= $this->depth ) { + $this->tree_array[$i]['state']['opened'] = true; + } + if ( in_array( $this->tree_array[$i]['text'], $this->current_values ) ) { + $this->tree_array[$i]['state']['selected'] = true; + } } - $lastNodeOnCurLevel = end( $this->children ); - return $lastNodeOnCurLevel->getLastNodeForLevel( $level - 1 ); } /** + * For the tree array that was generated from wikitext, the node doesn't know its parent + * although it's easy to know for the human. + * This function searches for the nodes and get the closest node of the parent level, and + * sets it as a parent. + * The parent ID will be used in the function setChildren() that adds every child to its + * parent's attribute "children" + */ + private function setParentsId() { + $numNodes = count( $this->tree_array ); + for ( $i = $numNodes - 1; $i >= 0; $i-- ) { + for ( $j = $i; $j >= 0; $j-- ) { + if ( $this->tree_array[$i]['level'] - $this->tree_array[$j]['level'] == 1 ) { + $this->tree_array[$i]['parent_id'] = $this->tree_array[$j]['node_id']; + break; + } + } + } + } + + /** + * This function convert the attribute $tree_array from its so-called flat structure + * into tree-like structure, as every node has an attribute called "children" that holds + * the children of this node. + * The attribute "children" is important because it is used in the library jsTree. + */ + private function setChildren() { + for ( $i = count( $this->tree_array ) - 1; $i >= 0; $i-- ) { + for ( $j = $i; $j >= 0; $j-- ) { + if ( isset( $this->tree_array[$i]['parent_id'] ) ) { + if ( $this->tree_array[$i]['parent_id'] == $this->tree_array[$j]['node_id'] ) { + if ( isset( $this->tree_array[$j]['children'] ) ) { + array_unshift( $this->tree_array[$j]['children'], $this->tree_array[$i] ); + } else { + $this->tree_array[$j]['children'][] = $this->tree_array[$i]; + } + unset( $this->tree_array[$i] ); + } + } + } + } + } + + /** + * This Function takes the Top Category name as a parameter, and generate + * tree_array, which is used within the class to modify the data passed to JS. * @param string $top_category - * @return mixed + * @param bool $hideroot + */ + public function getFromTopCategory( $top_category, $hideroot ) { + $this->top_category = $top_category; + $this->populateChildren(); + + $this->tree_array[0]['text'] = $top_category; + $children = $this->children; + $this->tree_array[0]['level'] = 1; + $this->tree_array[0]['state']['opened'] = true; + + $children = self::addSubCategories( $children, 2, $this->depth, $this->current_values ); + + $this->tree_array[0]['children'] = $children; + + $this->current_values = self::getCurValues( $this->tree_array ); + + if ( $hideroot ) { + $this->tree_array = $this->tree_array[0]['children']; + } + } + + /** + * This function handles adding the children of the nodes in the Top Category tree. + * Also, it determines whether or not the node is selected depending on $cur_values + * @param array $children + * @param int $level + * @param int $depth + * @param array $cur_values + * @return array */ - public static function newFromTopCategory( $top_category ) { - $pfTree = new PFTree( $top_category ); - $defaultDepth = 20; - $pfTree->populateChildren( $defaultDepth ); - return $pfTree; + public static function addSubCategories( $children, $level, $depth, $cur_values ) { + $newChildren = []; + foreach ( $children as $child ) { + $is_selected = false; + if ( $cur_values !== null ) { + if ( in_array( $child->title, $cur_values ) ) { + $is_selected = true; + unset( $cur_values[ array_search( $child->title, $cur_values ) ] ); + } + } + + $newChild = [ + 'text' => $child->title, + 'level' => $level, + 'children' => self::addSubCategories( $child->children, $level + 1, $depth, $cur_values ) + ]; + $newChild['state']['opened'] = $level <= $depth; + if ( $is_selected ) { + $newChild['state']['selected'] = true; + } + $newChildren[] = $newChild; + } + return $newChildren; + } + + private static function getCurValues( $tree ) { + $cur_values = []; + foreach ( $tree as $node ) { + if ( isset( $node['state']['selected'] ) && $node['state']['selected'] ) { + $cur_values[] = $node['text']; + } + if ( isset( $node['children'] ) ) { + $children = self::getCurValues( $node['children'] ); + $cur_values = array_merge( $cur_values, $children ); + } + } + return $cur_values; } /** * Recursive function to populate a tree based on category information. - * @param int $depth */ - private function populateChildren( $depth ) { - if ( $depth == 0 ) { + private function populateChildren() { + if ( $this->depth == 0 ) { return; } - $subcats = self::getSubcategories( $this->title ); + $subcats = self::getSubcategories( $this->top_category ); foreach ( $subcats as $subcat ) { - $childTree = new PFTree( $subcat ); - $childTree->populateChildren( $depth - 1 ); + $childTree = new PFTree( $this->depth - 1, $this->current_values ); + $childTree->top_category = $subcat; + $childTree->title = $subcat; + $childTree->populateChildren(); $this->addChild( $childTree ); } } /** * Gets all the subcategories of the passed-in category. * * @todo This might not belong in this class. * * @param string $categoryName * @return array */ private static function getSubcategories( $categoryName ) { $dbr = wfGetDB( DB_REPLICA ); $tables = [ 'page', 'categorylinks' ]; $fields = [ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect', 'page_len', 'page_latest', 'cl_to', 'cl_from' ]; $where = []; $joins = []; $options = [ 'ORDER BY' => 'cl_type, cl_sortkey' ]; $joins['categorylinks'] = [ 'JOIN', 'cl_from = page_id' ]; $where['cl_to'] = str_replace( ' ', '_', $categoryName ); $options['USE INDEX']['categorylinks'] = 'cl_sortkey'; $tables = array_merge( $tables, [ 'category' ] ); $fields = array_merge( $fields, [ 'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files' ] ); $joins['category'] = [ 'LEFT JOIN', [ 'cat_title = page_title', 'page_namespace' => NS_CATEGORY ] ]; $res = $dbr->select( $tables, $fields, $where, __METHOD__, $options, $joins ); $subcats = []; foreach ( $res as $row ) { $t = Title::newFromRow( $row ); if ( $t->getNamespace() == NS_CATEGORY ) { $subcats[] = $t->getText(); } } return $subcats; } - } diff --git a/includes/forminputs/PF_TreeInput.php b/includes/forminputs/PF_TreeInput.php index 5ad3e1cf..948eb900 100644 --- a/includes/forminputs/PF_TreeInput.php +++ b/includes/forminputs/PF_TreeInput.php @@ -1,304 +1,213 @@ [] ]; } public static function getDefaultCargoTypeLists() { return [ 'Hierarchy' => [] ]; } public static function getOtherCargoTypesHandled() { return [ 'String', 'Page' ]; } public static function getOtherCargoTypeListsHandled() { return [ 'String', 'Page' ]; } public static function getHTML( $cur_value, $input_name, $is_mandatory, $is_disabled, array $other_args ) { // Handle the now-deprecated 'category' and 'categories' // input types. if ( array_key_exists( 'input type', $other_args ) && $other_args['input type'] == 'category' ) { - $inputType = "radio"; self::$multipleSelect = false; } elseif ( array_key_exists( 'input type', $other_args ) && $other_args['input type'] == 'categories' ) { - $inputType = "checkbox"; self::$multipleSelect = true; } else { $is_list = ( array_key_exists( 'is_list', $other_args ) && $other_args['is_list'] == true ); if ( $is_list ) { - $inputType = "checkbox"; self::$multipleSelect = true; } else { - $inputType = "radio"; self::$multipleSelect = false; } } // get list delimiter - default is comma if ( array_key_exists( 'delimiter', $other_args ) ) { $delimiter = $other_args['delimiter']; } else { $delimiter = ','; } $cur_values = PFValuesUtils::getValuesArray( $cur_value, $delimiter ); if ( array_key_exists( 'height', $other_args ) ) { $height = Sanitizer::checkCSS( $other_args['height'] ); } else { $height = '100'; } if ( array_key_exists( 'width', $other_args ) ) { $width = Sanitizer::checkCSS( $other_args['width'] ); } else { $width = '500'; } if ( array_key_exists( 'depth', $other_args ) ) { $depth = $other_args['depth']; } else { $depth = '10'; } if ( array_key_exists( 'top category', $other_args ) ) { $top_category = $other_args['top category']; $title = self::makeTitle( $top_category ); if ( $title->getNamespace() != NS_CATEGORY ) { return null; } - - $tree = PFTree::newFromTopCategory( $top_category ); $hideroot = array_key_exists( 'hideroot', $other_args ); + + $pftree = new PFTree( $depth, $cur_values ); + $pftree->getFromTopCategory( $top_category, $hideroot ); } elseif ( array_key_exists( 'structure', $other_args ) ) { $structure = $other_args['structure']; - $tree = PFTree::newFromWikiText( $structure ); - $hideroot = true; + + $pftree = new PFTree( $depth, $cur_values ); + $pftree->getTreeFromWikiText( $structure ); + } else { // Escape - we can't do anything. return null; } - $inputText = self::treeToHTML( $tree, $input_name, $cur_values, $hideroot, $depth, $inputType ); - - // Replace values one at a time, by an incrementing index - - // inspired by http://bugs.php.net/bug.php?id=11457 - $dummy_str = "REPLACE THIS TEXT"; - $i = 0; - while ( ( $a = strpos( $inputText, $dummy_str ) ) > 0 ) { - $inputText = substr( $inputText, 0, $a ) . $i++ . substr( $inputText, $a + strlen( $dummy_str ) ); - } - $class = 'pfTreeInput'; if ( $is_mandatory ) { $class .= ' mandatory'; } - $text = Html::rawElement( - 'div', - [ - 'class' => $class, - 'id' => $input_name . 'treeinput', - 'style' => 'height: ' . $height . 'px; width: ' . $width . 'px; overflow: auto; position: relative;' - ], - $inputText - ); - - return $text; - } - - // Perhaps treeToHTML() and nodeToHTML() should be moved to the - // PFTree class? Currently PFTree doesn't know about HTML stuff, but - // maybe it should. - private static function treeToHTML( $fullTree, $input_name, $current_selection, $hideprefix, $depth, $inputType ) { - $key_prefix = $input_name . "key"; - $text = ''; - if ( !$hideprefix ) { - $text .= "\n"; - } - if ( self::$multipleSelect ) { - $text .= Html::hidden( $input_name . '[is_list]', 1 ); - } - return $text; - } - - private static function nodeToHTML( $node, $key_prefix, $input_name, $current_selection, $hidenode, $depth, $inputType, $index = 1 ) { - global $wgPageFormsTabIndex; - - $text = ''; - - $key_id = "$key_prefix-$index"; - // Replace characters not allowed in HTML IDs. - $key_id = preg_replace( '/[^a-zA-Z0-9-_:\.]/', '-', $key_id ); - // Make sure it starts with a letter. - preg_match( '/$[^a-zA-Z]/', $key_id, $matches ); - if ( count( $matches ) > 0 ) { - $key_id = 'a' . $key_id; - } - - if ( !$hidenode ) { - $liAttribs = [ 'id' => $key_id ]; - if ( in_array( $node->title, $current_selection ) ) { - $liAttribs['class'] = 'selected'; - } - if ( $depth > 0 ) { - $liAttribs['data'] = "'expand': true"; - } - // For some reason, the Dynatree JS library requires - // unclosed
  • tags; "
  • ...
  • " won't work. - $text .= Html::openElement( 'li', $liAttribs ); - $dummy_str = "REPLACE THIS TEXT"; + $tree = json_encode( $pftree->tree_array ); + $cur_value = implode( $delimiter, $pftree->current_values ); - $cur_input_name = $input_name; - if ( self::$multipleSelect ) { - $cur_input_name .= "[" . $dummy_str . "]"; - } - $nodeAttribs = [ - 'tabindex' => $wgPageFormsTabIndex, - 'id' => "chb-$key_id", - 'class' => 'hidden', - 'hidden', - 'style' => 'display:none', - ]; - if ( in_array( $node->title, $current_selection ) ) { - $nodeAttribs['checked'] = true; - } - - $text .= Html::input( $cur_input_name, $node->title, $inputType, $nodeAttribs ); - - $nodeDisplayTitle = $node->title; - Hooks::run( 'PageForms::TreeNodeDisplay', [ &$nodeDisplayTitle ] ); - $text .= $nodeDisplayTitle . "\n"; - } - - if ( array_key_exists( 'children', $node ) ) { - $text .= "\n"; - } + $params['multiple'] = self::$multipleSelect; + $params['delimiter'] = $delimiter; + $params['cur_value'] = $cur_value; + $params = json_encode( $params ); + $text = "
    "; return $text; } public static function getParameters() { $params = parent::getParameters(); $params[] = [ 'name' => 'top category', 'type' => 'string', 'description' => wfMessage( 'pf_forminputs_topcategory' )->text() ]; $params[] = [ 'name' => 'structure', 'type' => 'text', 'description' => wfMessage( 'pf_forminputs_structure' )->text() ]; $params[] = [ 'name' => 'hideroot', 'type' => 'boolean', 'description' => wfMessage( 'pf_forminputs_hideroot' )->text() ]; $params[] = [ 'name' => 'depth', 'type' => 'int', 'description' => wfMessage( 'pf_forminputs_depth' )->text() ]; $params[] = [ 'name' => 'height', 'type' => 'int', 'description' => wfMessage( 'pf_forminputs_height' )->text() ]; $params[] = [ 'name' => 'width', 'type' => 'int', 'description' => wfMessage( 'pf_forminputs_width' )->text() ]; return $params; } /** * Returns the HTML code to be included in the output page for this input. * @return string */ public function getHtmlText() { return self::getHTML( $this->mCurrentValue, $this->mInputName, $this->mIsMandatory, $this->mIsDisabled, $this->mOtherArgs ); } /** * Creates a Title object from a user-provided (and thus unsafe) string * @param string $title * @return null|Title */ static function makeTitle( $title ) { $title = trim( $title ); if ( strval( $title ) === '' ) { return null; } # The title must be in the category namespace # Ignore a leading Category: if there is one $t = Title::newFromText( $title, NS_CATEGORY ); if ( !$t || $t->getNamespace() != NS_CATEGORY || $t->getInterwiki() != '' ) { // If we were given something like "Wikipedia:Foo" or "Template:", // try it again but forced. $title = "Category:$title"; $t = Title::newFromText( $title ); } return $t; } } diff --git a/libs/PF_FullCalendar.js b/libs/PF_FullCalendar.js index 3e9f77b4..e9e4a011 100644 --- a/libs/PF_FullCalendar.js +++ b/libs/PF_FullCalendar.js @@ -1,1178 +1,1178 @@ /** * Code to integrate the FullCalendar JavaScript library into Page Forms. * * @author Priyanshu Varshney */ /* global moment */ ( function ( $, mw, pf ) { 'use strict'; $( '.pfFullCalendarJS' ).each( function() { $( '#fullCalendarLoading' ).css("display", "block"); // This counter is used to assign unique ids to the calendar events. // If the event is deleted, we lose that unique id and can't be used gain. var counter = 0; var monthNames = mw.config.get('monthMessages'); // Stuff from PF_FormPrinter.php var calendarParams = mw.config.get( 'wgPageFormsCalendarParams' ); var calendarGridValues = mw.config.get( 'wgPageFormsCalendarValues' ); var calendarHTML = mw.config.get('wgPageFormsCalendarHTML'); var $fcDiv = $( this ); var calendarId = $fcDiv.attr( 'id' ); var templateName = $fcDiv.attr( 'template-name' ); var eventTitleField = $fcDiv.attr( 'title-field' ); var eventDateField = $fcDiv.attr( 'event-date-field' ); var eventStartDateField = $fcDiv.attr( 'event-start-date-field' ); var eventEndDateField = $fcDiv.attr( 'event-end-date-field' ); var flagOneDayEvent = true; var pageLoaded = false; var isEventEndDateTime = false; var isEventStartTime = false; if( eventDateField === undefined ) { flagOneDayEvent = false; } var fieldType=[]; var englishMonthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; // From here the game begins - getting the form HTML to be used as the popup form - // for the calendar interface var formHtml = calendarHTML[templateName]; var popup = '
    '; var deleteButton = ''; var createButton = ''; var updateButton = ''; popup += formHtml; var createEventPopup = popup + createButton; var updateEventPopup = popup + updateButton; var suitableForCalendar = true; var calendarIdSelector = '#' + calendarId; var events = [], data = [], dateFields = [], dateStartFields = [], dateEndFields = [], eventsNoDate = [], checkboxesNum = []; var segment, dateSegment, yearFC, monthFC, dateFC, timeSegment, hourFC, minuteFC, secondFC, ampm24h, dateEntry, monthEntry, yearEntry, hourEntry, minuteEntry, secondEntry, ampm24hEntry, regularEntry, eventData, preEventData, currParam, temp, titleIndex, monthIndex, eventDate, eventDateDay,eventDateYear, eventDateMonth, eventDateHour, eventStartDate, eventEndDate , eventDateMinute, eventDateSecond, eventDateAmPm24h, reserveDate, idForm, eventStartDateDay, eventStartDateYear, eventStartDateMonth, eventStartDateHour, eventStartDateMinute, eventStartDateSecond, eventStartDateAmPm24; var currentEndDateMoment; var checkboxesValues = []; var listboxValues = []; var tokensProto, comboboxProto, result, eventTemplateName, parameterName, eventContents, allEvents, dateElement, nextDate, formatted, i, j; var autoFillDay = templateName + '[cf][' + eventDateField + '][day]', autoFillMonth = templateName + '[cf][' + eventDateField + '][month]', autoFillYear = templateName + '[cf][' + eventDateField + '][year]', autoFillHour = templateName+'[cf]['+ eventDateField + '][hour]', autoFillMinute = templateName+'[cf]['+ eventDateField + '][minute]', autoFillSecond = templateName+'[cf]['+ eventDateField + '][second]', autoFillAmPm24h = templateName+'[cf]['+ eventDateField + '][ampm24h]', autoFillStartDay = templateName + '[cf][' + eventStartDateField + '][day]', autoFillStartMonth = templateName + '[cf][' + eventStartDateField + '][month]', autoFillStartYear = templateName + '[cf][' + eventStartDateField + '][year]', autoFillEndDay = templateName + '[cf][' + eventEndDateField + '][day]', autoFillEndMonth = templateName + '[cf][' + eventEndDateField + '][month]', autoFillEndYear = templateName + '[cf][' + eventEndDateField + '][year]', autoFillStartHour = templateName+'[cf]['+ eventStartDateField + '][hour]', autoFillStartMinute = templateName+'[cf]['+ eventStartDateField + '][minute]', autoFillStartSecond = templateName+'[cf]['+ eventStartDateField + '][second]', autoFillStartAmPm24h = templateName+'[cf]['+ eventStartDateField + '][ampm24h]', autoFillEndHour = templateName+'[cf]['+ eventEndDateField + '][hour]', autoFillEndMinute = templateName+'[cf]['+ eventEndDateField + '][minute]', autoFillEndSecond = templateName+'[cf]['+ eventEndDateField + '][second]', autoFillEndAmPm24h = templateName+'[cf]['+ eventEndDateField + '][ampm24h]'; for( i = 0; i' ); $("[class|='fancybox-close-small']").attr("type", "button"); // Handle token input type $( ':input' ).each(function( ) { tokensProto = new pf.select2.tokens(); if( $( this ).hasClass( 'pfTokens' )){ tokensProto.apply( $(this) ); } }); // Handling the text with autocomplete $('#popupForm').find(".autocompleteInput").each( function() { $(this).attachAutocomplete(); }); // Handle combobox input type // $( ':input' ).each(function( ) { // comboboxProto = new pf.select2.combobox(); // if( $( this ).hasClass( 'pfComboBox' )){ // comboboxProto.apply($(this)); // } // }); // Handle the tree input types $('#popupForm').find(".pfTreeInput").each( function() { - $(this).applyFancytree(); + $(this).applyJSTree(); }); $('#popupForm').find(".pfRating").each( function() { - $(this).applyRatingInput(); + $(this).applyJSTree(); }); // Check if the event is only one day long // For current code - it is required to see if the event is one day long or not. // In future the code can be reduced and this if-else condition can be removed if ( flagOneDayEvent === true ) { idForm = start.format() + "_fc" + counter; // Atuomatically set the event date value $( ':input[name="' + autoFillDay + '"]' ).val( start.format( 'DD' ) ); $( ':input[name="' + autoFillYear + '"]' ).val( start.format( 'YYYY' ) ); if ( mw.config.get('wgAmericanDates') ) { //check for date-style format. $( ':input[name="' + autoFillMonth + '"]' ).val( englishMonthNames[parseInt( start.format( 'MM' ) ) -1 ] ); } else { $( ':input[name="' + autoFillMonth + '"]' ).val( start.format( 'MM' ) ); } checkAndSave( 'single' ); // Save all the data of the popup form and set the title, event date and the unique ID of the event $( "#form_submit" ).click(function( event ) { saveData( 'single' ); resetDateAndTime(); setDateAndTime( dateFields ); eventDate = eventDateYear + '-' + eventDateMonth + '-' + eventDateDay; reserveDate = eventDate; eventDate = checkDateTime( dateFields, eventDate ); eventData = { title: data[titleIndex].value, start: eventDate, end: reserveDate + 'T23:59:59', contents: data, id:idForm }; counter++; $( '.fancybox-close-small' ).click(); $( calendarIdSelector ).fullCalendar( 'renderEvent', eventData, true ); }); } else { idForm = start.format() + "_fc" + counter; currentEndDateMoment = moment(end); currentEndDateMoment = currentEndDateMoment.subtract(1 , 'days'); $( ':input[name="' + autoFillStartDay + '"]' ).val( Number(start.format( 'DD' )) ); $( ':input[name="' + autoFillStartYear + '"]' ).val( start.format( 'YYYY' ) ); if ( mw.config.get('wgAmericanDates') ) { //check for date-style format. $( ':input[name="' + autoFillStartMonth + '"]' ).val( englishMonthNames[parseInt( start.format( 'MM' ) ) -1 ] ); } else { $( ':input[name="' + autoFillStartMonth + '"]' ).val( start.format( 'MM' ) ); } $( ':input[name="' + autoFillEndDay + '"]' ).val( Number(currentEndDateMoment.format( 'DD' )) ); $( ':input[name="' + autoFillEndYear + '"]' ).val( currentEndDateMoment.format( 'YYYY' ) ); if ( mw.config.get('wgAmericanDates') ) { //check for date-style format. $( ':input[name="' + autoFillEndMonth + '"]' ).val( englishMonthNames[parseInt( currentEndDateMoment.format( 'MM' ) ) -1 ] ); } else { $( ':input[name="' + autoFillEndMonth + '"]' ).val( currentEndDateMoment.format( 'MM' ) ); } checkAndSave( 'multiple' ); $( "#form_submit" ).click(function( event ) { saveData( 'multiple' ); resetDateAndTime(); setDateAndTime( dateStartFields, '1' ); eventStartDate = eventDateYear + '-' + eventDateMonth + '-' + eventDateDay; reserveDate = eventStartDate; eventStartDate = checkDateTime( dateStartFields, eventStartDate ); resetDateAndTime(); setDateAndTime( dateEndFields, '2' ); eventEndDate = eventDateYear + '-' + eventDateMonth + '-' + eventDateDay; reserveDate = eventEndDate; eventEndDate = checkDateTime( dateEndFields, eventEndDate ); if ( fieldType[eventEndDateField] === 'date' ) { dateElement = new Date(eventEndDate); nextDate = new Date(dateElement.setDate(dateElement.getDate() + 1)); formatted = nextDate.getUTCFullYear() + '-' + padNumber(nextDate.getUTCMonth() + 1) + '-' + padNumber(nextDate.getUTCDate()); eventEndDate = formatted; } eventData = { title: data[titleIndex].value, start: eventStartDate, end: eventEndDate, resourceEditable:true, contents: data, id:idForm }; counter++; $( '.fancybox-close-small' ).click(); $( calendarIdSelector ).fullCalendar( 'renderEvent', eventData, true ); }); } }, // // Edit an event placed on the calendar by simply clicking on it eventClick: function( info ) { var content = $( calendarIdSelector ).fullCalendar( 'clientEvents', info.id ); var formContents = content[0].contents; var ratingArr = []; checkboxesValues = []; var paramName, rateSample = 0; // Open the popup form and populate it with the values to allow editing $.fancybox.open( updateEventPopup + deleteButton + '' ); $("[class|='fancybox-close-small']").attr("type", "button"); $('#popupForm').find(".pfTreeInput").each( function() { $(this).applyFancytree(); }); // Prepare the popup form for editing for( i =0 ; i').attr( 'type', 'hidden' ).attr( 'name', inputName ).attr( 'value',finalFieldValues[inputName] ).appendTo( '#pfForm' ); } } for( var k =0;k').attr( 'type', 'hidden' ).attr( 'name', entryName ).attr( 'value',eventsNoDate[k][parameterName] ).appendTo( '#pfForm' ); } } }); }); }( jQuery, mediaWiki, pf ) ); diff --git a/libs/PF_tree.js b/libs/PF_tree.js index 4cea4b05..1dd8a3ba 100644 --- a/libs/PF_tree.js +++ b/libs/PF_tree.js @@ -1,90 +1,105 @@ /** - * Defines the applyFancytree() function, which turns an HTML "tree" of + * Defines the applyJStree() function, which turns an HTML "tree" of * checkboxes or radiobuttons into a dynamic and collapsible tree of options - * using the Fancytree JS library. + * using the jsTree JS library. * * @author Mathias Lidal * @author Yaron Koren * @author Priyanshu Varshney + * @author Amr El-Absy */ -( function( $, mw, pf ) { - 'use strict'; - - // Attach the Fancytree widget to an existing
    element - // and pass the tree options as an argument to the fancytree() function. - jQuery.fn.applyFancytree = function() { - var node = this; - var selectMode = 2; - var checkboxClass = "fancytree-checkbox"; - if (node.find(":input:radio").length) { - selectMode = 1; - checkboxClass = "fancytree-radio"; - } + ( function ($, mw, pf) { - var newClassNames = { - checkbox: checkboxClass, - selected: "fancytree-selected" - }; + pf.TreeInput = function (elem) { + this.element = elem; + this.id = $(this.element).attr('id'); + }; - node.fancytree({ - checkbox: true, - autoScroll: true, - minExpandLevel: 5, - _classNames: newClassNames, - selectMode: selectMode, - // click event allows user to de/select the checkbox - // by just selecting the title - click: function(event, data) { - var node = data.node, - // Only for click and dblclick events - // 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' - targetType = data.targetType; - if ( targetType === "expander" ) { - data.node.toggleExpanded(); - } else if ( targetType === "checkbox" || - targetType === "title" ) { - data.node.toggleSelected(); - } - return false; - }, + var TreeInput_proto = new pf.TreeInput(); - // Un/check checkboxes/radiobuttons recursively after - // selection. - select: function (event, data) { - if ( data.node === undefined ) { - return; - } - var inputkey = "chb-" + data.node.key; - var checkBoxes = node.find("[id='" + inputkey + "']"); - checkBoxes.attr("checked", !checkBoxes.attr("checked")); - }, - // Prevent reappearing of checkbox when node is - // collapsed. - expand: function(select, data) { - if ( data.node === undefined ) { - return; + TreeInput_proto.setOptions = function () { + var data = $(this.element).attr('data'); + this.data = JSON.parse(data); + var params = $(this.element).attr('params'); + this.params = JSON.parse(params); + this.delimiter = this.params.delimiter; + this.multiple = this.params.multiple; + this.values = []; + this.cur_value = this.params.cur_value; + + var options = { + 'plugins' : [ 'checkbox' ], + 'core' : { + 'data' : this.data, + 'multiple': this.multiple, + 'themes' : { + "icons": false } - $("#chb-" + data.node.key).attr("checked", - data.node.isSelected()).addClass("hidden"); }, + 'checkbox': { + 'three_state': false, + 'cascade': "none" + } + }; - }); + return options; + }; - // Update real checkboxes according to selections. - $.map(node.fancytree("getTree").getSelectedNodes(), - function (data) { - if ( data.node === undefined ) { - return; - } - $("#chb-" + data.node.key).attr("checked", true); - data.node.setActive(); - }); - var activeNode = node.fancytree("getTree").getActiveNode(); - if (activeNode !== null) { - activeNode.setActive(false); + TreeInput_proto.check = function( data ) { + var div_id = $(this.element).attr('id'); + var input_name = div_id.replace("treeinput", ""); + var input = $(this.element).next('input.PFTree_data'); + + if ( this.multiple ) { + this.values.push( data ); + var data_string = this.values.join( this.delimiter ); + input.attr( 'value', data_string ); + } else { + this.values.push( data ); + input.attr('value', data); } + }; + + TreeInput_proto.uncheck = function( data ) { + var div_id = $( this.element ).attr('id'); + var input_name = div_id.replace( "treeinput", "" ); + var input = $( this.element ).next( 'input.PFTree_data' ); + this.values.splice( this.values.indexOf( data ), 1 ); + var data_string = this.values.join( this.delimiter ); + input.attr( 'value', data_string ); }; -}( jQuery, mediaWiki, pf ) ); + TreeInput_proto.setCurValue = function () { + if ( this.cur_value !== null && this.cur_value !== undefined && this.cur_value !== "" ) { + var div_id = $( this.element ).attr('id'); + var input_name = div_id.replace( "treeinput", "" ); + var input = $( this.element ).next( 'input.PFTree_data' ); + + input.attr( 'value', this.cur_value ); + this.values = this.cur_value.split( this.delimiter ); + } + }; + + pf.TreeInput.prototype = TreeInput_proto; + +} (jQuery, mediaWiki, pf) ); + +$.fn.extend({ + applyJSTree: function () { + var tree = new pf.TreeInput(this); + var options = tree.setOptions(); + + $(this).jstree(options); + + $(this).bind('select_node.jstree', function (evt, data) { + tree.check(data.node.text); + }); + $(this).bind('deselect_node.jstree', function (evt, data) { + tree.uncheck(data.node.text); + }); + + tree.setCurValue(); + } +}); diff --git a/libs/PageForms.js b/libs/PageForms.js index d613e55c..cb3ef299 100644 --- a/libs/PageForms.js +++ b/libs/PageForms.js @@ -1,1886 +1,1906 @@ /** * PageForms.js * * Javascript utility functions for the Page Forms extension. * * @author Yaron Koren * @author Sanyam Goyal * @author Stephan Gambke * @author Jeffrey Stuckman * @author Harold Solbrig * @author Eugene Mednikov */ /*global wgPageFormsShowOnSelect, wgPageFormsFieldProperties, wgPageFormsCargoFields, wgPageFormsDependentFields, validateAll, alert, mwTinyMCEInit, pf, Sortable*/ // Activate autocomplete functionality for the specified field ( function ( $, mw ) { /* extending jQuery functions for custom highlighting */ $.ui.autocomplete.prototype._renderItem = function( ul, item) { var delim = this.element[0].delimiter; var term; if ( delim === null ) { term = this.term; } else { term = this.term.split( delim ).pop(); } var re = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"); // HTML-encode the value's label. var itemLabel = $('
    ').text(item.label).html(); var loc = itemLabel.search(re); var t; if (loc >= 0) { t = itemLabel.substr(0, loc) + '' + itemLabel.substr(loc, term.length) + '' + itemLabel.substr(loc + term.length); } else { t = itemLabel; } return $( "
  • " ) .data( "item.autocomplete", item ) .append( " " + t + "" ) .appendTo( ul ); }; $.fn.attachAutocomplete = function() { try { return this.each(function() { // Get all the necessary values from the input's "autocompletesettings" // attribute. This should probably be done as three separate attributes, // instead. var field_string = $(this).attr("autocompletesettings"); if ( typeof field_string === 'undefined' ) { return; } var field_values = field_string.split(','); var delimiter = null; var data_source = field_values[0]; if (field_values[1] === 'list') { delimiter = ","; if (field_values[2] !== null && field_values[2] !== '' && field_values[2] !== undefined) { delimiter = field_values[2]; } } // Modify the delimiter. If it's "\n", change it to an actual // newline - otherwise, add a space to the end. // This doesn't cover the case of a delimiter that's a newline // plus something else, like ".\n" or "\n\n", but as far as we // know no one has yet needed that. if ( delimiter !== null && delimiter !== '' && delimiter !== undefined ) { if ( delimiter === "\\n" ) { delimiter = "\n"; } else { delimiter += " "; } } // Store this value within the object, so that it can be used // during highlighting of the search term as well. this.delimiter = delimiter; /* extending jQuery functions */ $.extend( $.ui.autocomplete, { filter: function(array, term) { var wgPageFormsAutocompleteOnAllChars = mw.config.get( 'wgPageFormsAutocompleteOnAllChars' ); var matcher; if ( wgPageFormsAutocompleteOnAllChars ) { matcher = new RegExp($.ui.autocomplete.escapeRegex(term), "i" ); } else { matcher = new RegExp("(^|\\s)" + $.ui.autocomplete.escapeRegex(term), "i" ); } // This may be an associative array instead of a // regular one - grep() requires a regular one. // (Is this "if" check necessary, or useful?) if ( typeof array === 'object' ) { // Unfortunately, Object.values() is // not supported on all browsers. array = Object.keys(array).map(function(key) { return array[key]; }); } return $.grep( array, function(value) { return matcher.test( value.label || value.value || value ); }); } } ); var values = $(this).data('autocompletevalues'); if ( !values ) { var wgPageFormsAutocompleteValues = mw.config.get( 'wgPageFormsAutocompleteValues' ); values = wgPageFormsAutocompleteValues[field_string]; } var split = function (val) { return val.split(delimiter); }; var extractLast = function (term) { return split(term).pop(); }; if (values !== null && values !== undefined) { // Local autocompletion if (delimiter !== null && delimiter !== undefined) { // Autocomplete for multiple values var thisInput = $(this); $(this).autocomplete({ minLength: 0, source: function(request, response) { // We need to re-get the set of values, since // the "values" variable gets overwritten. values = thisInput.data( 'autocompletevalues' ); if ( !values ) { values = wgPageFormsAutocompleteValues[field_string]; } response($.ui.autocomplete.filter(values, extractLast(request.term))); }, focus: function() { // prevent value inserted on focus return false; }, select: function(event, ui) { var terms = split( this.value ); // remove the current input terms.pop(); // add the selected item terms.push( ui.item.value ); // add placeholder to get the comma-and-space at the end terms.push(""); this.value = terms.join(delimiter); return false; } }); } else { // Autocomplete for a single value $(this).autocomplete({ // Unfortunately, Object.values() is // not supported on all browsers. source: ( typeof values === 'object' ) ? Object.keys(values).map(function(key) { return values[key]; }) : values }); } } else { // Remote autocompletion. var myServer = mw.util.wikiScript( 'api' ); var autocomplete_type = $(this).attr("autocompletedatatype"); if ( autocomplete_type === 'cargo field' ) { var table_and_field = data_source.split('|'); myServer += "?action=pfautocomplete&format=json&cargo_table=" + table_and_field[0] + "&cargo_field=" + table_and_field[1]; } else { myServer += "?action=pfautocomplete&format=json&" + autocomplete_type + "=" + data_source; } if (delimiter !== null && delimiter !== undefined) { $(this).autocomplete({ source: function(request, response) { $.getJSON(myServer, { substr: extractLast(request.term) }, function( data ) { response($.map(data.pfautocomplete, function(item) { return { value: item.title }; })); }); }, search: function() { // custom minLength var term = extractLast(this.value); if (term.length < 1) { return false; } }, focus: function() { // prevent value inserted on focus return false; }, select: function(event, ui) { var terms = split( this.value ); // remove the current input terms.pop(); // add the selected item terms.push( ui.item.value ); // add placeholder to get the comma-and-space at the end terms.push(""); this.value = terms.join(delimiter); return false; } } ); } else { $(this).autocomplete({ minLength: 1, source: function(request, response) { $.ajax({ url: myServer, dataType: "json", data: { substr:request.term }, success: function( data ) { response($.map(data.pfautocomplete, function(item) { return { value: item.title }; })); } }); }, open: function() { $(this).removeClass("ui-corner-all").addClass("ui-corner-top"); }, close: function() { $(this).removeClass("ui-corner-top").addClass("ui-corner-all"); } } ); } } }); } catch ( error ) { // Autocompletion (and specifically, the call to // this.menu.element in line 195 of jquery.ui.autocomplete.js) // for some reason sometimes fails when doing a preview of the // form definition. It's not that importatnt, so, in lieu of // showing it to the user (or debugging it), we'll just catch // the error and log it in the console. window.console.log("Error setting autocompletion: " + error); } }; /* * Functions to register/unregister methods for the initialization and * validation of inputs. */ // Initialize data object to hold initialization and validation data function setupPF() { $("#pfForm").data("PageForms",{ initFunctions : [], validationFunctions : [] }); } // Register a validation method // // More than one method may be registered for one input by subsequent calls to // PageForms_registerInputValidation. // // Validation functions and their data are stored in a numbered array // // @param valfunction The validation functions. Must take a string (the input's id) and an object as parameters // @param param The parameter object given to the validation function $.fn.PageForms_registerInputValidation = function(valfunction, param) { if ( ! this.attr("id") ) { return this; } if ( ! $("#pfForm").data("PageForms") ) { setupPF(); } $("#pfForm").data("PageForms").validationFunctions.push({ input : this.attr("id"), valfunction : valfunction, parameters : param }); return this; }; // Register an initialization method // // More than one method may be registered for one input by subsequent calls to // PageForms_registerInputInit. This method also executes the initFunction // if the element referenced by /this/ is not part of a multipleTemplateStarter. // // Initialization functions and their data are stored in a associative array // // @param initFunction The initialization function. Must take a string (the input's id) and an object as parameters // @param param The parameter object given to the initialization function // @param noexecute If set, the initialization method will not be executed here $.fn.PageForms_registerInputInit = function( initFunction, param, noexecute ) { // return if element has no id if ( ! this.attr("id") ) { return this; } // setup data structure if necessary if ( ! $("#pfForm").data("PageForms") ) { setupPF(); } // if no initialization function for this input was registered yet, // create entry if ( ! $("#pfForm").data("PageForms").initFunctions[this.attr("id")] ) { $("#pfForm").data("PageForms").initFunctions[this.attr("id")] = []; } // record initialization function $("#pfForm").data("PageForms").initFunctions[this.attr("id")].push({ initFunction : initFunction, parameters : param }); // execute initialization if input is not part of multipleTemplateStarter // and if not forbidden if ( this.closest(".multipleTemplateStarter").length === 0 && !noexecute) { var input = this; // ensure initFunction is only executed after doc structure is complete $(function() { if ( initFunction !== undefined ) { initFunction ( input.attr("id"), param ); } }); } return this; }; // Unregister all validation methods for the element referenced by /this/ $.fn.PageForms_unregisterInputValidation = function() { var pfdata = $("#pfForm").data("PageForms"); if ( this.attr("id") && pfdata ) { // delete every validation method for this input for ( var i = 0; i < pfdata.validationFunctions.length; i++ ) { if ( typeof pfdata.validationFunctions[i] !== 'undefined' && pfdata.validationFunctions[i].input === this.attr("id") ) { delete pfdata.validationFunctions[i]; } } } return this; }; // Unregister all initialization methods for the element referenced by /this/ $.fn.PageForms_unregisterInputInit = function() { if ( this.attr("id") && $("#pfForm").data("PageForms") ) { delete $("#pfForm").data("PageForms").initFunctions[this.attr("id")]; } return this; }; /* * Functions for handling 'show on select' */ // Display a div that would otherwise be hidden by "show on select". function showDiv( div_id, instanceWrapperDiv, initPage ) { var speed = initPage ? 0 : 'fast'; var elem; if ( instanceWrapperDiv !== null ) { elem = $('[data-origID="' + div_id + '"]', instanceWrapperDiv); } else { elem = $('#' + div_id); } elem .addClass('shownByPF') .find(".hiddenByPF") .removeClass('hiddenByPF') .addClass('shownByPF') .find(".disabledByPF") .prop('disabled', false) .removeClass('disabledByPF'); elem.each( function() { if ( $(this).css('display') === 'none' ) { $(this).slideDown(speed, function() { $(this).fadeTo(speed,1); }); } }); // Now re-show any form elements that are meant to be shown due // to the current value of form inputs in this div that are now // being uncovered. var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ); elem.find(".pfShowIfSelected, .pfShowIfChecked").each( function() { var uncoveredInput = $(this); var uncoveredInputID = null; if ( instanceWrapperDiv === null ) { uncoveredInputID = uncoveredInput.attr("id"); } else { uncoveredInputID = uncoveredInput.attr("data-origID"); } var showOnSelectVals = wgPageFormsShowOnSelect[uncoveredInputID]; if ( showOnSelectVals !== undefined ) { var inputVal = uncoveredInput.val(); for ( var i = 0; i < showOnSelectVals.length; i++ ) { var options = showOnSelectVals[i][0]; var div_id2 = showOnSelectVals[i][1]; if ( uncoveredInput.hasClass( 'pfShowIfSelected' ) ) { showDivIfSelected( options, div_id2, inputVal, instanceWrapperDiv, initPage ); } else { uncoveredInput.showDivIfChecked( options, div_id2, instanceWrapperDiv, initPage ); } } } }); } // Hide a div due to "show on select". The CSS class is there so that PF can // ignore the div's contents when the form is submitted. function hideDiv( div_id, instanceWrapperDiv, initPage ) { var speed = initPage ? 0 : 'fast'; var elem; // IDs can't contain spaces, and jQuery won't work with such IDs - if // this one has a space, display an alert. if ( div_id.indexOf( ' ' ) > -1 ) { // TODO - this should probably be a language value, instead of // hardcoded in English. alert( "Warning: this form has \"show on select\" pointing to an invalid element ID (\"" + div_id + "\") - IDs in HTML cannot contain spaces." ); } if ( instanceWrapperDiv !== null ) { elem = instanceWrapperDiv.find('[data-origID=' + div_id + ']'); } else { elem = $('#' + div_id); } // If we're just setting up the page, and this element has already // been marked to be shown by some other input, don't hide it. if ( initPage && elem.hasClass('shownByPF') ) { return; } elem.find("span, div").addClass('hiddenByPF'); elem.each( function() { if ( $(this).css('display') !== 'none' ) { // if 'display' is not 'hidden', but the element is hidden otherwise // (e.g. by having height = 0), just hide it, else animate the hiding if ( $(this).is(':hidden') ) { $(this).hide(); } else { $(this).fadeTo(speed, 0, function() { $(this).slideUp(speed); }); } } }); // Also, recursively hide further elements that are only shown because // inputs within this now-hidden div were checked/selected. var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ); elem.find(".pfShowIfSelected, .pfShowIfChecked").each( function() { var showOnSelectVals; if ( instanceWrapperDiv === null ) { showOnSelectVals = wgPageFormsShowOnSelect[$(this).attr("id")]; } else { showOnSelectVals = wgPageFormsShowOnSelect[$(this).attr("data-origID")]; } if ( showOnSelectVals !== undefined ) { for ( var i = 0; i < showOnSelectVals.length; i++ ) { //var options = showOnSelectVals[i][0]; var div_id2 = showOnSelectVals[i][1]; hideDiv( div_id2, instanceWrapperDiv, initPage ); } } }); } // Show this div if the current value is any of the relevant options - // otherwise, hide it. function showDivIfSelected(options, div_id, inputVal, instanceWrapperDiv, initPage) { for ( var i = 0; i < options.length; i++ ) { // If it's a listbox and the user has selected more than one // value, it'll be an array - handle either case. if (($.isArray(inputVal) && $.inArray(options[i], inputVal) >= 0) || (!$.isArray(inputVal) && (inputVal === options[i]))) { showDiv( div_id, instanceWrapperDiv, initPage ); return; } } hideDiv( div_id, instanceWrapperDiv, initPage ); } // Used for handling 'show on select' for the 'dropdown' and 'listbox' inputs. $.fn.showIfSelected = function(partOfMultiple, initPage) { var inputVal = this.val(), wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ), showOnSelectVals, instanceWrapperDiv; if ( partOfMultiple ) { showOnSelectVals = wgPageFormsShowOnSelect[this.attr("data-origID")]; instanceWrapperDiv = this.closest('.multipleTemplateInstance'); } else { showOnSelectVals = wgPageFormsShowOnSelect[this.attr("id")]; instanceWrapperDiv = null; } if ( showOnSelectVals !== undefined ) { for ( var i = 0; i < showOnSelectVals.length; i++ ) { var options = showOnSelectVals[i][0]; var div_id = showOnSelectVals[i][1]; showDivIfSelected( options, div_id, inputVal, instanceWrapperDiv, initPage ); } } return this; }; // Show this div if any of the relevant selections are checked - // otherwise, hide it. $.fn.showDivIfChecked = function(options, div_id, instanceWrapperDiv, initPage ) { for ( var i = 0; i < options.length; i++ ) { if ($(this).find('[value="' + options[i] + '"]').is(":checked")) { showDiv( div_id, instanceWrapperDiv, initPage ); return this; } } hideDiv( div_id, instanceWrapperDiv, initPage ); return this; }; // Used for handling 'show on select' for the 'checkboxes' and 'radiobutton' // inputs. $.fn.showIfChecked = function(partOfMultiple, initPage) { var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ), showOnSelectVals, instanceWrapperDiv, i; if ( partOfMultiple ) { showOnSelectVals = wgPageFormsShowOnSelect[this.attr("data-origID")]; instanceWrapperDiv = this.closest('.multipleTemplateInstance'); } else { showOnSelectVals = wgPageFormsShowOnSelect[this.attr("id")]; instanceWrapperDiv = null; } if ( showOnSelectVals !== undefined ) { for ( i = 0; i < showOnSelectVals.length; i++ ) { var options = showOnSelectVals[i][0]; var div_id = showOnSelectVals[i][1]; this.showDivIfChecked( options, div_id, instanceWrapperDiv, initPage ); } } return this; }; // Used for handling 'show on select' for the 'checkbox' input. $.fn.showIfCheckedCheckbox = function( partOfMultiple, initPage ) { var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ), divIDs, instanceWrapperDiv, i; if (partOfMultiple) { divIDs = wgPageFormsShowOnSelect[this.attr("data-origID")]; instanceWrapperDiv = this.closest(".multipleTemplateInstance"); } else { divIDs = wgPageFormsShowOnSelect[this.attr("id")]; instanceWrapperDiv = null; } for ( i = 0; i < divIDs.length; i++ ) { var divID = divIDs[i]; if ($(this).is(":checked")) { showDiv( divID, instanceWrapperDiv, initPage ); } else { hideDiv( divID, instanceWrapperDiv, initPage ); } } return this; }; /* * Validation functions */ // Set the error message for an input. $.fn.setErrorMessage = function(msg, val) { var container = this.find('.pfErrorMessages'); container.html($('
    ').addClass( 'errorMessage' ).text( mw.msg( msg, val ) )); }; // Append an error message to the end of an input. $.fn.addErrorMessage = function(msg, val) { this.find('input').addClass('inputError'); this.find('select2-container').addClass('inputError'); this.append($('
    ').addClass( 'errorMessage' ).text( mw.msg( msg, val ) )); // If this is part of a minimized multiple-template instance, add a // red border around the instance rectangle to make it easier to find. this.parents( '.multipleTemplateInstance.minimized' ).css( 'border', '1px solid red' ); }; $.fn.isAtMaxInstances = function() { var numInstances = this.find("div.multipleTemplateInstance").length; var maximumInstances = this.attr("maximumInstances"); if ( numInstances >= maximumInstances ) { this.parent().setErrorMessage( 'pf_too_many_instances_error', maximumInstances ); return true; } return false; }; $.fn.validateNumInstances = function() { var minimumInstances = this.attr("minimumInstances"); var maximumInstances = this.attr("maximumInstances"); var numInstances = this.find("div.multipleTemplateInstance").length; if ( numInstances < minimumInstances ) { this.parent().addErrorMessage( 'pf_too_few_instances_error', minimumInstances ); return false; } else if ( numInstances > maximumInstances ) { this.parent().addErrorMessage( 'pf_too_many_instances_error', maximumInstances ); return false; } else { return true; } }; $.fn.validateMandatoryField = function() { var fieldVal = this.find(".mandatoryField").val(); var isEmpty; if (fieldVal === null) { isEmpty = true; } else if ($.isArray(fieldVal)) { isEmpty = (fieldVal.length === 0); } else { isEmpty = (fieldVal.replace(/\s+/, '') === ''); } if (isEmpty) { this.addErrorMessage( 'pf_blank_error' ); return false; } else { return true; } }; $.fn.validateUniqueField = function() { var UNDEFINED = "undefined"; var field = this.find(".uniqueField"); var fieldVal = field.val(); if (typeof fieldVal === UNDEFINED || fieldVal.replace(/\s+/, '') === '') { return true; } var fieldOrigVal = field.prop("defaultValue"); if (fieldVal === fieldOrigVal) { return true; } var categoryFieldName = field.prop("id") + "_unique_for_category"; var categoryField = $("[name=" + categoryFieldName + "]"); var category = categoryField.val(); var namespaceFieldName = field.prop("id") + "_unique_for_namespace"; var namespaceField = $("[name=" + namespaceFieldName + "]"); var namespace = namespaceField.val(); var url = mw.config.get( 'wgScriptPath' ) + "/api.php?format=json&action="; var query, isNotUnique; // SMW var propertyFieldName = field.prop("id") + "_unique_property", propertyField = $("[name=" + propertyFieldName + "]"), property = propertyField.val(); if (typeof property !== UNDEFINED && property.replace(/\s+/, '') !== '') { query = "[[" + property + "::" + fieldVal + "]]"; if (typeof category !== UNDEFINED && category.replace(/\s+/, '') !== '') { query += "[[Category:" + category + "]]"; } if (typeof namespace !== UNDEFINED) { if (namespace.replace(/\s+/, '') !== '') { query += "[[:" + namespace + ":+]]"; } else { query += "[[:+]]"; } } var conceptFieldName = field.prop("id") + "_unique_for_concept"; var conceptField = $("[name=" + conceptFieldName + "]"); var concept = conceptField.val(); if (typeof concept !== UNDEFINED && concept.replace(/\s+/, '') !== '') { query += "[[Concept:" + concept + "]]"; } query += "|limit=1"; query = encodeURIComponent(query); url += "ask&query=" + query; isNotUnique = true; $.ajax({ url: url, dataType: 'json', async: false, success: function(data) { if (data.query.meta.count === 0) { isNotUnique = false; } } }); if (isNotUnique) { this.addErrorMessage( 'pf_not_unique_error' ); return false; } else { return true; } } // Cargo var cargoTableFieldName = field.prop("id") + "_unique_cargo_table"; var cargoTableField = $("[name=" + cargoTableFieldName + "]"); var cargoTable = cargoTableField.val(); var cargoFieldFieldName = field.prop("id") + "_unique_cargo_field"; var cargoFieldField = $("[name=" + cargoFieldFieldName + "]"); var cargoField = cargoFieldField.val(); if (typeof cargoTable !== UNDEFINED && cargoTable.replace(/\s+/, '') !== '' && typeof cargoField !== UNDEFINED && cargoField.replace(/\s+/, '') !== '') { query = "&where=" + cargoField + "+=+'" + fieldVal + "'"; if (typeof category !== UNDEFINED && category.replace(/\s+/, '') !== '') { category = category.replace(/\s/, '_'); query += "+AND+cl_to=" + category + "+AND+cl_from=_pageID"; cargoTable += ",categorylinks"; } if (typeof namespace !== UNDEFINED) { query += "+AND+_pageNamespace="; if (namespace.replace(/\s+/, '') !== '') { var ns = mw.config.get('wgNamespaceIds')[namespace.toLowerCase()]; if (typeof ns !== UNDEFINED) { query += ns; } } else { query += "0"; } } query += "&limit=1"; url += "cargoquery&tables=" + cargoTable + "&fields=" + cargoField + query; isNotUnique = true; $.ajax({ url: url, dataType: 'json', async: false, success: function(data) { if (data.cargoquery.length === 0) { isNotUnique = false; } } }); if (isNotUnique) { this.addErrorMessage( 'pf_not_unique_error' ); return false; } else { return true; } } return true; }; $.fn.validateMandatoryComboBox = function() { var combobox = this.find('.mandatoryField'); if (combobox.val() === null) { this.addErrorMessage( 'pf_blank_error' ); return false; } else { return true; } }; $.fn.validateMandatoryDateField = function() { if (this.find(".dayInput").val() === '' || this.find(".monthInput").val() === '' || this.find(".yearInput").val() === '') { this.addErrorMessage( 'pf_blank_error' ); return false; } else { return true; } }; $.fn.validateMandatoryRadioButton = function() { - var checkedValue = this.find("input:checked").val(); - if ( !checkedValue || checkedValue == '' ) { - this.addErrorMessage( 'pf_blank_error' ); - return false; + if ( $(this).hasClass( 'pfTreeInput' ) ) { + var input_value = $(this).siblings( 'input' ).attr( 'value' ); + if ( input_value === undefined || input_value === '' ) { + this.addErrorMessage( 'pf_blank_error' ); + return false; + } else { + return true; + } } else { - return true; + var checkedValue = this.find("input:checked").val(); + if (!checkedValue || checkedValue == '') { + this.addErrorMessage('pf_blank_error'); + return false; + } else { + return true; + } } }; $.fn.validateMandatoryCheckboxes = function() { // Get the number of checked checkboxes within this span - must // be at least one. - var numChecked = this.find("input:checked").size(); - if (numChecked === 0) { - this.addErrorMessage( 'pf_blank_error' ); - return false; + if ( $( this ).hasClass( 'pfTreeInput' ) ) { + var input_value = $( this ).siblings( 'input' ).attr( 'value' ); + if ( input_value === undefined || input_value === '' ) { + this.addErrorMessage( 'pf_blank_error' ); + return false; + } else { + return true; + } } else { - return true; + var numChecked = this.find("input:checked").size(); + if (numChecked === 0) { + this.addErrorMessage('pf_blank_error'); + return false; + } else { + return true; + } } }; /* * Type-based validation */ $.fn.validateURLField = function() { var fieldVal = this.find("input").val(); var url_protocol = mw.config.get( 'wgUrlProtocols' ); //removing backslash before colon from url_protocol string url_protocol = url_protocol.replace( /\\:/, ':' ); //removing '//' from wgUrlProtocols as this causes to match any protocol in regexp url_protocol = url_protocol.replace( /\|\\\/\\\//, '' ); var url_regexp = new RegExp( '(' + url_protocol + ')' + '(\\w+:{0,1}\\w*@)?(\\S+)(:[0-9]+)?(\/|\/([\\w#!:.?+=&%@!\\-\/]))?' ); if (fieldVal === "" || url_regexp.test(fieldVal)) { return true; } else { this.addErrorMessage( 'pf_bad_url_error' ); return false; } }; $.fn.validateEmailField = function() { var fieldVal = this.find("input").val(); // code borrowed from http://javascript.internet.com/forms/email-validation---basic.html var email_regexp = /^\s*\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,6})+\s*$/; if (fieldVal === '' || email_regexp.test(fieldVal)) { return true; } else { this.addErrorMessage( 'pf_bad_email_error' ); return false; } }; $.fn.validateNumberField = function() { var fieldVal = this.find("input").val(); // Handle "E notation"/scientific notation ("1.2e-3") in addition // to regular numbers if (fieldVal === '' || fieldVal.match(/^\s*[\-+]?((\d+[\.,]?\d*)|(\d*[\.,]?\d+))([eE]?[\-\+]?\d+)?\s*$/)) { return true; } else { this.addErrorMessage( 'pf_bad_number_error' ); return false; } }; $.fn.validateIntegerField = function() { var fieldVal = this.find("input").val(); if ( fieldVal === '' || fieldVal == parseInt( fieldVal, 10 ) ) { return true; } else { this.addErrorMessage( 'pf_bad_integer_error' ); return false; } }; $.fn.validateDateField = function() { // validate only if day and year fields are both filled in var dayVal = this.find(".dayInput").val(); var yearVal = this.find(".yearInput").val(); if (dayVal === '' || yearVal === '') { return true; } else if (dayVal.match(/^\d+$/) && dayVal <= 31) { // no year validation, since it can also include // 'BC' and possibly other non-number strings return true; } else { this.addErrorMessage( 'pf_bad_date_error' ); return false; } }; // Standalone pipes are not allowed, because they mess up the template // parsing; unless they're part of a call to a template or a parser function. $.fn.checkForPipes = function() { var fieldVal = this.find("input, textarea").val(); // We need to check for a few different things because this is // called for a variety of different input types. if ( fieldVal === undefined || fieldVal === '' ) { fieldVal = this.text(); } if ( fieldVal === undefined || fieldVal === '' ) { return true; } if ( fieldVal.indexOf( '|' ) < 0 ) { return true; } // Also allow pipes within special tags, like
     or .
     	// Code copied, more or less, from PFTemplateInForm::escapeNonTemplatePipes().
     	var startAndEndTags = [
     		[ '' ],
     		[ '' ],
     		[ '' ],
     		[ '' ]
     	];
     
     	for ( var i in startAndEndTags ) {
     		var startTag = startAndEndTags[i][0];
     		var endTag = startAndEndTags[i][1];
     		var pattern = RegExp( "(" + startTag + "[^]*?)\\|([^]*?" + endTag + ")", 'i' );
     		var matches;
     		while ( ( matches = fieldVal.match( pattern ) ) !== null ) {
     			// Special handling, to avoid escaping pipes
     			// within a string that looks like:
     			// startTag ... endTag | startTag ... endTag
     			if ( matches[1].includes( endTag ) &&
     				matches[2].includes( startTag ) ) {
     				fieldVal = fieldVal.replace( pattern, "$1" + "\2" + "$2");
     			} else {
     				fieldVal = fieldVal.replace( pattern, "$1" + "\1" + "$2" );
     			}
     		}
     	}
     	fieldVal = fieldVal.replace( "\2", '|' );
     
     	// Now check for pipes outside of brackets.
     	var nextPipe,
     		nextDoubleBracketsStart,
     		nextDoubleBracketsEnd;
     
     	// There's at least one pipe - here's where the real work begins.
     	// We do a mini-parsing of the string to try to make sure that every
     	// pipe is within either double square brackets (links) or double
     	// curly brackets (parser functions, template calls).
     	// For simplicity's sake, turn all curly brackets into square brackets,
     	// so we only have to check for one thing.
     	// This will incorrectly allow bad text like "[[a|b}}", but hopefully
     	// that's not a major problem.
     	fieldVal = fieldVal.replace( /{{/g, '[[' );
     	fieldVal = fieldVal.replace( /}}/g, ']]' );
     	var curIndex = 0;
     	var numUnclosedBrackets = 0;
     	while ( true ) {
     		nextDoubleBracketsStart = fieldVal.indexOf( '[[', curIndex );
     
     		if ( numUnclosedBrackets === 0 ) {
     			nextPipe = fieldVal.indexOf( '|', curIndex );
     			if ( nextPipe < 0 ) {
     				return true;
     			}
     			if ( nextDoubleBracketsStart < 0 || nextPipe < nextDoubleBracketsStart ) {
     				// There's a pipe where it shouldn't be.
     				this.addErrorMessage( 'pf_pipe_error' );
     				return false;
     			}
     		} else {
     			if ( nextDoubleBracketsEnd < 0 ) {
     				// Something is malformed - might as well throw
     				// an error.
     				this.addErrorMessage( 'pf_pipe_error' );
     				return false;
     			}
     		}
     
     		nextDoubleBracketsEnd = fieldVal.indexOf( ']]', curIndex );
     
     		if ( nextDoubleBracketsStart >= 0 && nextDoubleBracketsStart < nextDoubleBracketsEnd ) {
     			numUnclosedBrackets++;
     			curIndex = nextDoubleBracketsStart + 2;
     		} else {
     			numUnclosedBrackets--;
     			curIndex = nextDoubleBracketsEnd + 2;
     		}
     	}
     
     	// We'll never get here, but let's have this line anyway.
     	return true;
     };
     
     window.validateAll = function () {
     
     	// Hook that fires on form submission, before the validation.
     	mw.hook('pf.formValidationBefore').fire();
     
     	var num_errors = 0;
     
     	// Remove all old error messages.
     	$(".errorMessage").remove();
     
     	// Make sure all inputs are ignored in the "starter" instance
     	// of any multiple-instance template.
     	$(".multipleTemplateStarter").find("span, div").addClass("hiddenByPF");
     
     	$(".multipleTemplateList").each( function() {
     		if (! $(this).validateNumInstances() ) {
     			num_errors += 1;
     		}
     	});
     
     	$("span.inputSpan.mandatoryFieldSpan").not(".hiddenByPF").each( function() {
     		if (! $(this).validateMandatoryField() ) {
     			num_errors += 1;
     		}
     	});
     	$("div.ui-widget.mandatory").not(".hiddenByPF").each( function() {
     		if (! $(this).validateMandatoryComboBox() ) {
     			num_errors += 1;
     		}
     	});
     	$("span.dateInput.mandatoryFieldSpan").not(".hiddenByPF").each( function() {
     		if (! $(this).validateMandatoryDateField() ) {
     			num_errors += 1;
     		}
     	});
     	$("span.radioButtonSpan.mandatoryFieldSpan").not(".hiddenByPF").each( function() {
     		if (! $(this).validateMandatoryRadioButton() ) {
     			num_errors += 1;
     		}
     	});
     	$("span.checkboxesSpan.mandatoryFieldSpan").not(".hiddenByPF").each( function() {
     		if (! $(this).validateMandatoryCheckboxes() ) {
     			num_errors += 1;
     		}
     	});
     	$("div.pfTreeInput.mandatory").not(".hiddenByPF").each( function() {
     		// @HACK - handle both the options for tree, checkboxes and
     		// radiobuttons, at the same time, regardless of which one is
     		// being used. This seems to work fine, though.
     		if (! $(this).validateMandatoryCheckboxes() ) {
     			num_errors += 1;
     		}
     		if (! $(this).validateMandatoryRadioButton() ) {
     			num_errors += 1;
     		}
     	});
     	$("span.inputSpan.uniqueFieldSpan").not(".hiddenByPF").each( function() {
     		if (! $(this).validateUniqueField() ) {
     			num_errors += 1;
     		}
     	});
     	$("span.inputSpan, div.pfComboBox").not(".hiddenByPF, .freeText, .pageSection").each( function() {
     		if (! $(this).checkForPipes() ) {
     			num_errors += 1;
     		}
     	});
     	$("span.URLInput").not(".hiddenByPF").each( function() {
     		if (! $(this).validateURLField() ) {
     			num_errors += 1;
     		}
     	});
     	$("span.emailInput").not(".hiddenByPF").each( function() {
     		if (! $(this).validateEmailField() ) {
     			num_errors += 1;
     		}
     	});
     	$("span.numberInput").not(".hiddenByPF").each( function() {
     		if (! $(this).validateNumberField() ) {
     			num_errors += 1;
     		}
     	});
     	$("span.integerInput").not(".hiddenByPF").each( function() {
     		if (! $(this).validateIntegerField() ) {
     			num_errors += 1;
     		}
     	});
     	$("span.dateInput").not(".hiddenByPF").each( function() {
     		if (! $(this).validateDateField() ) {
     			num_errors += 1;
     		}
     	});
     	$("input.modifiedInput").not(".hiddenByPF").each( function() {
     		// No separate function needed.
     		$(this).parent().addErrorMessage( 'pf_modified_input_error' );
     		num_errors += 1;
     	});
     
     	// call registered validation functions
     	var pfdata = $("#pfForm").data('PageForms');
     
     	if ( pfdata && pfdata.validationFunctions.length > 0 ) { // found data object?
     
     		// for every registered input
     		for ( var i = 0; i < pfdata.validationFunctions.length; i++ ) {
     
     			// if input is not part of multipleTemplateStarter
     			if ( typeof pfdata.validationFunctions[i] !== 'undefined' &&
     				$("#" + pfdata.validationFunctions[i].input).closest(".multipleTemplateStarter").length === 0 &&
     				$("#" + pfdata.validationFunctions[i].input).closest(".hiddenByPF").length === 0 ) {
     
     				if (! pfdata.validationFunctions[i].valfunction(
     						pfdata.validationFunctions[i].input,
     						pfdata.validationFunctions[i].parameters)
     					) {
     					num_errors += 1;
     				}
     			}
     		}
     	}
     
     	if (num_errors > 0) {
     		// add error header, if it's not there already
     		if ($("#form_error_header").size() === 0) {
     			$("#contentSub").append('
     ' + mw.message( 'pf_formerrors_header' ).escaped() + '

    '); } scroll(0, 0); } else { // Disable inputs hidden due to either "show on select" or // because they're part of the "starter" div for // multiple-instance templates, so that they aren't // submitted by the form. $('.hiddenByPF').find("input, select, textarea").not(':disabled') .prop('disabled', true) .addClass('disabledByPF'); //remove error box if it exists because there are no errors in the form now $("#contentSub").find(".errorbox").remove(); } // Hook that fires on form submission, after the validation. mw.hook('pf.formValidationAfter').fire(); return (num_errors === 0); }; /** * Minimize all instances if the total height of all the instances * is over 800 pixels - to allow for easier navigation and sorting. */ $.fn.possiblyMinimizeAllOpenInstances = function() { if ( ! this.hasClass( 'minimizeAll' ) ) { return; } var displayedFieldsWhenMinimized = this.attr('data-displayed-fields-when-minimized'); var allDisplayedFields = null; if ( displayedFieldsWhenMinimized ) { allDisplayedFields = displayedFieldsWhenMinimized.split(',').map(function(item) { return item.trim().toLowerCase(); }); } this.find('.multipleTemplateInstance').not('.minimized').each( function() { var instance = $(this); instance.addClass('minimized'); var valuesStr = ''; instance.find( "input[type != 'hidden'][type != 'button'], select, textarea, div.ve-ce-surface" ).each( function() { // If the set of fields to be displayed was specified in // the form definition, check against that list. if ( allDisplayedFields !== null ) { var fieldFullName = $(this).attr('name'); if ( !fieldFullName ) { return; } var matches = fieldFullName.match(/.*\[.*\]\[(.*)\]/); var fieldRealName = matches[1].toLowerCase(); if ( !allDisplayedFields.includes( fieldRealName ) ) { return; } } var curVal = $(this).val(); if ( $(this).hasClass('ve-ce-surface') ) { // Special handling for VisualEditor/VEForAll textareas. curVal = $(this).text(); } if ( typeof curVal !== 'string' || curVal === '' ) { return; } var inputType = $(this).attr('type'); if ( inputType === 'checkbox' || inputType === 'radio' ) { if ( ! $(this).is(':checked') ) { return; } } if ( curVal.length > 70 ) { curVal = curVal.substring(0, 70) + "..."; } if ( valuesStr !== '' ) { valuesStr += ' · '; } valuesStr += curVal; }); if ( valuesStr === '' ) { valuesStr = 'No data'; } instance.find('.instanceMain').fadeOut( "medium", function() { instance.find('.instanceRearranger').after('' + valuesStr + ''); }); }); }; var num_elements = 0; /** * Functions for multiple-instance templates. * * @param addAboveCurInstance */ $.fn.addInstance = function( addAboveCurInstance ) { var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ); var wgPageFormsHeightForMinimizingInstances = mw.config.get( 'wgPageFormsHeightForMinimizingInstances' ); var wrapper = this.closest(".multipleTemplateWrapper"); var multipleTemplateList = wrapper.find('.multipleTemplateList'); // If the nubmer of instances is already at the maximum allowed, // exit here. if ( multipleTemplateList.isAtMaxInstances() ) { return false; } if ( wgPageFormsHeightForMinimizingInstances >= 0 ) { if ( ! multipleTemplateList.hasClass('minimizeAll') && multipleTemplateList.height() >= wgPageFormsHeightForMinimizingInstances ) { multipleTemplateList.addClass('minimizeAll'); } if ( multipleTemplateList.hasClass('minimizeAll') ) { multipleTemplateList .addClass('currentFocus') .possiblyMinimizeAllOpenInstances(); } } // Global variable. num_elements++; // Create the new instance var new_div = wrapper .find(".multipleTemplateStarter") .clone() .removeClass('multipleTemplateStarter') .addClass('multipleTemplateInstance') .addClass('multipleTemplate') // backwards compatibility .removeAttr("id") .fadeTo(0,0) .slideDown('fast', function() { $(this).fadeTo('fast', 1); }); // Add on a new attribute, "data-origID", representing the ID of all // HTML elements that had an ID; and delete the actual ID attribute // of any divs and spans (presumably, these exist only for the // sake of "show on select"). We do the deletions because no two // elements on the page are allowed to have the same ID. new_div.find('[id!=""]').attr('data-origID', function() { return this.id; }); new_div.find('div[id!=""], span[id!=""]').removeAttr('id'); new_div.find('.hiddenByPF') .removeClass('hiddenByPF') .find('.disabledByPF') .prop('disabled', false) .removeClass('disabledByPF'); // Make internal ID unique for the relevant form elements, and replace // the [num] index in the element names with an actual unique index new_div.find("input, select, textarea").each( function() { // Add in a 'b' at the end of the name to reduce the // chance of name collision with another field if (this.name) { var old_name = this.name.replace(/\[num\]/g, ''); $(this).attr('origName', old_name); this.name = this.name.replace(/\[num\]/g, '[' + num_elements + 'b]'); } if (this.id) { var old_id = this.id; this.id = this.id.replace(/input_/g, 'input_' + num_elements + '_'); // TODO: Data in wgPageFormsShowOnSelect should probably be stored in // $("#pfForm").data('PageForms') if ( wgPageFormsShowOnSelect[ old_id ] ) { wgPageFormsShowOnSelect[ this.id ] = wgPageFormsShowOnSelect[ old_id ]; } // register initialization and validation methods for new inputs var pfdata = $("#pfForm").data('PageForms'); if ( pfdata ) { // found data object? var i; if ( pfdata.initFunctions[old_id] ) { // For every initialization method for // input with id old_id, register the // method for the new input. for ( i = 0; i < pfdata.initFunctions[old_id].length; i++ ) { $(this).PageForms_registerInputInit( pfdata.initFunctions[old_id][i].initFunction, pfdata.initFunctions[old_id][i].parameters, true //do not yet execute ); } } // For every validation method for the // input with ID old_id, register it // for the new input. for ( i = 0; i < pfdata.validationFunctions.length; i++ ) { if ( typeof pfdata.validationFunctions[i] !== 'undefined' && pfdata.validationFunctions[i].input === old_id ) { $(this).PageForms_registerInputValidation( pfdata.validationFunctions[i].valfunction, pfdata.validationFunctions[i].parameters ); } } } } } ); new_div.find('a').attr('href', function() { return this.href.replace(/input_/g, 'input_' + num_elements + '_'); }); new_div.find('span').attr('id', function() { return this.id.replace(/span_/g, 'span_' + num_elements + '_'); }); // Add the new instance. if ( addAboveCurInstance ) { new_div.insertBefore(this.closest(".multipleTemplateInstance")); } else { this.closest(".multipleTemplateWrapper") .find(".multipleTemplateList") .append(new_div); } new_div.initializeJSElements(true); // Initialize new inputs. new_div.find("input, select, textarea").each( function() { if (this.id) { var pfdata = $("#pfForm").data('PageForms'); if ( pfdata ) { // have to store data array: the id attribute // of 'this' might be changed in the init function var thatData = pfdata.initFunctions[this.id] ; if ( thatData ) { // if anything registered at all // Call every initialization method // for this input for ( var i = 0; i < thatData.length; i++ ) { var initFunction = thatData[i].initFunction; if ( initFunction === undefined ) { continue; } // If the code attempted to store // this function before it was // defined, only its name was stored. // In that case, get the function now. // @TODO - move getFunctionFromName() // so that it can be called from here, // which would be better than window[]. if ( typeof initFunction === 'string' ) { initFunction = window[initFunction]; } initFunction( this.id, thatData[i].parameters ); } } } } } ); // Hook that fires each time a new template instance is added. // The first parameter is a jQuery selection of the newly created instance div. mw.hook('pf.addTemplateInstance').fire(new_div); }; // The first argument is needed, even though it's an attribute of the element // on which this function is called, because it's the 'name' attribute for // regular inputs, and the 'origName' attribute for inputs in multiple-instance // templates. $.fn.setDependentAutocompletion = function( dependentField, baseField, baseValue ) { // Get data from either Cargo or Semantic MediaWiki. var myServer = mw.config.get( 'wgScriptPath' ) + "/api.php", wgPageFormsCargoFields = mw.config.get( 'wgPageFormsCargoFields' ), wgPageFormsFieldProperties = mw.config.get( 'wgPageFormsFieldProperties' ); myServer += "?action=pfautocomplete&format=json"; if ( wgPageFormsCargoFields.hasOwnProperty( dependentField ) ) { var cargoTableAndFieldStr = wgPageFormsCargoFields[dependentField]; var cargoTableAndField = cargoTableAndFieldStr.split('|'); var cargoTable = cargoTableAndField[0]; var cargoField = cargoTableAndField[1]; var baseCargoTableAndFieldStr = wgPageFormsCargoFields[baseField]; var baseCargoTableAndField = baseCargoTableAndFieldStr.split('|'); var baseCargoTable = baseCargoTableAndField[0]; var baseCargoField = baseCargoTableAndField[1]; myServer += "&cargo_table=" + cargoTable + "&cargo_field=" + cargoField + "&is_array=true" + "&base_cargo_table=" + baseCargoTable + "&base_cargo_field=" + baseCargoField + "&basevalue=" + baseValue; } else { var propName = wgPageFormsFieldProperties[dependentField]; var baseProp = wgPageFormsFieldProperties[baseField]; myServer += "&property=" + propName + "&baseprop=" + baseProp + "&basevalue=" + baseValue; } var dependentValues = []; var thisInput = $(this); // We use $.ajax() here instead of $.getJSON() so that the // 'async' parameter can be set. That, in turn, is set because // if the 2nd, "dependent" field is a combo box, it can have weird // behavior: clicking on the down arrow for the combo box leads to a // "blur" event for the base field, which causes the possible // values to get recalculated, but not in time for the dropdown to // change values - it still shows the old values. By setting // "async: false", we guarantee that old values won't be shown - if // the values haven't been recalculated yet, the dropdown won't // appear at all. // @TODO - handle this the right way, by having special behavior for // the dropdown - it should get delayed until the values are // calculated, then appear. $.ajax({ url: myServer, dataType: 'json', async: false, success: function(data) { var realData = data.pfautocomplete; $.each(realData, function(key, val) { dependentValues.push(val.title); }); thisInput.data('autocompletevalues', dependentValues); thisInput.attachAutocomplete(); } }); }; /** * Called on a 'base' field (e.g., for a country) - sets the autocompletion * for its 'dependent' field (e.g., for a city). * * @param partOfMultiple */ $.fn.setAutocompleteForDependentField = function( partOfMultiple ) { var curValue = $(this).val(); if ( curValue === null ) { return this; } var nameAttr = partOfMultiple ? 'origName' : 'name'; var name = $(this).attr(nameAttr); var wgPageFormsDependentFields = mw.config.get( 'wgPageFormsDependentFields' ); var dependent_on_me = []; for ( var i = 0; i < wgPageFormsDependentFields.length; i++ ) { var dependentFieldPair = wgPageFormsDependentFields[i]; if ( dependentFieldPair[0] === name ) { dependent_on_me.push(dependentFieldPair[1]); } } // @TODO - change to $.uniqueSort() once support for MW 1.28 is // removed (and jQuery >= 3 is thus guaranteed). dependent_on_me = $.unique(dependent_on_me); var self = this; $.each( dependent_on_me, function() { var element, cmbox, tokens, dependentField = this; if ( partOfMultiple ) { element = $( self ).closest( '.multipleTemplateInstance' ) .find('[origName="' + dependentField + '"]'); } else { element = $('[name="' + dependentField + '"]'); } if ( element.hasClass( 'pfComboBox' ) ) { cmbox = new pf.select2.combobox(); cmbox.refresh(element); } else if ( element.hasClass( 'pfTokens' ) ) { tokens = new pf.select2.tokens(); tokens.refresh(element); } else { element.setDependentAutocompletion(dependentField, name, curValue); } }); return this; }; /** * Initialize all the JS-using elements contained within this block - can be * called for either the entire HTML body, or for a div representing an * instance of a multiple-instance template. * * @param partOfMultiple */ $.fn.initializeJSElements = function( partOfMultiple ) { var fancyBoxSettings; this.find(".pfShowIfSelected").each( function() { // Avoid duplicate calls on any one element. if ( !partOfMultiple && $(this).parents('.multipleTemplateWrapper').length > 0 ) { return; } $(this) .showIfSelected(partOfMultiple, true) .change( function() { $(this).showIfSelected(partOfMultiple, false); }); }); this.find(".pfShowIfChecked").each( function() { // Avoid duplicate calls on any one element. if ( !partOfMultiple && $(this).parents('.multipleTemplateWrapper').length > 0 ) { return; } $(this) .showIfChecked(partOfMultiple, true) .click( function() { $(this).showIfChecked(partOfMultiple, false); }); }); this.find(".pfShowIfCheckedCheckbox").each( function() { // Avoid duplicate calls on any one element. if ( !partOfMultiple && $(this).parents('.multipleTemplateWrapper').length > 0 ) { return; } $(this) .showIfCheckedCheckbox(partOfMultiple, true) .click( function() { $(this).showIfCheckedCheckbox(partOfMultiple, false); }); }); if ( partOfMultiple ) { // Enable the new remove button this.find(".removeButton").click( function() { // Unregister initialization and validation for deleted inputs $(this).parentsUntil( '.multipleTemplateInstance' ).last().parent().find("input, select, textarea").each( function() { $(this).PageForms_unregisterInputInit(); $(this).PageForms_unregisterInputValidation(); } ); // Remove the encompassing div for this instance. $(this).closest(".multipleTemplateInstance") .fadeTo('fast', 0, function() { $(this).slideUp('fast', function() { $(this).remove(); }); }); return false; }); // ...and the new adder this.find('.addAboveButton').click( function() { $(this).addInstance( true ); return false; // needed to disable behavior }); } var combobox = new pf.select2.combobox(); this.find('.pfComboBox').not('#semantic_property_starter, .multipleTemplateStarter .pfComboBox, .select2-container').each( function() { combobox.apply($(this)); }); var tokens = new pf.select2.tokens(); this.find('.pfTokens').not('.multipleTemplateStarter .pfTokens, .select2-container').each( function() { tokens.apply($(this)); }); // We use a different version of FancyBox depending on the version // of jQuery (1 vs. 3) (which in turn depends on the version of // MediaWiki (<= 1.29 vs. >= 1.30)). if ( parseInt($().jquery) >= 3 ) { fancyBoxSettings = { toolbar : false, smallBtn : true, iframe : { preload : false, css : { width : '75%', height : '75%' } }, animationEffect : false }; } else { fancyBoxSettings = { 'width' : '75%', 'height' : '75%', 'autoScale' : false, 'transitionIn' : 'none', 'transitionOut' : 'none', 'type' : 'iframe', 'overlayColor' : '#222', 'overlayOpacity' : '0.8' }; } // Only defined if $wgPageFormsSimpleUpload == true. if ( typeof this.initializeSimpleUpload === 'function' ) { this.initializeSimpleUpload(); } if ( partOfMultiple ) { this.find('.pfFancyBox').fancybox(fancyBoxSettings); this.find('.autocompleteInput').attachAutocomplete(); this.find('.autoGrow').autoGrow(); this.find(".pfRating").applyRatingInput(); this.find(".pfTreeInput").each( function() { - $(this).applyFancytree(); + $(this).applyJSTree(); }); } else { this.find('.pfFancyBox').not('multipleTemplateWrapper .pfFancyBox').fancybox(fancyBoxSettings); this.find('.autocompleteInput').not('.multipleTemplateWrapper .autocompleteInput').attachAutocomplete(); this.find('.autoGrow').not('.multipleTemplateWrapper .autoGrow').autoGrow(); this.find(".pfRating").not(".multipleTemplateWrapper .pfRating").applyRatingInput(); this.find(".pfTreeInput").not(".multipleTemplateWrapper .pfTreeInput").each( function() { - $(this).applyFancytree(); + $(this).applyJSTree(); }); } // @TODO - this should ideally be called only for inputs that have // a dependent field - which might involve changing the storage of // "dependent fields" information from a global variable to a // per-input HTML attribute. this.find('input, select').each( function() { $(this) .setAutocompleteForDependentField( partOfMultiple ) .blur( function() { $(this).setAutocompleteForDependentField( partOfMultiple ); }); }); // The 'blur' event doesn't get triggered for radio buttons for // Chrome and Safari (the WebKit-based browsers) so use the 'change' // event in addition. // @TODO - blur() shuldn't be called at all for radio buttons. this.find('input:radio') .change( function() { $(this).setAutocompleteForDependentField( partOfMultiple ); }); var myThis = this; if ( $.fn.applyVisualEditor ) { if ( partOfMultiple ) { myThis.find(".visualeditor").applyVisualEditor(); } else { myThis.find(".visualeditor").not(".multipleTemplateWrapper .visualeditor").applyVisualEditor(); } } else { $(document).on('VEForAllLoaded', function(e) { if ( partOfMultiple ) { myThis.find(".visualeditor").applyVisualEditor(); } else { myThis.find(".visualeditor").not(".multipleTemplateWrapper .visualeditor").applyVisualEditor(); } }); } // @TODO - this should be in the TinyMCE extension, and use a hook. if ( typeof( mwTinyMCEInit ) === 'function' ) { if ( partOfMultiple ) { myThis.find(".tinymce").each( function() { mwTinyMCEInit( '#' + $(this).attr('id') ); }); } else { myThis.find(".tinymce").not(".multipleTemplateWrapper .tinymce").each( function() { mwTinyMCEInit( '#' + $(this).attr('id') ); }); } } else { $(document).on('TinyMCELoaded', function(e) { if ( partOfMultiple ) { myThis.find(".tinymce").each( function() { mwTinyMCEInit( '#' + $(this).attr('id') ); }); } else { myThis.find(".tinymce").not(".multipleTemplateWrapper .tinymce").each( function() { mwTinyMCEInit( '#' + $(this).attr('id') ); }); } }); } }; // Once the document has finished loading, set up everything! $(document).ready( function() { var i, inputID, validationFunctionData; function getFunctionFromName( functionName ) { var func = window; var namespaces = functionName.split( "." ); for ( var i = 0; i < namespaces.length; i++ ) { func = func[ namespaces[ i ] ]; } // If this gets called before the function is defined, just // store the function name instead, for later lookup. if ( func === null ) { return functionName; } return func; } // Initialize inputs created by #forminput. if ( $('.pfFormInput').length > 0 ) { $('.autocompleteInput').attachAutocomplete(); } // Exit now if a Page Forms form is not present. if ( $('#pfForm').length === 0 ) { return; } // jQuery's .ready() function is being called before the resource was actually loaded. // This is a workaround for https://phabricator.wikimedia.org/T216805. setTimeout( function(){ // "Mask" to prevent users from clicking while form is still loading. $('#loadingMask').css({'width': $(document).width(),'height': $(document).height()}); // register init functions var initFunctionData = mw.config.get( 'ext.pf.initFunctionData' ); for ( inputID in initFunctionData ) { for ( i in initFunctionData[inputID] ) { /*jshint -W069 */ $( '#' + inputID ).PageForms_registerInputInit( getFunctionFromName( initFunctionData[ inputID ][ i ][ 'name' ] ), initFunctionData[ inputID ][ i ][ 'param' ] ); /*jshint +W069 */ } } // register validation functions validationFunctionData = mw.config.get( 'ext.pf.validationFunctionData' ); for ( inputID in validationFunctionData ) { for ( i in validationFunctionData[inputID] ) { /*jshint -W069 */ $( '#' + inputID ).PageForms_registerInputValidation( getFunctionFromName( validationFunctionData[ inputID ][ i ][ 'name' ] ), validationFunctionData[ inputID ][ i ][ 'param' ] ); /*jshint +W069 */ } } $( 'body' ).initializeJSElements(false); $('.multipleTemplateInstance').initializeJSElements(true); $('.multipleTemplateAdder').click( function() { $(this).addInstance( false ); }); var wgPageFormsHeightForMinimizingInstances = mw.config.get( 'wgPageFormsHeightForMinimizingInstances' ); if ( wgPageFormsHeightForMinimizingInstances >= 0) { $('.multipleTemplateList').each( function() { if ( $(this).height() > wgPageFormsHeightForMinimizingInstances ) { $(this).addClass('minimizeAll'); $(this).possiblyMinimizeAllOpenInstances(); } }); } $('.multipleTemplateList').each( function() { var list = $(this); var sortable = Sortable.create(list[0], { handle: '.instanceRearranger', onStart: function (/**Event*/evt) { list.possiblyMinimizeAllOpenInstances(); } }); }); // If the form is submitted, validate everything! $('#pfForm').submit( function() { return validateAll(); } ); // We are all done - remove the loading spinner. $('.loadingImage').remove(); }, 0 ); mw.hook('pf.formSetupAfter').fire(); }); // If some part of the form is clicked, minimize any multiple-instance // template instances that need minimizing, and move the "focus" to the current // instance list, if one is being clicked and it's different from the // previous one. // We make only the form itself clickable, instead of the whole screen, to // try to avoid a click on a popup, like the "Upload file" window, minimizing // the current open instance. $('form#pfForm').click( function(e) { var target = $(e.target); // Ignore the "add instance" buttons - those get handling of their own. if ( target.hasClass('multipleTemplateAdder') || target.hasClass('addAboveButton') ) { return; } var instance = target.closest('.multipleTemplateInstance'); if ( instance === null ) { $('.multipleTemplateList.currentFocus') .removeClass('currentFocus') .possiblyMinimizeAllOpenInstances(); return; } var instancesList = instance.closest('.multipleTemplateList'); if ( !instancesList.hasClass('currentFocus') ) { $('.multipleTemplateList.currentFocus') .removeClass('currentFocus') .possiblyMinimizeAllOpenInstances(); if ( instancesList.hasClass('minimizeAll') ) { instancesList.addClass('currentFocus'); } } if ( instance.hasClass('minimized') ) { instancesList.possiblyMinimizeAllOpenInstances(); instance.removeClass('minimized'); instance.find('.fieldValuesDisplay').html(''); instance.find('.instanceMain').fadeIn(); instance.find('.fieldValuesDisplay').remove(); } }); $('#pf-expand-all a').click(function( event ) { event.preventDefault(); // Page Forms minimized template instances. $('.minimized').each( function() { $(this).removeClass('minimized'); $(this).find('.fieldValuesDisplay').html(''); $(this).find('.instanceMain').fadeIn(); $(this).find('.fieldValuesDisplay').remove(); }); // Standard MediaWiki "collapsible" sections. $('div.mw-collapsed a.mw-collapsible-text').click(); }); }( jQuery, mediaWiki ) ); diff --git a/libs/jquery.fancytree.js b/libs/jquery.fancytree.js deleted file mode 100644 index e2a76d3a..00000000 --- a/libs/jquery.fancytree.js +++ /dev/null @@ -1,6869 +0,0 @@ -/*! - * jquery.fancytree.js - * Tree view control with support for lazy loading and much more. - * https://github.com/mar10/fancytree/ - * - * Copyright (c) 2008-2018, Martin Wendt (http://wwWendt.de) - * Released under the MIT license - * https://github.com/mar10/fancytree/wiki/LicenseInfo - * - * @version @VERSION - * @date @DATE - */ - -/** Core Fancytree module. - */ - -// UMD wrapper for the Fancytree core module -(function(factory) { - if (typeof define === "function" && define.amd) { - // AMD. Register as an anonymous module. - define(["jquery", "./jquery.fancytree.ui-deps"], factory); - // } else if (typeof module === "object" && module.exports) { - // // Node/CommonJS - // require("./jquery.fancytree.ui-deps.js"); - // // module.exports = factory(require("jquery")); - } else { - // Browser globals - factory(jQuery); - } -})(function($) { - "use strict"; - - // prevent duplicate loading - if ($.ui && $.ui.fancytree) { - $.ui.fancytree.warn("Fancytree: ignored duplicate include"); - return; - } - - /****************************************************************************** - * Private functions and variables - */ - - var i, - attr, - FT = null, // initialized below - TEST_IMG = new RegExp(/\.|\//), // strings are considered image urls if they contain '.' or '/' - REX_HTML = /[&<>"'\/]/g, // Escape those characters - REX_TOOLTIP = /[<>"'\/]/g, // Don't escape `&` in tooltips - RECURSIVE_REQUEST_ERROR = "$recursive_request", - ENTITY_MAP = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "/": "/", - }, - IGNORE_KEYCODES = { 16: true, 17: true, 18: true }, - SPECIAL_KEYCODES = { - 8: "backspace", - 9: "tab", - 10: "return", - 13: "return", - // 16: null, 17: null, 18: null, // ignore shift, ctrl, alt - 19: "pause", - 20: "capslock", - 27: "esc", - 32: "space", - 33: "pageup", - 34: "pagedown", - 35: "end", - 36: "home", - 37: "left", - 38: "up", - 39: "right", - 40: "down", - 45: "insert", - 46: "del", - 59: ";", - 61: "=", - // 91: null, 93: null, // ignore left and right meta - 96: "0", - 97: "1", - 98: "2", - 99: "3", - 100: "4", - 101: "5", - 102: "6", - 103: "7", - 104: "8", - 105: "9", - 106: "*", - 107: "+", - 109: "-", - 110: ".", - 111: "/", - 112: "f1", - 113: "f2", - 114: "f3", - 115: "f4", - 116: "f5", - 117: "f6", - 118: "f7", - 119: "f8", - 120: "f9", - 121: "f10", - 122: "f11", - 123: "f12", - 144: "numlock", - 145: "scroll", - 173: "-", - 186: ";", - 187: "=", - 188: ",", - 189: "-", - 190: ".", - 191: "/", - 192: "`", - 219: "[", - 220: "\\", - 221: "]", - 222: "'", - }, - MODIFIERS = { - 16: "shift", - 17: "ctrl", - 18: "alt", - 91: "meta", - 93: "meta", - }, - MOUSE_BUTTONS = { 0: "", 1: "left", 2: "middle", 3: "right" }, - // Boolean attributes that can be set with equivalent class names in the LI tags - // Note: v2.23: checkbox and hideCheckbox are *not* in this list - CLASS_ATTRS = "active expanded focus folder lazy radiogroup selected unselectable unselectableIgnore".split( - " " - ), - CLASS_ATTR_MAP = {}, - // Top-level Fancytree attributes, that can be set by dict - TREE_ATTRS = "columns types".split(" "), - // TREE_ATTR_MAP = {}, - // Top-level FancytreeNode attributes, that can be set by dict - NODE_ATTRS = "checkbox expanded extraClasses folder icon iconTooltip key lazy partsel radiogroup refKey selected statusNodeType title tooltip type unselectable unselectableIgnore unselectableStatus".split( - " " - ), - NODE_ATTR_MAP = {}, - // Mapping of lowercase -> real name (because HTML5 data-... attribute only supports lowercase) - NODE_ATTR_LOWERCASE_MAP = {}, - // Attribute names that should NOT be added to node.data - NONE_NODE_DATA_MAP = { - active: true, - children: true, - data: true, - focus: true, - }; - - for (i = 0; i < CLASS_ATTRS.length; i++) { - CLASS_ATTR_MAP[CLASS_ATTRS[i]] = true; - } - for (i = 0; i < NODE_ATTRS.length; i++) { - attr = NODE_ATTRS[i]; - NODE_ATTR_MAP[attr] = true; - if (attr !== attr.toLowerCase()) { - NODE_ATTR_LOWERCASE_MAP[attr.toLowerCase()] = attr; - } - } - // for(i=0; i t; - } - } - return true; - } - - /** - * Deep-merge a list of objects (but replace array-type options). - * - * jQuery's $.extend(true, ...) method does a deep merge, that also merges Arrays. - * This variant is used to merge extension defaults with user options, and should - * merge objects, but override arrays (for example the `triggerStart: [...]` option - * of ext-edit). Also `null` values are copied over and not skipped. - * - * See issue #876 - * - * Example: - * _simpleDeepMerge({}, o1, o2); - */ - function _simpleDeepMerge() { - var options, - name, - src, - copy, - clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length; - - // Handle case when target is a string or something (possible in deep copy) - if (typeof target !== "object" && !$.isFunction(target)) { - target = {}; - } - if (i === length) { - throw "need at least two args"; - } - for (; i < length; i++) { - // Only deal with non-null/undefined values - if ((options = arguments[i]) != null) { - // Extend the base object - for (name in options) { - src = target[name]; - copy = options[name]; - // Prevent never-ending loop - if (target === copy) { - continue; - } - // Recurse if we're merging plain objects - // (NOTE: unlike $.extend, we don't merge arrays, but relace them) - if (copy && $.isPlainObject(copy)) { - clone = src && $.isPlainObject(src) ? src : {}; - // Never move original objects, clone them - target[name] = _simpleDeepMerge(clone, copy); - // Don't bring in undefined values - } else if (copy !== undefined) { - target[name] = copy; - } - } - } - } - // Return the modified object - return target; - } - - /** Return a wrapper that calls sub.methodName() and exposes - * this : tree - * this._local : tree.ext.EXTNAME - * this._super : base.methodName.call() - * this._superApply : base.methodName.apply() - */ - function _makeVirtualFunction(methodName, tree, base, extension, extName) { - // $.ui.fancytree.debug("_makeVirtualFunction", methodName, tree, base, extension, extName); - // if(rexTestSuper && !rexTestSuper.test(func)){ - // // extension.methodName() doesn't call _super(), so no wrapper required - // return func; - // } - // Use an immediate function as closure - var proxy = (function() { - var prevFunc = tree[methodName], // org. tree method or prev. proxy - baseFunc = extension[methodName], // - _local = tree.ext[extName], - _super = function() { - return prevFunc.apply(tree, arguments); - }, - _superApply = function(args) { - return prevFunc.apply(tree, args); - }; - - // Return the wrapper function - return function() { - var prevLocal = tree._local, - prevSuper = tree._super, - prevSuperApply = tree._superApply; - - try { - tree._local = _local; - tree._super = _super; - tree._superApply = _superApply; - return baseFunc.apply(tree, arguments); - } finally { - tree._local = prevLocal; - tree._super = prevSuper; - tree._superApply = prevSuperApply; - } - }; - })(); // end of Immediate Function - return proxy; - } - - /** - * Subclass `base` by creating proxy functions - */ - function _subclassObject(tree, base, extension, extName) { - // $.ui.fancytree.debug("_subclassObject", tree, base, extension, extName); - for (var attrName in extension) { - if (typeof extension[attrName] === "function") { - if (typeof tree[attrName] === "function") { - // override existing method - tree[attrName] = _makeVirtualFunction( - attrName, - tree, - base, - extension, - extName - ); - } else if (attrName.charAt(0) === "_") { - // Create private methods in tree.ext.EXTENSION namespace - tree.ext[extName][attrName] = _makeVirtualFunction( - attrName, - tree, - base, - extension, - extName - ); - } else { - $.error( - "Could not override tree." + - attrName + - ". Use prefix '_' to create tree." + - extName + - "._" + - attrName - ); - } - } else { - // Create member variables in tree.ext.EXTENSION namespace - if (attrName !== "options") { - tree.ext[extName][attrName] = extension[attrName]; - } - } - } - } - - function _getResolvedPromise(context, argArray) { - if (context === undefined) { - return $.Deferred(function() { - this.resolve(); - }).promise(); - } else { - return $.Deferred(function() { - this.resolveWith(context, argArray); - }).promise(); - } - } - - function _getRejectedPromise(context, argArray) { - if (context === undefined) { - return $.Deferred(function() { - this.reject(); - }).promise(); - } else { - return $.Deferred(function() { - this.rejectWith(context, argArray); - }).promise(); - } - } - - function _makeResolveFunc(deferred, context) { - return function() { - deferred.resolveWith(context); - }; - } - - function _getElementDataAsDict($el) { - // Evaluate 'data-NAME' attributes with special treatment for 'data-json'. - var d = $.extend({}, $el.data()), - json = d.json; - - delete d.fancytree; // added to container by widget factory (old jQuery UI) - delete d.uiFancytree; // added to container by widget factory - - if (json) { - delete d.json; - //
  • is already returned as object (http://api.jquery.com/data/#data-html5) - d = $.extend(d, json); - } - return d; - } - - function _escapeTooltip(s) { - return ("" + s).replace(REX_TOOLTIP, function(s) { - return ENTITY_MAP[s]; - }); - } - - // TODO: use currying - function _makeNodeTitleMatcher(s) { - s = s.toLowerCase(); - return function(node) { - return node.title.toLowerCase().indexOf(s) >= 0; - }; - } - - function _makeNodeTitleStartMatcher(s) { - var reMatch = new RegExp("^" + s, "i"); - return function(node) { - return reMatch.test(node.title); - }; - } - - /****************************************************************************** - * FancytreeNode - */ - - /** - * Creates a new FancytreeNode - * - * @class FancytreeNode - * @classdesc A FancytreeNode represents the hierarchical data model and operations. - * - * @param {FancytreeNode} parent - * @param {NodeData} obj - * - * @property {Fancytree} tree The tree instance - * @property {FancytreeNode} parent The parent node - * @property {string} key Node id (must be unique inside the tree) - * @property {string} title Display name (may contain HTML) - * @property {object} data Contains all extra data that was passed on node creation - * @property {FancytreeNode[] | null | undefined} children Array of child nodes.
    - * For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array - * to define a node that has no children. - * @property {boolean} expanded Use isExpanded(), setExpanded() to access this property. - * @property {string} extraClasses Additional CSS classes, added to the node's `<span>`.
    - * Note: use `node.add/remove/toggleClass()` to modify. - * @property {boolean} folder Folder nodes have different default icons and click behavior.
    - * Note: Also non-folders may have children. - * @property {string} statusNodeType null for standard nodes. Otherwise type of special system node: 'error', 'loading', 'nodata', or 'paging'. - * @property {boolean} lazy True if this node is loaded on demand, i.e. on first expansion. - * @property {boolean} selected Use isSelected(), setSelected() to access this property. - * @property {string} tooltip Alternative description used as hover popup - * @property {string} iconTooltip Description used as hover popup for icon. @since 2.27 - * @property {string} type Node type, used with tree.types map. @since 2.27 - */ - function FancytreeNode(parent, obj) { - var i, l, name, cl; - - this.parent = parent; - this.tree = parent.tree; - this.ul = null; - this.li = null; //
  • tag - this.statusNodeType = null; // if this is a temp. node to display the status of its parent - this._isLoading = false; // if this node itself is loading - this._error = null; // {message: '...'} if a load error occurred - this.data = {}; - - // TODO: merge this code with node.toDict() - // copy attributes from obj object - for (i = 0, l = NODE_ATTRS.length; i < l; i++) { - name = NODE_ATTRS[i]; - this[name] = obj[name]; - } - // unselectableIgnore and unselectableStatus imply unselectable - if ( - this.unselectableIgnore != null || - this.unselectableStatus != null - ) { - this.unselectable = true; - } - if (obj.hideCheckbox) { - $.error( - "'hideCheckbox' node option was removed in v2.23.0: use 'checkbox: false'" - ); - } - // node.data += obj.data - if (obj.data) { - $.extend(this.data, obj.data); - } - // Copy all other attributes to this.data.NAME - for (name in obj) { - if ( - !NODE_ATTR_MAP[name] && - !$.isFunction(obj[name]) && - !NONE_NODE_DATA_MAP[name] - ) { - // node.data.NAME = obj.NAME - this.data[name] = obj[name]; - } - } - - // Fix missing key - if (this.key == null) { - // test for null OR undefined - if (this.tree.options.defaultKey) { - this.key = this.tree.options.defaultKey(this); - _assert(this.key, "defaultKey() must return a unique key"); - } else { - this.key = "_" + FT._nextNodeKey++; - } - } else { - this.key = "" + this.key; // Convert to string (#217) - } - - // Fix tree.activeNode - // TODO: not elegant: we use obj.active as marker to set tree.activeNode - // when loading from a dictionary. - if (obj.active) { - _assert( - this.tree.activeNode === null, - "only one active node allowed" - ); - this.tree.activeNode = this; - } - if (obj.selected) { - // #186 - this.tree.lastSelectedNode = this; - } - // TODO: handle obj.focus = true - - // Create child nodes - cl = obj.children; - if (cl) { - if (cl.length) { - this._setChildren(cl); - } else { - // if an empty array was passed for a lazy node, keep it, in order to mark it 'loaded' - this.children = this.lazy ? [] : null; - } - } else { - this.children = null; - } - // Add to key/ref map (except for root node) - // if( parent ) { - this.tree._callHook("treeRegisterNode", this.tree, true, this); - // } - } - - FancytreeNode.prototype = /** @lends FancytreeNode# */ { - /* Return the direct child FancytreeNode with a given key, index. */ - _findDirectChild: function(ptr) { - var i, - l, - cl = this.children; - - if (cl) { - if (typeof ptr === "string") { - for (i = 0, l = cl.length; i < l; i++) { - if (cl[i].key === ptr) { - return cl[i]; - } - } - } else if (typeof ptr === "number") { - return this.children[ptr]; - } else if (ptr.parent === this) { - return ptr; - } - } - return null; - }, - // TODO: activate() - // TODO: activateSilently() - /* Internal helper called in recursive addChildren sequence.*/ - _setChildren: function(children) { - _assert( - children && (!this.children || this.children.length === 0), - "only init supported" - ); - this.children = []; - for (var i = 0, l = children.length; i < l; i++) { - this.children.push(new FancytreeNode(this, children[i])); - } - }, - /** - * Append (or insert) a list of child nodes. - * - * @param {NodeData[]} children array of child node definitions (also single child accepted) - * @param {FancytreeNode | string | Integer} [insertBefore] child node (or key or index of such). - * If omitted, the new children are appended. - * @returns {FancytreeNode} first child added - * - * @see FancytreeNode#applyPatch - */ - addChildren: function(children, insertBefore) { - var i, - l, - pos, - origFirstChild = this.getFirstChild(), - origLastChild = this.getLastChild(), - firstNode = null, - nodeList = []; - - if ($.isPlainObject(children)) { - children = [children]; - } - if (!this.children) { - this.children = []; - } - for (i = 0, l = children.length; i < l; i++) { - nodeList.push(new FancytreeNode(this, children[i])); - } - firstNode = nodeList[0]; - if (insertBefore == null) { - this.children = this.children.concat(nodeList); - } else { - // Returns null if insertBefore is not a direct child: - insertBefore = this._findDirectChild(insertBefore); - pos = $.inArray(insertBefore, this.children); - _assert(pos >= 0, "insertBefore must be an existing child"); - // insert nodeList after children[pos] - this.children.splice.apply( - this.children, - [pos, 0].concat(nodeList) - ); - } - if (origFirstChild && !insertBefore) { - // #708: Fast path -- don't render every child of root, just the new ones! - // #723, #729: but only if it's appended to an existing child list - for (i = 0, l = nodeList.length; i < l; i++) { - nodeList[i].render(); // New nodes were never rendered before - } - // Adjust classes where status may have changed - // Has a first child - if (origFirstChild !== this.getFirstChild()) { - // Different first child -- recompute classes - origFirstChild.renderStatus(); - } - if (origLastChild !== this.getLastChild()) { - // Different last child -- recompute classes - origLastChild.renderStatus(); - } - } else if (!this.parent || this.parent.ul || this.tr) { - // render if the parent was rendered (or this is a root node) - this.render(); - } - if (this.tree.options.selectMode === 3) { - this.fixSelection3FromEndNodes(); - } - this.triggerModifyChild( - "add", - nodeList.length === 1 ? nodeList[0] : null - ); - return firstNode; - }, - /** - * Add class to node's span tag and to .extraClasses. - * - * @param {string} className class name - * - * @since 2.17 - */ - addClass: function(className) { - return this.toggleClass(className, true); - }, - /** - * Append or prepend a node, or append a child node. - * - * This a convenience function that calls addChildren() - * - * @param {NodeData} node node definition - * @param {string} [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child') - * @returns {FancytreeNode} new node - */ - addNode: function(node, mode) { - if (mode === undefined || mode === "over") { - mode = "child"; - } - switch (mode) { - case "after": - return this.getParent().addChildren( - node, - this.getNextSibling() - ); - case "before": - return this.getParent().addChildren(node, this); - case "firstChild": - // Insert before the first child if any - var insertBefore = this.children ? this.children[0] : null; - return this.addChildren(node, insertBefore); - case "child": - case "over": - return this.addChildren(node); - } - _assert(false, "Invalid mode: " + mode); - }, - /**Add child status nodes that indicate 'More...', etc. - * - * This also maintains the node's `partload` property. - * @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes. - * @param {string} [mode='child'] 'child'|firstChild' - * @since 2.15 - */ - addPagingNode: function(node, mode) { - var i, n; - - mode = mode || "child"; - if (node === false) { - for (i = this.children.length - 1; i >= 0; i--) { - n = this.children[i]; - if (n.statusNodeType === "paging") { - this.removeChild(n); - } - } - this.partload = false; - return; - } - node = $.extend( - { - title: this.tree.options.strings.moreData, - statusNodeType: "paging", - icon: false, - }, - node - ); - this.partload = true; - return this.addNode(node, mode); - }, - /** - * Append new node after this. - * - * This a convenience function that calls addNode(node, 'after') - * - * @param {NodeData} node node definition - * @returns {FancytreeNode} new node - */ - appendSibling: function(node) { - return this.addNode(node, "after"); - }, - /** - * Modify existing child nodes. - * - * @param {NodePatch} patch - * @returns {$.Promise} - * @see FancytreeNode#addChildren - */ - applyPatch: function(patch) { - // patch [key, null] means 'remove' - if (patch === null) { - this.remove(); - return _getResolvedPromise(this); - } - // TODO: make sure that root node is not collapsed or modified - // copy (most) attributes to node.ATTR or node.data.ATTR - var name, - promise, - v, - IGNORE_MAP = { children: true, expanded: true, parent: true }; // TODO: should be global - - for (name in patch) { - v = patch[name]; - if (!IGNORE_MAP[name] && !$.isFunction(v)) { - if (NODE_ATTR_MAP[name]) { - this[name] = v; - } else { - this.data[name] = v; - } - } - } - // Remove and/or create children - if (patch.hasOwnProperty("children")) { - this.removeChildren(); - if (patch.children) { - // only if not null and not empty list - // TODO: addChildren instead? - this._setChildren(patch.children); - } - // TODO: how can we APPEND or INSERT child nodes? - } - if (this.isVisible()) { - this.renderTitle(); - this.renderStatus(); - } - // Expand collapse (final step, since this may be async) - if (patch.hasOwnProperty("expanded")) { - promise = this.setExpanded(patch.expanded); - } else { - promise = _getResolvedPromise(this); - } - return promise; - }, - /** Collapse all sibling nodes. - * @returns {$.Promise} - */ - collapseSiblings: function() { - return this.tree._callHook("nodeCollapseSiblings", this); - }, - /** Copy this node as sibling or child of `node`. - * - * @param {FancytreeNode} node source node - * @param {string} [mode=child] 'before' | 'after' | 'child' - * @param {Function} [map] callback function(NodeData) that could modify the new node - * @returns {FancytreeNode} new - */ - copyTo: function(node, mode, map) { - return node.addNode(this.toDict(true, map), mode); - }, - /** Count direct and indirect children. - * - * @param {boolean} [deep=true] pass 'false' to only count direct children - * @returns {int} number of child nodes - */ - countChildren: function(deep) { - var cl = this.children, - i, - l, - n; - if (!cl) { - return 0; - } - n = cl.length; - if (deep !== false) { - for (i = 0, l = n; i < l; i++) { - n += cl[i].countChildren(); - } - } - return n; - }, - // TODO: deactivate() - /** Write to browser console if debugLevel >= 4 (prepending node info) - * - * @param {*} msg string or object or array of such - */ - debug: function(msg) { - if (this.tree.options.debugLevel >= 4) { - Array.prototype.unshift.call(arguments, this.toString()); - consoleApply("log", arguments); - } - }, - /** Deprecated. - * @deprecated since 2014-02-16. Use resetLazy() instead. - */ - discard: function() { - this.warn( - "FancytreeNode.discard() is deprecated since 2014-02-16. Use .resetLazy() instead." - ); - return this.resetLazy(); - }, - /** Remove DOM elements for all descendents. May be called on .collapse event - * to keep the DOM small. - * @param {boolean} [includeSelf=false] - */ - discardMarkup: function(includeSelf) { - var fn = includeSelf ? "nodeRemoveMarkup" : "nodeRemoveChildMarkup"; - this.tree._callHook(fn, this); - }, - /** Write error to browser console if debugLevel >= 1 (prepending tree info) - * - * @param {*} msg string or object or array of such - */ - error: function(msg) { - if (this.options.debugLevel >= 1) { - Array.prototype.unshift.call(arguments, this.toString()); - consoleApply("error", arguments); - } - }, - /**Find all nodes that match condition (excluding self). - * - * @param {string | function(node)} match title string to search for, or a - * callback function that returns `true` if a node is matched. - * @returns {FancytreeNode[]} array of nodes (may be empty) - */ - findAll: function(match) { - match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match); - var res = []; - this.visit(function(n) { - if (match(n)) { - res.push(n); - } - }); - return res; - }, - /**Find first node that matches condition (excluding self). - * - * @param {string | function(node)} match title string to search for, or a - * callback function that returns `true` if a node is matched. - * @returns {FancytreeNode} matching node or null - * @see FancytreeNode#findAll - */ - findFirst: function(match) { - match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match); - var res = null; - this.visit(function(n) { - if (match(n)) { - res = n; - return false; - } - }); - return res; - }, - /* Apply selection state (internal use only) */ - _changeSelectStatusAttrs: function(state) { - var changed = false, - opts = this.tree.options, - unselectable = FT.evalOption( - "unselectable", - this, - this, - opts, - false - ), - unselectableStatus = FT.evalOption( - "unselectableStatus", - this, - this, - opts, - undefined - ); - - if (unselectable && unselectableStatus != null) { - state = unselectableStatus; - } - switch (state) { - case false: - changed = this.selected || this.partsel; - this.selected = false; - this.partsel = false; - break; - case true: - changed = !this.selected || !this.partsel; - this.selected = true; - this.partsel = true; - break; - case undefined: - changed = this.selected || !this.partsel; - this.selected = false; - this.partsel = true; - break; - default: - _assert(false, "invalid state: " + state); - } - // this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed); - if (changed) { - this.renderStatus(); - } - return changed; - }, - /** - * Fix selection status, after this node was (de)selected in multi-hier mode. - * This includes (de)selecting all children. - */ - fixSelection3AfterClick: function(callOpts) { - var flag = this.isSelected(); - - // this.debug("fixSelection3AfterClick()"); - - this.visit(function(node) { - node._changeSelectStatusAttrs(flag); - }); - this.fixSelection3FromEndNodes(callOpts); - }, - /** - * Fix selection status for multi-hier mode. - * Only end-nodes are considered to update the descendants branch and parents. - * Should be called after this node has loaded new children or after - * children have been modified using the API. - */ - fixSelection3FromEndNodes: function(callOpts) { - var opts = this.tree.options; - - // this.debug("fixSelection3FromEndNodes()"); - _assert(opts.selectMode === 3, "expected selectMode 3"); - - // Visit all end nodes and adjust their parent's `selected` and `partsel` - // attributes. Return selection state true, false, or undefined. - function _walk(node) { - var i, - l, - child, - s, - state, - allSelected, - someSelected, - unselIgnore, - unselState, - children = node.children; - - if (children && children.length) { - // check all children recursively - allSelected = true; - someSelected = false; - - for (i = 0, l = children.length; i < l; i++) { - child = children[i]; - // the selection state of a node is not relevant; we need the end-nodes - s = _walk(child); - // if( !child.unselectableIgnore ) { - unselIgnore = FT.evalOption( - "unselectableIgnore", - child, - child, - opts, - false - ); - if (!unselIgnore) { - if (s !== false) { - someSelected = true; - } - if (s !== true) { - allSelected = false; - } - } - } - state = allSelected - ? true - : someSelected - ? undefined - : false; - } else { - // This is an end-node: simply report the status - unselState = FT.evalOption( - "unselectableStatus", - node, - node, - opts, - undefined - ); - state = unselState == null ? !!node.selected : !!unselState; - } - node._changeSelectStatusAttrs(state); - return state; - } - _walk(this); - - // Update parent's state - this.visitParents(function(node) { - var i, - l, - child, - state, - unselIgnore, - unselState, - children = node.children, - allSelected = true, - someSelected = false; - - for (i = 0, l = children.length; i < l; i++) { - child = children[i]; - unselIgnore = FT.evalOption( - "unselectableIgnore", - child, - child, - opts, - false - ); - if (!unselIgnore) { - unselState = FT.evalOption( - "unselectableStatus", - child, - child, - opts, - undefined - ); - state = - unselState == null - ? !!child.selected - : !!unselState; - // When fixing the parents, we trust the sibling status (i.e. - // we don't recurse) - if (state || child.partsel) { - someSelected = true; - } - if (!state) { - allSelected = false; - } - } - } - state = allSelected ? true : someSelected ? undefined : false; - node._changeSelectStatusAttrs(state); - }); - }, - // TODO: focus() - /** - * Update node data. If dict contains 'children', then also replace - * the hole sub tree. - * @param {NodeData} dict - * - * @see FancytreeNode#addChildren - * @see FancytreeNode#applyPatch - */ - fromDict: function(dict) { - // copy all other attributes to this.data.xxx - for (var name in dict) { - if (NODE_ATTR_MAP[name]) { - // node.NAME = dict.NAME - this[name] = dict[name]; - } else if (name === "data") { - // node.data += dict.data - $.extend(this.data, dict.data); - } else if ( - !$.isFunction(dict[name]) && - !NONE_NODE_DATA_MAP[name] - ) { - // node.data.NAME = dict.NAME - this.data[name] = dict[name]; - } - } - if (dict.children) { - // recursively set children and render - this.removeChildren(); - this.addChildren(dict.children); - } - this.renderTitle(); - /* - var children = dict.children; - if(children === undefined){ - this.data = $.extend(this.data, dict); - this.render(); - return; - } - dict = $.extend({}, dict); - dict.children = undefined; - this.data = $.extend(this.data, dict); - this.removeChildren(); - this.addChild(children); - */ - }, - /** Return the list of child nodes (undefined for unexpanded lazy nodes). - * @returns {FancytreeNode[] | undefined} - */ - getChildren: function() { - if (this.hasChildren() === undefined) { - // TODO: only required for lazy nodes? - return undefined; // Lazy node: unloaded, currently loading, or load error - } - return this.children; - }, - /** Return the first child node or null. - * @returns {FancytreeNode | null} - */ - getFirstChild: function() { - return this.children ? this.children[0] : null; - }, - /** Return the 0-based child index. - * @returns {int} - */ - getIndex: function() { - // return this.parent.children.indexOf(this); - return $.inArray(this, this.parent.children); // indexOf doesn't work in IE7 - }, - /** Return the hierarchical child index (1-based, e.g. '3.2.4'). - * @param {string} [separator="."] - * @param {int} [digits=1] - * @returns {string} - */ - getIndexHier: function(separator, digits) { - separator = separator || "."; - var s, - res = []; - $.each(this.getParentList(false, true), function(i, o) { - s = "" + (o.getIndex() + 1); - if (digits) { - // prepend leading zeroes - s = ("0000000" + s).substr(-digits); - } - res.push(s); - }); - return res.join(separator); - }, - /** Return the parent keys separated by options.keyPathSeparator, e.g. "id_1/id_17/id_32". - * @param {boolean} [excludeSelf=false] - * @returns {string} - */ - getKeyPath: function(excludeSelf) { - var path = [], - sep = this.tree.options.keyPathSeparator; - this.visitParents(function(n) { - if (n.parent) { - path.unshift(n.key); - } - }, !excludeSelf); - return sep + path.join(sep); - }, - /** Return the last child of this node or null. - * @returns {FancytreeNode | null} - */ - getLastChild: function() { - return this.children - ? this.children[this.children.length - 1] - : null; - }, - /** Return node depth. 0: System root node, 1: visible top-level node, 2: first sub-level, ... . - * @returns {int} - */ - getLevel: function() { - var level = 0, - dtn = this.parent; - while (dtn) { - level++; - dtn = dtn.parent; - } - return level; - }, - /** Return the successor node (under the same parent) or null. - * @returns {FancytreeNode | null} - */ - getNextSibling: function() { - // TODO: use indexOf, if available: (not in IE6) - if (this.parent) { - var i, - l, - ac = this.parent.children; - - for (i = 0, l = ac.length - 1; i < l; i++) { - // up to length-2, so next(last) = null - if (ac[i] === this) { - return ac[i + 1]; - } - } - } - return null; - }, - /** Return the parent node (null for the system root node). - * @returns {FancytreeNode | null} - */ - getParent: function() { - // TODO: return null for top-level nodes? - return this.parent; - }, - /** Return an array of all parent nodes (top-down). - * @param {boolean} [includeRoot=false] Include the invisible system root node. - * @param {boolean} [includeSelf=false] Include the node itself. - * @returns {FancytreeNode[]} - */ - getParentList: function(includeRoot, includeSelf) { - var l = [], - dtn = includeSelf ? this : this.parent; - while (dtn) { - if (includeRoot || dtn.parent) { - l.unshift(dtn); - } - dtn = dtn.parent; - } - return l; - }, - /** Return the predecessor node (under the same parent) or null. - * @returns {FancytreeNode | null} - */ - getPrevSibling: function() { - if (this.parent) { - var i, - l, - ac = this.parent.children; - - for (i = 1, l = ac.length; i < l; i++) { - // start with 1, so prev(first) = null - if (ac[i] === this) { - return ac[i - 1]; - } - } - } - return null; - }, - /** - * Return an array of selected descendant nodes. - * @param {boolean} [stopOnParents=false] only return the topmost selected - * node (useful with selectMode 3) - * @returns {FancytreeNode[]} - */ - getSelectedNodes: function(stopOnParents) { - var nodeList = []; - this.visit(function(node) { - if (node.selected) { - nodeList.push(node); - if (stopOnParents === true) { - return "skip"; // stop processing this branch - } - } - }); - return nodeList; - }, - /** Return true if node has children. Return undefined if not sure, i.e. the node is lazy and not yet loaded). - * @returns {boolean | undefined} - */ - hasChildren: function() { - if (this.lazy) { - if (this.children == null) { - // null or undefined: Not yet loaded - return undefined; - } else if (this.children.length === 0) { - // Loaded, but response was empty - return false; - } else if ( - this.children.length === 1 && - this.children[0].isStatusNode() - ) { - // Currently loading or load error - return undefined; - } - return true; - } - return !!(this.children && this.children.length); - }, - /** Return true if node has keyboard focus. - * @returns {boolean} - */ - hasFocus: function() { - return this.tree.hasFocus() && this.tree.focusNode === this; - }, - /** Write to browser console if debugLevel >= 3 (prepending node info) - * - * @param {*} msg string or object or array of such - */ - info: function(msg) { - if (this.tree.options.debugLevel >= 3) { - Array.prototype.unshift.call(arguments, this.toString()); - consoleApply("info", arguments); - } - }, - /** Return true if node is active (see also FancytreeNode#isSelected). - * @returns {boolean} - */ - isActive: function() { - return this.tree.activeNode === this; - }, - /** Return true if node is vertically below `otherNode`, i.e. rendered in a subsequent row. - * @param {FancytreeNode} otherNode - * @returns {boolean} - * @since 2.28 - */ - isBelowOf: function(otherNode) { - return this.getIndexHier(".", 5) > otherNode.getIndexHier(".", 5); - }, - /** Return true if node is a direct child of otherNode. - * @param {FancytreeNode} otherNode - * @returns {boolean} - */ - isChildOf: function(otherNode) { - return this.parent && this.parent === otherNode; - }, - /** Return true, if node is a direct or indirect sub node of otherNode. - * @param {FancytreeNode} otherNode - * @returns {boolean} - */ - isDescendantOf: function(otherNode) { - if (!otherNode || otherNode.tree !== this.tree) { - return false; - } - var p = this.parent; - while (p) { - if (p === otherNode) { - return true; - } - if (p === p.parent) { - $.error("Recursive parent link: " + p); - } - p = p.parent; - } - return false; - }, - /** Return true if node is expanded. - * @returns {boolean} - */ - isExpanded: function() { - return !!this.expanded; - }, - /** Return true if node is the first node of its parent's children. - * @returns {boolean} - */ - isFirstSibling: function() { - var p = this.parent; - return !p || p.children[0] === this; - }, - /** Return true if node is a folder, i.e. has the node.folder attribute set. - * @returns {boolean} - */ - isFolder: function() { - return !!this.folder; - }, - /** Return true if node is the last node of its parent's children. - * @returns {boolean} - */ - isLastSibling: function() { - var p = this.parent; - return !p || p.children[p.children.length - 1] === this; - }, - /** Return true if node is lazy (even if data was already loaded) - * @returns {boolean} - */ - isLazy: function() { - return !!this.lazy; - }, - /** Return true if node is lazy and loaded. For non-lazy nodes always return true. - * @returns {boolean} - */ - isLoaded: function() { - return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node - }, - /** Return true if children are currently beeing loaded, i.e. a Ajax request is pending. - * @returns {boolean} - */ - isLoading: function() { - return !!this._isLoading; - }, - /* - * @deprecated since v2.4.0: Use isRootNode() instead - */ - isRoot: function() { - return this.isRootNode(); - }, - /** Return true if node is partially selected (tri-state). - * @returns {boolean} - * @since 2.23 - */ - isPartsel: function() { - return !this.selected && !!this.partsel; - }, - /** (experimental) Return true if this is partially loaded. - * @returns {boolean} - * @since 2.15 - */ - isPartload: function() { - return !!this.partload; - }, - /** Return true if this is the (invisible) system root node. - * @returns {boolean} - * @since 2.4 - */ - isRootNode: function() { - return this.tree.rootNode === this; - }, - /** Return true if node is selected, i.e. has a checkmark set (see also FancytreeNode#isActive). - * @returns {boolean} - */ - isSelected: function() { - return !!this.selected; - }, - /** Return true if this node is a temporarily generated system node like - * 'loading', 'paging', or 'error' (node.statusNodeType contains the type). - * @returns {boolean} - */ - isStatusNode: function() { - return !!this.statusNodeType; - }, - /** Return true if this node is a status node of type 'paging'. - * @returns {boolean} - * @since 2.15 - */ - isPagingNode: function() { - return this.statusNodeType === "paging"; - }, - /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. - * @returns {boolean} - * @since 2.4 - */ - isTopLevel: function() { - return this.tree.rootNode === this.parent; - }, - /** Return true if node is lazy and not yet loaded. For non-lazy nodes always return false. - * @returns {boolean} - */ - isUndefined: function() { - return this.hasChildren() === undefined; // also checks if the only child is a status node - }, - /** Return true if all parent nodes are expanded. Note: this does not check - * whether the node is scrolled into the visible part of the screen. - * @returns {boolean} - */ - isVisible: function() { - var i, - l, - parents = this.getParentList(false, false); - - for (i = 0, l = parents.length; i < l; i++) { - if (!parents[i].expanded) { - return false; - } - } - return true; - }, - /** Deprecated. - * @deprecated since 2014-02-16: use load() instead. - */ - lazyLoad: function(discard) { - this.warn( - "FancytreeNode.lazyLoad() is deprecated since 2014-02-16. Use .load() instead." - ); - return this.load(discard); - }, - /** - * Load all children of a lazy node if neccessary. The expanded state is maintained. - * @param {boolean} [forceReload=false] Pass true to discard any existing nodes before. Otherwise this method does nothing if the node was already loaded. - * @returns {$.Promise} - */ - load: function(forceReload) { - var res, - source, - that = this, - wasExpanded = this.isExpanded(); - - _assert(this.isLazy(), "load() requires a lazy node"); - // _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" ); - if (!forceReload && !this.isUndefined()) { - return _getResolvedPromise(this); - } - if (this.isLoaded()) { - this.resetLazy(); // also collapses - } - // This method is also called by setExpanded() and loadKeyPath(), so we - // have to avoid recursion. - source = this.tree._triggerNodeEvent("lazyLoad", this); - if (source === false) { - // #69 - return _getResolvedPromise(this); - } - _assert( - typeof source !== "boolean", - "lazyLoad event must return source in data.result" - ); - res = this.tree._callHook("nodeLoadChildren", this, source); - if (wasExpanded) { - this.expanded = true; - res.always(function() { - that.render(); - }); - } else { - res.always(function() { - that.renderStatus(); // fix expander icon to 'loaded' - }); - } - return res; - }, - /** Expand all parents and optionally scroll into visible area as neccessary. - * Promise is resolved, when lazy loading and animations are done. - * @param {object} [opts] passed to `setExpanded()`. - * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} - * @returns {$.Promise} - */ - makeVisible: function(opts) { - var i, - that = this, - deferreds = [], - dfd = new $.Deferred(), - parents = this.getParentList(false, false), - len = parents.length, - effects = !(opts && opts.noAnimation === true), - scroll = !(opts && opts.scrollIntoView === false); - - // Expand bottom-up, so only the top node is animated - for (i = len - 1; i >= 0; i--) { - // that.debug("pushexpand" + parents[i]); - deferreds.push(parents[i].setExpanded(true, opts)); - } - $.when.apply($, deferreds).done(function() { - // All expands have finished - // that.debug("expand DONE", scroll); - if (scroll) { - that.scrollIntoView(effects).done(function() { - // that.debug("scroll DONE"); - dfd.resolve(); - }); - } else { - dfd.resolve(); - } - }); - return dfd.promise(); - }, - /** Move this node to targetNode. - * @param {FancytreeNode} targetNode - * @param {string} mode
    -		 *      'child': append this node as last child of targetNode.
    -		 *               This is the default. To be compatble with the D'n'd
    -		 *               hitMode, we also accept 'over'.
    -		 *      'firstChild': add this node as first child of targetNode.
    -		 *      'before': add this node as sibling before targetNode.
    -		 *      'after': add this node as sibling after targetNode.
    - * @param {function} [map] optional callback(FancytreeNode) to allow modifcations - */ - moveTo: function(targetNode, mode, map) { - if (mode === undefined || mode === "over") { - mode = "child"; - } else if (mode === "firstChild") { - if (targetNode.children && targetNode.children.length) { - mode = "before"; - targetNode = targetNode.children[0]; - } else { - mode = "child"; - } - } - var pos, - prevParent = this.parent, - targetParent = - mode === "child" ? targetNode : targetNode.parent; - - if (this === targetNode) { - return; - } else if (!this.parent) { - $.error("Cannot move system root"); - } else if (targetParent.isDescendantOf(this)) { - $.error("Cannot move a node to its own descendant"); - } - if (targetParent !== prevParent) { - prevParent.triggerModifyChild("remove", this); - } - // Unlink this node from current parent - if (this.parent.children.length === 1) { - if (this.parent === targetParent) { - return; // #258 - } - this.parent.children = this.parent.lazy ? [] : null; - this.parent.expanded = false; - } else { - pos = $.inArray(this, this.parent.children); - _assert(pos >= 0, "invalid source parent"); - this.parent.children.splice(pos, 1); - } - // Remove from source DOM parent - // if(this.parent.ul){ - // this.parent.ul.removeChild(this.li); - // } - - // Insert this node to target parent's child list - this.parent = targetParent; - if (targetParent.hasChildren()) { - switch (mode) { - case "child": - // Append to existing target children - targetParent.children.push(this); - break; - case "before": - // Insert this node before target node - pos = $.inArray(targetNode, targetParent.children); - _assert(pos >= 0, "invalid target parent"); - targetParent.children.splice(pos, 0, this); - break; - case "after": - // Insert this node after target node - pos = $.inArray(targetNode, targetParent.children); - _assert(pos >= 0, "invalid target parent"); - targetParent.children.splice(pos + 1, 0, this); - break; - default: - $.error("Invalid mode " + mode); - } - } else { - targetParent.children = [this]; - } - // Parent has no