From cadd6ab5b375ad1830cfcc9492f8754e12afb0f9 Mon Sep 17 00:00:00 2001 From: va barbosa Date: Mon, 17 Aug 2020 10:05:54 -0400 Subject: [PATCH 1/8] Update node properties to allow changing file Added a file browser dialog and added support for changing file from the properties dialog Fixes #732 --- .../src/PipelineEditorWidget.tsx | 37 +++++ packages/pipeline-editor/src/properties.json | 53 +++++++ packages/pipeline-editor/style/index.css | 5 + .../ui-components/src/BrowseFileDialog.tsx | 145 ++++++++++++++++++ packages/ui-components/src/index.ts | 1 + packages/ui-components/style/index.css | 5 + 6 files changed, 246 insertions(+) create mode 100644 packages/ui-components/src/BrowseFileDialog.tsx diff --git a/packages/pipeline-editor/src/PipelineEditorWidget.tsx b/packages/pipeline-editor/src/PipelineEditorWidget.tsx index ee8fb5660..02a45460a 100644 --- a/packages/pipeline-editor/src/PipelineEditorWidget.tsx +++ b/packages/pipeline-editor/src/PipelineEditorWidget.tsx @@ -30,6 +30,7 @@ import { pipelineIcon, savePipelineIcon, runtimesIcon, + showBrowseFileDialog, showFormDialog, errorIcon } from '@elyra/ui-components'; @@ -200,6 +201,7 @@ export class PipelineEditor extends React.Component< position = 10; node: React.RefObject; propertiesInfo: any; + propertiesController: any; constructor(props: any) { super(props); @@ -233,6 +235,10 @@ export class PipelineEditor extends React.Component< this.applyPropertyChanges = this.applyPropertyChanges.bind(this); this.closePropertiesDialog = this.closePropertiesDialog.bind(this); this.openPropertiesDialog = this.openPropertiesDialog.bind(this); + this.propertiesActionHandler = this.propertiesActionHandler.bind(this); + this.propertiesControllerHandler = this.propertiesControllerHandler.bind( + this + ); this.node = React.createRef(); this.handleEvent = this.handleEvent.bind(this); @@ -342,6 +348,8 @@ export class PipelineEditor extends React.Component< ]; const propertiesCallbacks = { + actionHandler: this.propertiesActionHandler, + controllerHandler: this.propertiesControllerHandler, applyPropertyChanges: this.applyPropertyChanges, closePropertiesDialog: this.closePropertiesDialog }; @@ -456,6 +464,13 @@ export class PipelineEditor extends React.Component< } const app_data = node.app_data; + if (app_data.filename !== propertySet.filename) { + app_data.filename = propertySet.filename; + node.label = propertySet.filename + .replace(/^.*[\\/]/, '') + .replace(/\.[^/.]+$/, ''); + } + app_data.runtime_image = propertySet.runtime_image; app_data.outputs = propertySet.outputs; app_data.env_vars = propertySet.env_vars; @@ -471,6 +486,28 @@ export class PipelineEditor extends React.Component< this.setState({ showPropertiesDialog: false, propertiesInfo: propsInfo }); } + propertiesControllerHandler(propertiesController: any): void { + this.propertiesController = propertiesController; + } + + propertiesActionHandler(id: string, appData: any, data: any): void { + if (id === 'browse_file') { + const propertyId = { name: data.parameter_ref }; + showBrowseFileDialog(this.browserFactory.defaultBrowser.model.manager, { + filter: (model: any): boolean => { + return model.type == 'notebook'; + } + }).then((result: any) => { + if (result.button.accept && result.value.length) { + this.propertiesController.updatePropertyValue( + propertyId, + result.value[0].path + ); + } + }); + } + } + /* * Add options to the node context menu * Pipeline specific context menu items are: diff --git a/packages/pipeline-editor/src/properties.json b/packages/pipeline-editor/src/properties.json index 68f0af596..266b1f3cc 100644 --- a/packages/pipeline-editor/src/properties.json +++ b/packages/pipeline-editor/src/properties.json @@ -83,6 +83,59 @@ "default": "Files generated during execution that will become available to all subsequent pipeline steps.\nOne filename (including subdirectory) per line." } } + ], + "action_info": [ + { + "id": "browse_file", + "label": { + "default": "Browse..." + }, + "control": "button", + "data": { + "parameter_ref": "filename" + } + } + ], + "group_info": [ + { + "id": "nodeGroupInfo", + "label": { + "default": "Node Properties" + }, + "type": "panels", + "group_info": [ + { + "id": "browseFilePanel", + "type": "columnPanel", + "label": { + "default": "Browse File Panel" + }, + "group_info": [ + { + "id": "nodeFileControl", + "type": "controls", + "parameter_refs": ["filename"] + }, + { + "id": "nodeBrowseFileAction", + "type": "actionPanel", + "action_refs": ["browse_file"] + } + ] + }, + { + "id": "nodePropertiesControls", + "type": "controls", + "parameter_refs": [ + "runtime_image", + "dependencies", + "include_subdirectories", + "env_vars", + "outputs" + ] + } + ] + } ] }, "resources": {} diff --git a/packages/pipeline-editor/style/index.css b/packages/pipeline-editor/style/index.css index 489c6fd8d..73052633f 100644 --- a/packages/pipeline-editor/style/index.css +++ b/packages/pipeline-editor/style/index.css @@ -119,3 +119,8 @@ td { padding-left: 10px; padding-right: 10px; } + +[data-id='properties-nodeBrowseFileAction'] { + flex-direction: row; + justify-content: end; +} diff --git a/packages/ui-components/src/BrowseFileDialog.tsx b/packages/ui-components/src/BrowseFileDialog.tsx new file mode 100644 index 000000000..9ac9c5ea1 --- /dev/null +++ b/packages/ui-components/src/BrowseFileDialog.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2018-2020 IBM Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Dialog } from '@jupyterlab/apputils'; +import { IDocumentManager } from '@jupyterlab/docmanager'; +import { + BreadCrumbs, + DirListing, + FilterFileBrowserModel +} from '@jupyterlab/filebrowser'; +import { Widget, PanelLayout } from '@lumino/widgets'; + +const BROWSE_FILE_CLASS = 'elyra-browseFileDialog'; + +export interface IBrowseFileDialogOptions { + filter?: (model: any) => boolean; + multiselect?: boolean; + includeDir?: boolean; +} + +/** + * Browse file widget for dialog body + */ +export class BrowseFileDialog extends Widget + implements Dialog.IBodyWidget { + directoryListing: DirListing; + breadCrumbs: BreadCrumbs; + dirListingHandleEvent: (event: Event) => void; + multiselect: boolean; + includeDir: boolean; + + constructor(props: any) { + super(props); + + const model = new FilterFileBrowserModel({ + manager: props.manager, + filter: props.filter + }); + + const layout = (this.layout = new PanelLayout()); + + this.directoryListing = new DirListing({ + model: model + }); + + this.multiselect = props.multiselect; + this.includeDir = props.includeDir; + this.dirListingHandleEvent = this.directoryListing.handleEvent; + this.directoryListing.handleEvent = (event: Event): void => { + this.handleEvent(event); + }; + + this.breadCrumbs = new BreadCrumbs({ + model: model + }); + + layout.addWidget(this.breadCrumbs); + layout.addWidget(this.directoryListing); + } + + getValue(): any { + const itemsIter = this.directoryListing.selectedItems(); + const selected = []; + let item = null; + + while ((item = itemsIter.next()) !== undefined) { + if (this.includeDir || item.type !== 'directory') { + selected.push(item); + } + } + + return selected; + } + + handleEvent(event: Event): void { + let modifierKey = false; + if (event instanceof MouseEvent) { + modifierKey = + (event as MouseEvent).shiftKey || (event as MouseEvent).metaKey; + } else if (event instanceof KeyboardEvent) { + modifierKey = + (event as KeyboardEvent).shiftKey || (event as KeyboardEvent).metaKey; + } + + switch (event.type) { + case 'keydown': + case 'keyup': + case 'mousedown': + case 'mouseup': + case 'click': + if (this.multiselect || !modifierKey) { + this.dirListingHandleEvent.call(this.directoryListing, event); + } + break; + case 'dblclick': { + const clickedItem = this.directoryListing.modelForClick( + event as MouseEvent + ); + if (clickedItem.type === 'directory') { + this.dirListingHandleEvent.call(this.directoryListing, event); + } else { + event.preventDefault(); + event.stopPropagation(); + } + break; + } + default: + this.dirListingHandleEvent.call(this.directoryListing, event); + break; + } + } +} + +export const showBrowseFileDialog = ( + manager: IDocumentManager, + options: IBrowseFileDialogOptions +): Promise> => { + const dialog = new Dialog({ + title: 'Select a file', + body: new BrowseFileDialog({ + manager: manager, + filter: options.filter, + multiselect: options.multiselect, + includeDir: options.includeDir + }), + buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'Select' })] + }); + + dialog.addClass(BROWSE_FILE_CLASS); + + return dialog.launch(); +}; diff --git a/packages/ui-components/src/index.ts b/packages/ui-components/src/index.ts index 2db9d8e52..3f95d21b4 100644 --- a/packages/ui-components/src/index.ts +++ b/packages/ui-components/src/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export * from './BrowseFileDialog'; export * from './ExpandableErrorDialog'; export * from './ExpandableComponent'; export * from './FormDialog'; diff --git a/packages/ui-components/style/index.css b/packages/ui-components/style/index.css index e745583c7..455d36ff6 100644 --- a/packages/ui-components/style/index.css +++ b/packages/ui-components/style/index.css @@ -174,3 +174,8 @@ word-wrap: break-word; z-index: 999; } + +.elyra-browseFileDialog .jp-Dialog-content { + height: 400px; + width: 600px; +} From d9c78c116bf97b9d65a0f3f2e2fa4905f2e90c7a Mon Sep 17 00:00:00 2001 From: va barbosa Date: Mon, 17 Aug 2020 12:59:23 -0400 Subject: [PATCH 2/8] Update button for consistency across browser --- packages/pipeline-editor/style/canvas.css | 9 +++++++++ packages/pipeline-editor/style/index.css | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/pipeline-editor/style/canvas.css b/packages/pipeline-editor/style/canvas.css index 3589183cc..2d745414d 100644 --- a/packages/pipeline-editor/style/canvas.css +++ b/packages/pipeline-editor/style/canvas.css @@ -323,6 +323,15 @@ body[data-jp-theme-light='false'] .bx--modal.is-visible { .bx--modal-footer .bx--btn--secondary { background: var(--md-grey-500); } +.bx--modal-content .bx--btn--tertiary { + border-color: var(--md-blue-500); + color: var(--md-blue-500); + margin-top: 0.6rem; +} +.bx--modal-content .bx--btn--tertiary:hover { + background: var(--md-blue-500); + color: #ffffff; +} .bx--modal-footer .bx--btn:disabled { background-color: var(--jp-layout-color3); opacity: 0.3; diff --git a/packages/pipeline-editor/style/index.css b/packages/pipeline-editor/style/index.css index 73052633f..ba5d424c2 100644 --- a/packages/pipeline-editor/style/index.css +++ b/packages/pipeline-editor/style/index.css @@ -121,6 +121,7 @@ td { } [data-id='properties-nodeBrowseFileAction'] { - flex-direction: row; + flex-direction: column; justify-content: end; + align-items: flex-end; } From c77d5a27693493da1049893a25d0b1c68d531a71 Mon Sep 17 00:00:00 2001 From: va barbosa Date: Mon, 17 Aug 2020 13:19:31 -0400 Subject: [PATCH 3/8] Update browse button css --- packages/pipeline-editor/style/canvas.css | 1 - packages/pipeline-editor/style/index.css | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/pipeline-editor/style/canvas.css b/packages/pipeline-editor/style/canvas.css index 2d745414d..7506f2549 100644 --- a/packages/pipeline-editor/style/canvas.css +++ b/packages/pipeline-editor/style/canvas.css @@ -326,7 +326,6 @@ body[data-jp-theme-light='false'] .bx--modal.is-visible { .bx--modal-content .bx--btn--tertiary { border-color: var(--md-blue-500); color: var(--md-blue-500); - margin-top: 0.6rem; } .bx--modal-content .bx--btn--tertiary:hover { background: var(--md-blue-500); diff --git a/packages/pipeline-editor/style/index.css b/packages/pipeline-editor/style/index.css index ba5d424c2..c74d9039c 100644 --- a/packages/pipeline-editor/style/index.css +++ b/packages/pipeline-editor/style/index.css @@ -125,3 +125,6 @@ td { justify-content: end; align-items: flex-end; } +[data-id='properties-nodeBrowseFileAction'] .bx--btn--tertiary { + margin-bottom: 0.6rem; +} From abab7e7f79629a1863805530bd1b1b812e50fa58 Mon Sep 17 00:00:00 2001 From: va barbosa Date: Mon, 17 Aug 2020 13:56:41 -0400 Subject: [PATCH 4/8] Improve browse button layout --- packages/pipeline-editor/src/properties.json | 25 +++++++------------- packages/pipeline-editor/style/index.css | 15 +++++++----- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/pipeline-editor/src/properties.json b/packages/pipeline-editor/src/properties.json index 266b1f3cc..77a7d7d39 100644 --- a/packages/pipeline-editor/src/properties.json +++ b/packages/pipeline-editor/src/properties.json @@ -105,23 +105,14 @@ "type": "panels", "group_info": [ { - "id": "browseFilePanel", - "type": "columnPanel", - "label": { - "default": "Browse File Panel" - }, - "group_info": [ - { - "id": "nodeFileControl", - "type": "controls", - "parameter_refs": ["filename"] - }, - { - "id": "nodeBrowseFileAction", - "type": "actionPanel", - "action_refs": ["browse_file"] - } - ] + "id": "nodeFileControl", + "type": "controls", + "parameter_refs": ["filename"] + }, + { + "id": "nodeBrowseFileAction", + "type": "actionPanel", + "action_refs": ["browse_file"] }, { "id": "nodePropertiesControls", diff --git a/packages/pipeline-editor/style/index.css b/packages/pipeline-editor/style/index.css index c74d9039c..a9bf3a948 100644 --- a/packages/pipeline-editor/style/index.css +++ b/packages/pipeline-editor/style/index.css @@ -120,11 +120,14 @@ td { padding-right: 10px; } -[data-id='properties-nodeBrowseFileAction'] { - flex-direction: column; - justify-content: end; - align-items: flex-end; +.properties-control-panel[data-id='properties-nodeGroupInfo'] { + position: relative; } -[data-id='properties-nodeBrowseFileAction'] .bx--btn--tertiary { - margin-bottom: 0.6rem; +.properties-control-panel[data-id='properties-nodeFileControl'] { + width: calc(100% - 100px); +} +.properties-action-panel[data-id='properties-nodeBrowseFileAction'] { + position: absolute; + right: 0; + top: 0.6rem; } From 9d843ad1d2ce9d1d70d83ea091651c1773183dad Mon Sep 17 00:00:00 2001 From: va barbosa Date: Mon, 17 Aug 2020 16:27:43 -0400 Subject: [PATCH 5/8] Resolve maximum call stack error --- packages/pipeline-editor/style/index.css | 4 ++++ packages/ui-components/src/BrowseFileDialog.tsx | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/pipeline-editor/style/index.css b/packages/pipeline-editor/style/index.css index a9bf3a948..cef1c85d5 100644 --- a/packages/pipeline-editor/style/index.css +++ b/packages/pipeline-editor/style/index.css @@ -131,3 +131,7 @@ td { right: 0; top: 0.6rem; } + +body.elyra-browseFileDialog-open .properties-modal { + display: none; +} diff --git a/packages/ui-components/src/BrowseFileDialog.tsx b/packages/ui-components/src/BrowseFileDialog.tsx index 9ac9c5ea1..89ae1ebfe 100644 --- a/packages/ui-components/src/BrowseFileDialog.tsx +++ b/packages/ui-components/src/BrowseFileDialog.tsx @@ -24,6 +24,7 @@ import { import { Widget, PanelLayout } from '@lumino/widgets'; const BROWSE_FILE_CLASS = 'elyra-browseFileDialog'; +const BROWSE_FILE_OPEN_CLASS = 'elyra-browseFileDialog-open'; export interface IBrowseFileDialogOptions { filter?: (model: any) => boolean; @@ -140,6 +141,12 @@ export const showBrowseFileDialog = ( }); dialog.addClass(BROWSE_FILE_CLASS); + document.body.className += ` ${BROWSE_FILE_OPEN_CLASS}`; - return dialog.launch(); + return dialog.launch().then((result: any) => { + document.body.className = document.body.className + .replace(BROWSE_FILE_OPEN_CLASS, '') + .trim(); + return result; + }); }; From 44794a15eb68efb137e73521ff52ef624b91142b Mon Sep 17 00:00:00 2001 From: va barbosa Date: Mon, 17 Aug 2020 17:01:22 -0400 Subject: [PATCH 6/8] Enable select file with doubleclick --- .../ui-components/src/BrowseFileDialog.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/ui-components/src/BrowseFileDialog.tsx b/packages/ui-components/src/BrowseFileDialog.tsx index 89ae1ebfe..fbcc8b440 100644 --- a/packages/ui-components/src/BrowseFileDialog.tsx +++ b/packages/ui-components/src/BrowseFileDialog.tsx @@ -30,6 +30,7 @@ export interface IBrowseFileDialogOptions { filter?: (model: any) => boolean; multiselect?: boolean; includeDir?: boolean; + acceptFileOnDblClick?: boolean; } /** @@ -42,6 +43,7 @@ export class BrowseFileDialog extends Widget dirListingHandleEvent: (event: Event) => void; multiselect: boolean; includeDir: boolean; + acceptFileOnDblClick: boolean; constructor(props: any) { super(props); @@ -57,6 +59,7 @@ export class BrowseFileDialog extends Widget model: model }); + this.acceptFileOnDblClick = props.acceptFileOnDblClick; this.multiselect = props.multiselect; this.includeDir = props.includeDir; this.dirListingHandleEvent = this.directoryListing.handleEvent; @@ -115,6 +118,14 @@ export class BrowseFileDialog extends Widget } else { event.preventDefault(); event.stopPropagation(); + if (this.acceptFileOnDblClick) { + const okButton = document.querySelector( + `.${BROWSE_FILE_OPEN_CLASS} .jp-mod-accept` + ); + if (okButton) { + (okButton as HTMLButtonElement).click(); + } + } } break; } @@ -135,7 +146,13 @@ export const showBrowseFileDialog = ( manager: manager, filter: options.filter, multiselect: options.multiselect, - includeDir: options.includeDir + includeDir: options.includeDir, + acceptFileOnDblClick: Object.prototype.hasOwnProperty.call( + options, + 'acceptFileOnDblClick' + ) + ? options.acceptFileOnDblClick + : true }), buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'Select' })] }); From 24ec48148740a362ffd2453f8d27c2af8d96fade Mon Sep 17 00:00:00 2001 From: va barbosa Date: Mon, 17 Aug 2020 18:36:09 -0400 Subject: [PATCH 7/8] Move get filename to util function --- .../src/PipelineEditorWidget.tsx | 10 ++-------- packages/pipeline-editor/src/utils.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/pipeline-editor/src/PipelineEditorWidget.tsx b/packages/pipeline-editor/src/PipelineEditorWidget.tsx index 02a45460a..7127fdc8f 100644 --- a/packages/pipeline-editor/src/PipelineEditorWidget.tsx +++ b/packages/pipeline-editor/src/PipelineEditorWidget.tsx @@ -466,9 +466,7 @@ export class PipelineEditor extends React.Component< if (app_data.filename !== propertySet.filename) { app_data.filename = propertySet.filename; - node.label = propertySet.filename - .replace(/^.*[\\/]/, '') - .replace(/\.[^/.]+$/, ''); + node.label = Utils.getFilenameFromPath(propertySet.filename, true); } app_data.runtime_image = propertySet.runtime_image; @@ -714,11 +712,7 @@ export class PipelineEditor extends React.Component< str => str + '=' ); - data.nodeTemplate.label = item.path.replace(/^.*[\\/]/, ''); - data.nodeTemplate.label = data.nodeTemplate.label.replace( - /\.[^/.]+$/, - '' - ); + data.nodeTemplate.label = Utils.getFilenameFromPath(item.path, true); data.nodeTemplate.image = IconUtil.encode(notebookIcon); data.nodeTemplate.app_data['filename'] = item.path; data.nodeTemplate.app_data[ diff --git a/packages/pipeline-editor/src/utils.ts b/packages/pipeline-editor/src/utils.ts index 1b639f56c..baa5aaf15 100644 --- a/packages/pipeline-editor/src/utils.ts +++ b/packages/pipeline-editor/src/utils.ts @@ -146,4 +146,23 @@ export default class Utils { this.deletePipelineAppdataField(node, currentFieldName); } } + + /** + * Return the last portion (filename) of a path. The file extension is + * stripped if `excludeExtension` is set to true. + * + * @param path + * @param excludeExtension + */ + static getFilenameFromPath(path: string, excludeExtension?: boolean): string { + // strip directory prefixes + let filename = path.replace(/^.*[\\/]/, ''); + + if (excludeExtension) { + //strip file extension + filename = filename.replace(/\.[^/.]+$/, ''); + } + + return filename; + } } From 4dfe3be0a09a5964f7d5e9c6b55e888571b72314 Mon Sep 17 00:00:00 2001 From: va barbosa Date: Mon, 17 Aug 2020 19:07:14 -0400 Subject: [PATCH 8/8] address review comments --- .../src/PipelineEditorWidget.tsx | 10 ++++++++-- packages/pipeline-editor/src/utils.ts | 19 ------------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/packages/pipeline-editor/src/PipelineEditorWidget.tsx b/packages/pipeline-editor/src/PipelineEditorWidget.tsx index 7127fdc8f..81d6f7716 100644 --- a/packages/pipeline-editor/src/PipelineEditorWidget.tsx +++ b/packages/pipeline-editor/src/PipelineEditorWidget.tsx @@ -466,7 +466,10 @@ export class PipelineEditor extends React.Component< if (app_data.filename !== propertySet.filename) { app_data.filename = propertySet.filename; - node.label = Utils.getFilenameFromPath(propertySet.filename, true); + node.label = path.basename( + propertySet.filename, + path.extname(propertySet.filename) + ); } app_data.runtime_image = propertySet.runtime_image; @@ -712,7 +715,10 @@ export class PipelineEditor extends React.Component< str => str + '=' ); - data.nodeTemplate.label = Utils.getFilenameFromPath(item.path, true); + data.nodeTemplate.label = path.basename( + item.path, + path.extname(item.path) + ); data.nodeTemplate.image = IconUtil.encode(notebookIcon); data.nodeTemplate.app_data['filename'] = item.path; data.nodeTemplate.app_data[ diff --git a/packages/pipeline-editor/src/utils.ts b/packages/pipeline-editor/src/utils.ts index baa5aaf15..1b639f56c 100644 --- a/packages/pipeline-editor/src/utils.ts +++ b/packages/pipeline-editor/src/utils.ts @@ -146,23 +146,4 @@ export default class Utils { this.deletePipelineAppdataField(node, currentFieldName); } } - - /** - * Return the last portion (filename) of a path. The file extension is - * stripped if `excludeExtension` is set to true. - * - * @param path - * @param excludeExtension - */ - static getFilenameFromPath(path: string, excludeExtension?: boolean): string { - // strip directory prefixes - let filename = path.replace(/^.*[\\/]/, ''); - - if (excludeExtension) { - //strip file extension - filename = filename.replace(/\.[^/.]+$/, ''); - } - - return filename; - } }