import { Plugin , WorkspaceLeaf , addIcon , TextFileView , Setting , MarkdownRenderer , MarkdownSourceView , MarkdownView , setIcon , ToggleComponent } from 'obsidian' ; import * as Papa from 'papaparse' ; import Handsontable from "handsontable" ; import 'handsontable/dist/handsontable.full.min.css' ; import './styles.scss' ; export default class CsvPlugin extends Plugin { async onload ( ) { this . addDocumentIcon ( "csv" ) ; this . registerView ( "csv" , this . csvViewCreator ) ; this . registerExtensions ( [ "csv" ] , "csv" ) ; } csvViewCreator = ( leaf ) => { return new CsvView ( leaf ) ; } ; addDocumentIcon = ( extension ) => { addIcon ( `document-${ extension }` , `
  <path fill="currentColor" stroke="currentColor" d="M14,4v92h72V29.2l-0.6-0.6l-24-24L60.8,4L14,4z M18,8h40v24h24v60H18L18,8z M62,10.9L79.1,28H62V10.9z"></path>
  <text font-family="sans-serif" font-weight="bold" font-size="30" fill="currentColor" x="50%" y="60%" dominant-baseline="middle" text-anchor="middle">
    ${ extension }
  </text>
    ` ) ; } ; } class ExtHandsontable extends Handsontable { extContext : any ; constructor ( element , options , context ) { super ( element , options ) ; this . extContext = context ; } } class CsvView extends TextFileView { parseResult : any ; headerToggle : ToggleComponent ; headers : string [ ] = null ; fileOptionsEl : HTMLElement ; hot : Handsontable ; hotSettings : Handsontable . GridSettings ; hotExport : Handsontable . plugins . ExportFile ; hotState : Handsontable . plugins . PersistenState ; hotFilters : Handsontable . plugins . Filters ; loadingBar : HTMLElement ; public get extContentEl ( ) : HTMLElement { return this . contentEl ; } constructor ( leaf ) { super ( leaf ) ; this . onResize = ( ) => { this . hot . view . wt . wtOverlays . updateMainScrollableElements ( ) ; this . hot . render ( ) ; } ; this . loadingBar = document . createElement ( 'div' ) ; this . loadingBar . addClass ( "progress-bar" ) ; this . loadingBar . innerHTML = `<div class="progress-bar-message u-center-text">Loading CSV...</div><div class="progress-bar-indicator"><div class="progress-bar-line"></div><div class="progress-bar-subline" style="display: none;"></div><div class="progress-bar-subline mod-increase"></div><div class="progress-bar-subline mod-decrease"></div></div>` ; this . extContentEl . appendChild ( this . loadingBar ) ; this . fileOptionsEl = document . createElement ( 'div' ) ; this . fileOptionsEl . classList . add ( 'csv-controls' ) ; this . extContentEl . appendChild ( this . fileOptionsEl ) ; new Setting ( this . fileOptionsEl ) . setName ( 'File Includes Headers' ) . addToggle ( toggle => { this . headerToggle = toggle ; toggle . setValue ( false ) . onChange ( this . toggleHeaders ) ; } ) ; const tableContainer = document . createElement ( 'div' ) ; tableContainer . classList . add ( 'csv-table-wrapper' ) ; this . extContentEl . appendChild ( tableContainer ) ; const hotContainer = document . createElement ( 'div' ) ; tableContainer . appendChild ( hotContainer ) ; Handsontable . renderers . registerRenderer ( 'markdown' , this . markdownCellRenderer ) ; Handsontable . editors . registerEditor ( 'markdown' , MarkdownCellEditor ) ; this . hotSettings = { afterChange : this . hotChange , afterColumnSort : this . requestSave , afterColumnMove : this . requestSave , afterRowMove : this . requestSave , afterCreateCol : this . requestSave , afterCreateRow : this . requestSave , afterRemoveCol : this . requestSave , afterRemoveRow : this . requestSave , licenseKey : 'non-commercial-and-evaluation' , colHeaders : true , rowHeaders : true , autoColumnSize : true , autoRowSize : true , renderer : 'markdown' , editor : 'markdown' , className : 'csv-table' , contextMenu : true , currentRowClassName : 'active-row' , currentColClassName : 'active-col' , columnSorting : true , dropdownMenu : true , filters : true , manualColumnFreeze : true , manualColumnMove : false , manualColumnResize : true , manualRowMove : false , manualRowResize : true , persistentState : true , search : true , height : '100%' , width : '100%' , } ; this . hot = new ExtHandsontable ( hotContainer , this . hotSettings , { leaf : this . leaf } ) ; this . hotExport = this . hot . getPlugin ( 'exportFile' ) ; this . hotState = this . hot . getPlugin ( 'persistentState' ) ; this . hotFilters = this . hot . getPlugin ( 'filters' ) ; } hotChange = ( changes , source ) => { if ( source === 'loadData' ) { return ; } this . requestSave ( ) ; } ; toggleHeaders = ( value ) => { value = value || false ; if ( value ) { if ( this . hotSettings . colHeaders === true ) { let data = this . hot . getSourceDataArray ( ) ; this . hotSettings . colHeaders = data . shift ( ) ; this . hot . loadData ( data ) ; this . hot . updateSettings ( this . hotSettings ) ; } } else { if ( this . hotSettings . colHeaders !== true ) { let data = this . hot . getSourceDataArray ( ) ; data . unshift ( this . hot . getColHeader ( ) ) ; this . hotSettings . colHeaders = true ; this . hot . loadData ( data ) ; this . hot . updateSettings ( this . hotSettings ) ; } } this . hotState . saveValue ( 'hasHeadings' , value ) ; } ; getViewData = ( ) => { let data = this . hot . getSourceDataArray ( ) ; if ( this . hotSettings . colHeaders !== true ) { data . unshift ( this . hot . getColHeader ( ) ) ; } let csvString = Papa . unparse ( data ) ; return csvString ; } ; setViewData = ( data , clear ) => { this . loadingBar . show ( ) ; setTimeout ( ( ) => this . loadDataAsync ( data ) . then ( ( ) => this . loadingBar . hide ( ) ) , 50 ) ; } ; loadDataAsync = async ( data ) => { return new Promise < void > ( ( resolve , reject ) => { this . hot . rootElement . id = this . file . path ; this . hotSettings . colHeaders = true ; if ( data . charCodeAt ( 0 ) === 0xFEFF ) data = data . slice ( 1 ) ; Papa . parse ( data , { header : false , complete : results => { this . parseResult = results ; this . hot . loadData ( this . parseResult . data ) ; this . hot . updateSettings ( this . hotSettings ) ; let hasHeadings = { value : false } ; this . hotState . loadValue ( 'hasHeadings' , hasHeadings ) ; this . headerToggle . setValue ( hasHeadings . value ) ; this . toggleHeaders ( hasHeadings . value ) ; resolve ( ) ; } , error : ( error ) => { reject ( error ) ; } } ) ; } ) ; } ; markdownCellRenderer = ( instance , TD , row , col , prop , value , cellProperties ) : HTMLTableCellElement | void => { TD . innerHTML = '' ; MarkdownRenderer . renderMarkdown ( value , TD , this . file . path || '' , this || null ) ; return TD ; } ; clear = ( ) => { } ; getDisplayText ( ) { if ( this . file ) return this . file . basename ; else return "csv (no file)" ; } canAcceptExtension ( extension ) { return extension == 'csv' ; } getViewType ( ) { return "csv" ; } getIcon ( ) { return "document-csv" ; } } class MarkdownCellEditor extends Handsontable . editors . BaseEditor { eGui : HTMLElement ; view : MarkdownView ; init ( ) { const extContext = ( this . hot as ExtHandsontable ) . extContext ; if ( extContext && extContext . leaf && ! this . eGui ) { this . eGui = this . hot . rootDocument . createElement ( 'DIV' ) ; Handsontable . dom . addClass ( this . eGui , 'htMarkdownEditor' ) ; Handsontable . dom . addClass ( this . eGui , 'csv-cell-edit' ) ; this . view = new MarkdownView ( extContext . leaf ) ; this . view . currentMode = this . view . sourceMode ; this . eGui . appendChild ( this . view . sourceMode . editorEl ) ; this . eGui . style . display = 'none' ; this . hot . rootElement . appendChild ( this . eGui ) ; } } open ( event ? ) { this . refreshDimensions ( ) ; this . eGui . show ( ) ; this . view . sourceMode . cmEditor . focus ( ) ; this . view . sourceMode . cmEditor . refresh ( ) ; } refreshDimensions ( ) { this . TD = this . getEditedCell ( ) ; if ( ! this . TD ) { this . close ( ) ; return ; } const { wtOverlays } = this . hot . view . wt ; const currentOffset = Handsontable . dom . offset ( this . TD ) ; const containerOffset = Handsontable . dom . offset ( this . hot . rootElement ) ; const scrollableContainer = wtOverlays . scrollableElement ; const editorSection = this . checkEditorSection ( ) ; let width = Handsontable . dom . outerWidth ( this . TD ) + 1 ; let height = Handsontable . dom . outerHeight ( this . TD ) + 1 ; let editTop = currentOffset . top - containerOffset . top - 1 - ( scrollableContainer . scrollTop || 0 ) ; let editLeft = currentOffset . left - containerOffset . left - 1 - ( scrollableContainer . scrollLeft || 0 ) ; let cssTransformOffset ; switch ( editorSection ) { case 'top' : cssTransformOffset = Handsontable . dom . getCssTransform ( wtOverlays . topOverlay . clone . wtTable . holder . parentNode ) ; break ; case 'left' : cssTransformOffset = Handsontable . dom . getCssTransform ( wtOverlays . leftOverlay . clone . wtTable . holder . parentNode ) ; break ; case 'top-left-corner' : cssTransformOffset = Handsontable . dom . getCssTransform ( wtOverlays . topLeftCornerOverlay . clone . wtTable . holder . parentNode ) ; break ; case 'bottom-left-corner' : cssTransformOffset = Handsontable . dom . getCssTransform ( wtOverlays . bottomLeftCornerOverlay . clone . wtTable . holder . parentNode ) ; break ; case 'bottom' : cssTransformOffset = Handsontable . dom . getCssTransform ( wtOverlays . bottomOverlay . clone . wtTable . holder . parentNode ) ; break ; default : break ; } if ( this . hot . getSelectedLast ( ) [ 0 ] === 0 ) { editTop += 1 ; } if ( this . hot . getSelectedLast ( ) [ 1 ] === 0 ) { editLeft += 1 ; } const selectStyle = this . eGui . style ; if ( cssTransformOffset && cssTransformOffset !== - 1 ) { selectStyle [ cssTransformOffset [ 0 ] ] = cssTransformOffset [ 1 ] ; } else { Handsontable . dom . resetCssTransform ( this . eGui ) ; } const cellComputedStyle = Handsontable . dom . getComputedStyle ( this . TD , this . hot . rootWindow ) ; if ( parseInt ( cellComputedStyle . borderTopWidth , 10 ) > 0 ) { height -= 1 ; } if ( parseInt ( cellComputedStyle . borderLeftWidth , 10 ) > 0 ) { width -= 1 ; } selectStyle . height = `${ height }px` ; selectStyle . minWidth = `${ width }px` ; selectStyle . maxWidth = `${ width }px` ; selectStyle . top = `${ editTop }px` ; selectStyle . left = `${ editLeft }px` ; selectStyle . margin = '0px' ; } getEditedCell ( ) { const { wtOverlays } = this . hot . view . wt ; const editorSection = this . checkEditorSection ( ) ; let editedCell ; switch ( editorSection ) { case 'top' : editedCell = wtOverlays . topOverlay . clone . wtTable . getCell ( { row : this . row , col : this . col } ) ; this . eGui . style . zIndex = '101' ; break ; case 'top-left-corner' : case 'bottom-left-corner' : editedCell = wtOverlays . topLeftCornerOverlay . clone . wtTable . getCell ( { row : this . row , col : this . col } ) ; this . eGui . style . zIndex = '103' ; break ; case 'left' : editedCell = wtOverlays . leftOverlay . clone . wtTable . getCell ( { row : this . row , col : this . col } ) ; this . eGui . style . zIndex = '102' ; break ; default : editedCell = this . hot . getCell ( this . row , this . col ) ; this . eGui . style . zIndex = '' ; break ; } return editedCell < 0 ? void 0 : editedCell ; } close ( ) { this . eGui . hide ( ) ; } focus ( ) { this . view . sourceMode . cmEditor . focus ( ) ; this . view . sourceMode . cmEditor . refresh ( ) ; } getValue ( ) { return this . view . sourceMode . get ( ) ; } setValue ( newValue ? ) { this . view . sourceMode . set ( newValue , true ) ; } }