const deepmerge = require('../helpers/merge')
const { DataDefaultScopedSlotProps, DataOptions } = require('./v-data')
const { DataIteratorEvents, DataIteratorProps, DataIteratorSlots, DataIteratorItemScopedProps } = require('./v-data-iterator')
const { DataFooterPageTextScopedProps } = require('./v-data-footer')

const DataTableHeader = {
  text: 'string',
  value: 'string',
  'align?': `'start' | 'center' | 'end'`,
  'sortable?': 'boolean',
  'filterable?': 'boolean',
  'groupable?': 'boolean',
  'divider?': 'boolean',
  'class?': 'string | string[]',
  'cellClass?': 'string | string[]',
  'width?': 'string | number',
  'filter?': '(value: any, search: string, item: any) => boolean',
  'sort?': '(a: any, b: any) => number',
}

const dataString = `{
  expand: (value: boolean) => void,
  headers: DataTableHeader[],
  isExpanded: boolean,
  isMobile: boolean,
  isSelected: boolean,
  item: any,
  select: (value: boolean) => void
}`
const DataTableEvents = [
  {
    name: 'click:row',
    source: 'v-data-table',
    value: `any, ${dataString}`,
  },
  {
    name: 'contextmenu:row',
    source: 'v-data-table',
    value: `MouseEvent, ${dataString}`,
  },
  {
    name: 'dblclick:row',
    source: 'v-data-table',
    value: `MouseEvent, ${dataString}`,
  },
].concat(DataIteratorEvents)

const DataTableHeaderScopedProps = {
  isMobile: 'boolean',
  props: {
    headers: 'DataTableHeader[]',
    options: DataOptions,
    mobile: 'boolean',
    showGroupBy: 'boolean',
    someItems: 'boolean',
    everyItem: 'boolean',
  },
  on: {
    sort: DataDefaultScopedSlotProps.sort,
    group: DataDefaultScopedSlotProps.group,
    'toggle-select-all': '(value: boolean) => void',
  },
}

const DataTableFooterScopedProps = {
  props: {
    options: DataOptions,
    pagination: DataDefaultScopedSlotProps.pagination,
    itemsPerPageText: 'string',
  },
  on: '{}',
  headers: 'DataTableHeader[]',
  widths: '[]',
}

const DataTableHeaderColumnScopedProps = {
  header: 'DataTableHeader',
}

const DataTableItemScopedProps = {
  ...DataIteratorItemScopedProps,
  headers: 'DataTableHeader[]',
  isMobile: 'boolean',
}

const DataTableItemColumnScopedProps = {
  isMobile: 'boolean',
  item: 'any',
  header: 'DataTableHeader',
  value: 'any',
}

const DataTableHeaderSelectScopedProps = {
  isMobile: 'boolean',
  props: {
    value: 'boolean',
    indeterminate: 'boolean',
  },
  on: {
    input: '(value: boolean) => void',
  },
}

const DataTableExpandedItemScopedProps = {
  isMobile: 'boolean',
  item: 'any',
  headers: 'DataTableHeader[]',
}

const DataTableBodyScopedProps = {
  ...DataDefaultScopedSlotProps,
  headers: 'DataTableHeader[]',
  isMobile: 'boolean',
  isSelected: '(item: any) => boolean',
  select: '(item: any, value: boolean) => void',
  isExpanded: '(item: any) => boolean',
  expand: '(item: any, value: boolean) => void',
}

const DataGroupScopedProps = {
  group: 'string',
  options: DataOptions,
  isMobile: 'boolean',
  items: 'any[]',
  headers: 'i DataTableHeader[]',
}

const DataGroupHeaderScopedProps = {
  group: 'string',
  groupBy: DataOptions.groupBy,
  isMobile: 'boolean',
  items: 'any[]',
  headers: 'DataTableHeader[]',
  isOpen: 'boolean',
  toggle: '() => void',
  remove: '() => void',
}

const DataGroupSummaryScopedProps = {
  group: 'string',
  groupBy: DataOptions.groupBy,
  isMobile: 'boolean',
  items: 'any[]',
  headers: 'DataTableHeader[]',
  isOpen: 'boolean',
  toggle: '() => void',
}

const DataTableSlots = [
  { name: 'body.append', props: DataTableBodyScopedProps },
  { name: 'body.prepend', props: DataTableBodyScopedProps },
  { name: 'body', props: DataTableBodyScopedProps },
  { name: 'foot', props: DataDefaultScopedSlotProps },
  { name: 'footer', props: DataTableFooterScopedProps },
  { name: 'footer.prepend' },
  { name: 'footer.page-text', props: DataFooterPageTextScopedProps },
  { name: 'header', props: DataTableHeaderScopedProps },
  { name: 'header.data-table-select', props: DataTableHeaderSelectScopedProps },
  { name: 'header.<name>', props: DataTableHeaderColumnScopedProps },
  { name: 'top', props: { ...DataDefaultScopedSlotProps, isMobile: 'boolean' } },
  { name: 'progress' },
  { name: 'group', props: DataGroupScopedProps },
  { name: 'group.header', props: DataGroupHeaderScopedProps },
  { name: 'group.summary', props: DataGroupSummaryScopedProps },
  { name: 'item', props: DataTableItemScopedProps },
  { name: 'item.data-table-select', props: DataTableItemScopedProps },
  { name: 'item.data-table-expand', props: DataTableItemScopedProps },
  { name: 'item.<name>', props: DataTableItemColumnScopedProps },
  { name: 'expanded-item', props: DataTableExpandedItemScopedProps },
  ...DataIteratorSlots.filter(slot => !['default'].includes(slot.name)),
]

module.exports = {
  'v-data-table': {
    props: deepmerge(DataIteratorProps, [
      {
        name: 'headers',
        type: 'DataTableHeader[]',
        example: DataTableHeader,
      },
      {
        name: 'customFilter',
        default: 'gh:defaultFilter',
        example: '(value: any, search: string | null, item: any) => boolean',
      },
    ]),
    slots: DataTableSlots,
    events: DataTableEvents,
  },
  DataTableHeader,
  DataTableEvents,
  DataTableHeaderScopedProps,
  DataTableSlots,
}


@mixin background-text-color($color_name, $color_type) {

$map_value: map-deep-get(colors.$text-on-colors, $color_name, $color_type);

Applies a **2px** by default and **1.5px** border around the badge when using the **dot** property.

Text content to show in the badge.

Reduce the size of the badge and hide its contents.

Designates a specific icon to render in the badge.

Moves the badge to be inline with the wrapping element. Supports the usage of the **left** prop.

Overlaps the slotted content on top of the component.

Sets the maximum number allowed when using the **content** prop with a `number` like value. If the content number exceeds the maximum value, a `+` suffix is added.

Changes the location of the badge. Possible values are **top-right**, **bottom-right**, **bottom-left**, **top-left**.

Defines the background color of the badge.


@each $color_name, $color_color in colors.$colors {

@each $color_type, $color_value in $color_color {

keep keep keep replace replace replace replace keep keep keep keep

// Utilities
import {
  computed,
  inject,
  provide,
  reactive,
  ref,
  watch,
  watchEffect,
} from 'vue'
import {
  colorToInt,
  colorToRGB,
  createRange,
  darken,
  getCurrentInstance,
  getLuma,
  IN_BROWSER,
  intToHex,
  lighten,
  mergeDeep,
  propsFactory,
} from '@/util'
import { APCAcontrast } from '@/util/color/APCA'

// Types
import type { App, DeepReadonly, InjectionKey, Ref } from 'vue'
import type { HeadAttrs, HeadClient } from '@vueuse/head'

type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T

export type ThemeOptions = false | {
  cspNonce?: string
  defaultTheme?: string
  variations?: false | VariationsOptions
  themes?: Record<string, ThemeDefinition>
}
export type ThemeDefinition = DeepPartial<InternalThemeDefinition>

interface InternalThemeOptions {
  cspNonce?: string
  isDisabled: boolean
  defaultTheme: string
  variations: false | VariationsOptions
  themes: Record<string, InternalThemeDefinition>
}

interface VariationsOptions {
  colors: string[]
  lighten: number
  darken: number
}

interface InternalThemeDefinition {
  dark: boolean
  colors: Colors
  variables: Record<string, string | number>
}

export interface Colors extends BaseColors, OnColors {
  [key: string]: string
}

interface BaseColors {
  background: string
  surface: string
  primary: string
  secondary: string
  success: string
  warning: string
  error: string
  info: string
}

interface OnColors {
  'on-background': string
  'on-surface': string
  'on-primary': string
  'on-secondary': string
  'on-success': string
  'on-warning': string
  'on-error': string
  'on-info': string
}

export interface ThemeInstance {
  readonly isDisabled: boolean
  readonly themes: Ref<Record<string, InternalThemeDefinition>>

  readonly name: Readonly<Ref<string>>
  readonly current: DeepReadonly<Ref<InternalThemeDefinition>>
  readonly computedThemes: DeepReadonly<Ref<Record<string, InternalThemeDefinition>>>

  readonly themeClasses: Readonly<Ref<string | undefined>>
  readonly styles: Readonly<Ref<string>>

  readonly global: {
    readonly name: Ref<string>
    readonly current: DeepReadonly<Ref<InternalThemeDefinition>>
  }
}

export const ThemeSymbol: InjectionKey<ThemeInstance> = Symbol.for('vuetify:theme')

export const makeThemeProps = propsFactory({
  theme: String,
}, 'theme')

const defaultThemeOptions: Exclude<ThemeOptions, false> = {
  defaultTheme: 'light',
  variations: { colors: [], lighten: 0, darken: 0 },
  themes: {
    light: {
      dark: false,
      colors: {
        background: '#FFFFFF',
        surface: '#FFFFFF',
        'surface-variant': '#424242',
        'on-surface-variant': '#EEEEEE',
        primary: '#6200EE',
        'primary-darken-1': '#3700B3',
        secondary: '#03DAC6',
        'secondary-darken-1': '#018786',
        error: '#B00020',
        info: '#2196F3',
        success: '#4CAF50',
        warning: '#FB8C00',
      },
      variables: {
        'border-color': '#000000',
        'border-opacity': 0.12,
        'high-emphasis-opacity': 0.87,
        'medium-emphasis-opacity': 0.60,
        'disabled-opacity': 0.38,
        'idle-opacity': 0.04,
        'hover-opacity': 0.04,
        'focus-opacity': 0.12,
        'selected-opacity': 0.08,
        'activated-opacity': 0.12,
        'pressed-opacity': 0.12,
        'dragged-opacity': 0.08,
        'kbd-background-color': '#212529',
        'kbd-color': '#FFFFFF',
        'code-background-color': '#C2C2C2',
      },
    },
    dark: {
      dark: true,
      colors: {
        background: '#121212',
        surface: '#212121',
        'surface-variant': '#BDBDBD',
        'on-surface-variant': '#424242',
        primary: '#BB86FC',
        'primary-darken-1': '#3700B3',
        secondary: '#03DAC5',
        'secondary-darken-1': '#03DAC5',
        error: '#CF6679',
        info: '#2196F3',
        success: '#4CAF50',
        warning: '#FB8C00',
      },
      variables: {
        'border-color': '#FFFFFF',
        'border-opacity': 0.12,
        'high-emphasis-opacity': 0.87,
        'medium-emphasis-opacity': 0.60,
        'disabled-opacity': 0.38,
        'idle-opacity': 0.10,
        'hover-opacity': 0.04,
        'focus-opacity': 0.12,
        'selected-opacity': 0.08,
        'activated-opacity': 0.12,
        'pressed-opacity': 0.16,
        'dragged-opacity': 0.08,
        'kbd-background-color': '#212529',
        'kbd-color': '#FFFFFF',
        'code-background-color': '#B7B7B7',
      },
    },
  },
}

function parseThemeOptions (options: ThemeOptions = defaultThemeOptions): InternalThemeOptions {
  if (!options) return { ...defaultThemeOptions, isDisabled: true } as InternalThemeOptions

  const themes: Record<string, InternalThemeDefinition> = {}
  for (const [key, theme] of Object.entries(options.themes ?? {})) {
    const defaultTheme = theme.dark
      ? defaultThemeOptions.themes?.dark
      : defaultThemeOptions.themes?.light
    themes[key] = mergeDeep(defaultTheme, theme) as InternalThemeDefinition
  }

  return mergeDeep(
    defaultThemeOptions,
    { ...options, themes },
  ) as InternalThemeOptions
}

// Composables
export function createTheme (app: App, options?: ThemeOptions): ThemeInstance {
  const head = app._context.provides.usehead as HeadClient | undefined
  const parsedOptions = reactive(parseThemeOptions(options))
  const name = ref(parsedOptions.defaultTheme)
  const themes = ref(parsedOptions.themes)

  const computedThemes = computed(() => {
    const acc: Record<string, InternalThemeDefinition> = {}
    for (const [name, original] of Object.entries(themes.value)) {
      const theme: InternalThemeDefinition = acc[name] = {
        ...original,
        colors: {
          ...original.colors,
        },
      }

      if (parsedOptions.variations) {
        for (const name of parsedOptions.variations.colors) {
          const color = theme.colors[name]
          for (const variation of (['lighten', 'darken'] as const)) {
            const fn = variation === 'lighten' ? lighten : darken
            for (const amount of createRange(parsedOptions.variations[variation], 1)) {
              theme.colors[`${name}-${variation}-${amount}`] = intToHex(fn(colorToInt(color), amount))
            }
          }
        }
      }

      for (const color of Object.keys(theme.colors)) {
        if (/on-[a-z]/.test(color) || theme.colors[`on-${color}`]) continue

        const onColor = `on-${color}` as keyof OnColors
        const colorVal = colorToInt(theme.colors[color]!)

        const blackContrast = Math.abs(APCAcontrast(0, colorVal))
        const whiteContrast = Math.abs(APCAcontrast(0xffffff, colorVal))

        // TODO: warn about poor color selections
        // const contrastAsText = Math.abs(APCAcontrast(colorVal, colorToInt(theme.colors.background)))
        // const minContrast = Math.max(blackContrast, whiteContrast)
        // if (minContrast < 60) {
        //   consoleInfo(`${key} theme color ${color} has poor contrast (${minContrast.toFixed()}%)`)
        // } else if (contrastAsText < 60 && !['background', 'surface'].includes(color)) {
        //   consoleInfo(`${key} theme color ${color} has poor contrast as text (${contrastAsText.toFixed()}%)`)
        // }

        // Prefer white text if both have an acceptable contrast ratio
        theme.colors[onColor] = whiteContrast > Math.min(blackContrast, 50) ? '#fff' : '#000'
      }
    }

    return acc
  })
  const current = computed(() => computedThemes.value[name.value])

  const styles = computed(() => {
    const lines: string[] = []

    if (current.value.dark) {
      createCssClass(lines, ':root', ['color-scheme: dark'])
    }

    for (const [themeName, theme] of Object.entries(computedThemes.value)) {
      const { variables, dark } = theme

      createCssClass(lines, `.v-theme--${themeName}`, [
        `color-scheme: ${dark ? 'dark' : 'normal'}`,
        ...genCssVariables(theme),
        ...Object.keys(variables).map(key => {
          const value = variables[key]
          const color = typeof value === 'string' && value.startsWith('#') ? colorToRGB(value) : undefined
          const rgb = color ? `${color.r}, ${color.g}, ${color.b}` : undefined

          return `--v-${key}: ${rgb ?? value}`
        }),
      ])
    }

    const colors = new Set(Object.values(computedThemes.value).flatMap(theme => Object.keys(theme.colors)))
    for (const key of colors) {
      if (/on-[a-z]/.test(key)) {
        createCssClass(lines, `.${key}`, [`color: rgb(var(--v-theme-${key})) !important`])
      } else {
        createCssClass(lines, `.bg-${key}`, [
          `--v-theme-overlay-multiplier: var(--v-theme-${key}-overlay-multiplier)`,
          `background: rgb(var(--v-theme-${key})) !important`,
          `color: rgb(var(--v-theme-on-${key})) !important`,
        ])
        createCssClass(lines, `.text-${key}`, [`color: rgb(var(--v-theme-${key})) !important`])
        createCssClass(lines, `.border-${key}`, [`--v-border-color: var(--v-theme-${key})`])
      }
    }

    return lines.map((str, i) => i === 0 ? str : `    ${str}`).join('')
  })

  if (head) {
    head.addHeadObjs(computed(() => {
      const style: HeadAttrs = {
        children: styles.value,
        type: 'text/css',
        id: 'vuetify-theme-stylesheet',
      }
      if (parsedOptions.cspNonce) style.nonce = parsedOptions.cspNonce

      return { style: [style] }
    }))

    if (IN_BROWSER) {
      watchEffect(() => head.updateDOM())
    }
  } else {
    let styleEl = IN_BROWSER
      ? document.getElementById('vuetify-theme-stylesheet')
      : null

    watch(styles, updateStyles, { immediate: true })
    function updateStyles () {
      if (parsedOptions.isDisabled) return

      if (typeof document !== 'undefined' && !styleEl) {
        const el = document.createElement('style')
        el.type = 'text/css'
        el.id = 'vuetify-theme-stylesheet'
        if (parsedOptions.cspNonce) el.setAttribute('nonce', parsedOptions.cspNonce)

        styleEl = el
        document.head.appendChild(styleEl)
      }

      if (styleEl) styleEl.innerHTML = styles.value
    }
  }

  const themeClasses = computed(() => parsedOptions.isDisabled ? undefined : `v-theme--${name.value}`)

  return {
    isDisabled: parsedOptions.isDisabled,
    name,
    themes,
    current,
    computedThemes,
    themeClasses,
    styles,
    global: {
      name,
      current,
    },
  }
}

export function provideTheme (props: { theme?: string }) {
  getCurrentInstance('provideTheme')

  const theme = inject(ThemeSymbol, null)

  if (!theme) throw new Error('Could not find Vuetify theme injection')

  const name = computed<string>(() => {
    return props.theme ?? theme?.name.value
  })

  const themeClasses = computed(() => theme.isDisabled ? undefined : `v-theme--${name.value}`)

  const newTheme: ThemeInstance = {
    ...theme,
    name,
    themeClasses,
  }

  provide(ThemeSymbol, newTheme)

  return newTheme
}

export function useTheme () {
  getCurrentInstance('useTheme')

  const theme = inject(ThemeSymbol, null)

  if (!theme) throw new Error('Could not find Vuetify theme injection')

  return theme
}

function createCssClass (lines: string[], selector: string, content: string[]) {
  lines.push(
    `${selector} {\n`,
    ...content.map(line => `  ${line};\n`),
    '}\n',
  )
}

function genCssVariables (theme: InternalThemeDefinition) {
  const lightOverlay = theme.dark ? 2 : 1
  const darkOverlay = theme.dark ? 1 : 2

  const variables: string[] = []
  for (const [key, value] of Object.entries(theme.colors)) {
    const rgb = colorToRGB(value)
    variables.push(`--v-theme-${key}: ${rgb.r},${rgb.g},${rgb.b}`)
    if (!key.startsWith('on-')) {
      variables.push(`--v-theme-${key}-overlay-multiplier: ${getLuma(value) > 0.18 ? lightOverlay : darkOverlay}`)
    }
  }

  return variables
}


{
  "compilerOptions": {
    /* Basic Options */
    "target": "ESNext",                       /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
    "module": "ESNext",                       /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": [                                  /* Specify library files to be included in the compilation */
      "esnext",
      "dom",
      "dom.iterable"
    ],
    "allowJs": false,                         /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    "jsx": "preserve",                        /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    "declaration": true,                      /* Generates corresponding '.d.ts' file. */
    "declarationMap": true,
    "sourceMap": true,                        /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    "outDir": "./dist",                       /* Redirect output structure to the directory. */
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    "downlevelIteration": true,               /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
    "skipLibCheck": false,

    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    "noUnusedLocals": true,                   /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    "noImplicitReturns": true,                /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    "moduleResolution": "node",               /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {                             /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // },
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Source Map Options */
    // "sourceRoot": "./",                    /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "./",                       /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    "experimentalDecorators": true,           /* Enables experimental support for ES7 decorators. */
    "emitDecoratorMetadata": true             /* Enables experimental support for emitting type metadata for decorators. */
  },
  "exclude": [
    "node_modules"
  ]
}


/// <reference types="../../../../types/cypress" />

import { VCarousel, VCarouselItem } from '..'
import { generate } from '@/../cypress/templates'

const props = {}

const stories = {
  Default: (
    <VCarousel>
      <VCarouselItem />
    </VCarousel>
  ),
}
// Tests
describe('VCarouselItem', () => {
  generate({ stories, props, component: VCarouselItem })
})


export { VTimeline } from './VTimeline'
export { VTimelineItem } from './VTimelineItem'


keep keep keep replace replace replace replace keep keep keep keep keep


<template>
  <div>
    <v-data-table
      v-model:options="options"
      :headers="headers"
      :items="desserts"
      :server-items-length="totalDesserts"
      :loading="loading"
      class="elevation-1"
    ></v-data-table>
  </div>
</template>

<script>
  export default {
    data () {
      return {
        totalDesserts: 0,
        desserts: [],
        loading: true,
        options: {},
        headers: [
          {
            text: 'Dessert (100g serving)',
            align: 'start',
            sortable: false,
            value: 'name',
          },
          { text: 'Calories', value: 'calories' },
          { text: 'Fat (g)', value: 'fat' },
          { text: 'Carbs (g)', value: 'carbs' },
          { text: 'Protein (g)', value: 'protein' },
          { text: 'Iron (%)', value: 'iron' },
        ],
      }
    },
    watch: {
      options: {
        handler () {
          this.getDataFromApi()
        },
        deep: true,
      },
    },
    methods: {
      getDataFromApi () {
        this.loading = true
        this.fakeApiCall().then(data => {
          this.desserts = data.items
          this.totalDesserts = data.total
          this.loading = false
        })
      },
      /**
       * In a real application this would be a call to fetch() or axios.get()
       */
      fakeApiCall () {
        return new Promise((resolve, reject) => {
          const { sortBy, sortDesc, page, itemsPerPage } = this.options

          let items = this.getDesserts()
          const total = items.length

          if (sortBy.length === 1 && sortDesc.length === 1) {
            items = items.sort((a, b) => {
              const sortA = a[sortBy[0]]
              const sortB = b[sortBy[0]]

              if (sortDesc[0]) {
                if (sortA < sortB) return 1
                if (sortA > sortB) return -1
                return 0
              } else {
                if (sortA < sortB) return -1
                if (sortA > sortB) return 1
                return 0
              }
            })
          }

          if (itemsPerPage > 0) {
            items = items.slice((page - 1) * itemsPerPage, page * itemsPerPage)
          }

          setTimeout(() => {
            resolve({
              items,
              total,
            })
          }, 1000)
        })
      },
      getDesserts () {
        return [
          {
            name: 'Frozen Yogurt',
            calories: 159,
            fat: 6.0,
            carbs: 24,
            protein: 4.0,
            iron: '1%',
          },
          {
            name: 'Ice cream sandwich',
            calories: 237,
            fat: 9.0,
            carbs: 37,
            protein: 4.3,
            iron: '1%',
          },
          {
            name: 'Eclair',
            calories: 262,
            fat: 16.0,
            carbs: 23,
            protein: 6.0,
            iron: '7%',
          },
          {
            name: 'Cupcake',
            calories: 305,
            fat: 3.7,
            carbs: 67,
            protein: 4.3,
            iron: '8%',
          },
          {
            name: 'Gingerbread',
            calories: 356,
            fat: 16.0,
            carbs: 49,
            protein: 3.9,
            iron: '16%',
          },
          {
            name: 'Jelly bean',
            calories: 375,
            fat: 0.0,
            carbs: 94,
            protein: 0.0,
            iron: '0%',
          },
          {
            name: 'Lollipop',
            calories: 392,
            fat: 0.2,
            carbs: 98,
            protein: 0,
            iron: '2%',
          },
          {
            name: 'Honeycomb',
            calories: 408,
            fat: 3.2,
            carbs: 87,
            protein: 6.5,
            iron: '45%',
          },
          {
            name: 'Donut',
            calories: 452,
            fat: 25.0,
            carbs: 51,
            protein: 4.9,
            iron: '22%',
          },
          {
            name: 'KitKat',
            calories: 518,
            fat: 26.0,
            carbs: 65,
            protein: 7,
            iron: '6%',
          },
        ]
      },
    },
  }
</script>


<template>
  <div>
    <v-expansion-panels class="mb-6">
      <v-expansion-panel
        v-for="i in 3"
        :key="i"
      >
        <v-expansion-panel-title expand-icon="mdi-menu-down">
          Item
        </v-expansion-panel-title>
        <v-expansion-panel-text>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</v-expansion-panel-text>
      </v-expansion-panel>
    </v-expansion-panels>

    <v-expansion-panels>
      <v-expansion-panel>
        <v-expansion-panel-title expand-icon="mdi-plus" collapse-icon="mdi-minus">
          Item
        </v-expansion-panel-title>
        <v-expansion-panel-text>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
        </v-expansion-panel-text>
      </v-expansion-panel>

      <v-expansion-panel>
        <v-expansion-panel-title>
          Item
          <template v-slot:actions="{ expanded }">
            <v-icon :color="!expanded ? 'teal' : ''" :icon="expanded ? 'mdi-pencil' : 'mdi-check'"></v-icon>
          </template>
        </v-expansion-panel-title>
        <v-expansion-panel-text>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
        </v-expansion-panel-text>
      </v-expansion-panel>

      <v-expansion-panel>
        <v-expansion-panel-title disable-icon-rotate>
          Item
          <template v-slot:actions>
            <v-icon color="error" icon="mdi-alert-circle">
            </v-icon>
          </template>
        </v-expansion-panel-title>
        <v-expansion-panel-text>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
        </v-expansion-panel-text>
      </v-expansion-panel>
    </v-expansion-panels>
  </div>
</template>


<template>

    <v-switch
      v-model="mirror"
      label="Toggle mirror"
    ></v-switch>
    <v-timeline
      :mirror="mirror"
    >
      <v-timeline-item
        v-for="n in 2"
        :key="n"
      >
        <template v-slot:opposite>
          Tus eu perfecto
        </template>
        <v-card class="elevation-2">
          <v-card-title class="text-h5">
            Lorem ipsum
          </v-card-title>
          <v-card-text>
            Lorem ipsum dolor sit amet, no nam oblique veritus. Commune scaevola imperdiet nec ut, sed euismod convenire principes at. Est et nobis iisque percipit, an vim zril disputando voluptatibus, vix an salutandi sententiae.
          </v-card-text>
        </v-card>
      </v-timeline-item>
    </v-timeline>
    <v-timeline
      :mirror="mirror"
      single-side="before"
    >
      <v-timeline-item
        v-for="n in 2"
        :key="n"
      >
        <template v-slot:opposite>
          Tus eu perfecto
        </template>
        <v-card class="elevation-2">
          <v-card-title class="text-h5">
            Lorem ipsum
          </v-card-title>
          <v-card-text>
            Lorem ipsum dolor sit amet, no nam oblique veritus. Commune scaevola imperdiet nec ut, sed euismod convenire principes at. Est et nobis iisque percipit, an vim zril disputando voluptatibus, vix an salutandi sententiae.
          </v-card-text>
        </v-card>
      </v-timeline-item>
    </v-timeline>

</template>


@if (map.has-key(colors.$text-on-colors, $color_name)) {


// Utilities
import { mount } from '@vue/test-utils'
import { describe, expect, it } from '@jest/globals'

describe('NAME', () => {
  it('', () => {
    expect().toBe()
  })
})


---
meta:
  title: Data iterator component
  description: The data iterator component is used for filter and displaying data including sorting, searching, pagination, and selection.
  keywords: data iterators, vuetify data iterator component, vue data iterator component
related:
  - /components/data-tables/
  - /components/simple-tables/
  - /components/toolbars/
---

# Data iterators

The `v-data-iterator` component is used for displaying data, and shares a majority of its functionality with the `v-data-table` component. Features include sorting, searching, pagination, and selection.

<entry />

## Usage

The `v-data-iterator` allows you to customize exactly how to display your data. In this example we are using a grid with cards.

<usage name="v-data-iterator" />

## API

<api-inline />

## Examples

### Slots

#### Default

The `v-data-iterator` has internal state for both selection and expansion, just like `v-data-table`. In this example we use the methods `isExpanded` and `expand` available on the default slot.

<example file="v-data-iterator/slot-default" />

#### Header and footer

The `v-data-iterator` has both a header and footer slot for adding extra content.

<example file="v-data-iterator/slot-header-and-footer" />

### Misc

#### Filter

Order, filters and pagination can be controlled externally by using the individual props

<example file="v-data-iterator/misc-filter" />

<backmatter />


@use 'sass:map';
@use '../settings';
@use '../settings/colors';
@use '../tools/functions' as *;

@mixin background-color($color_value) {
  background-color: $color_value !important;
}
@mixin text-color($color_value) {
  color: $color_value !important;
  caret-color: $color_value !important;
}
@mixin background-text-color($color_name, $color_type) {
  $map_value: map-deep-get(colors.$text-on-colors, $color_name, $color_type);

  color: $map_value !important;
}

@if (settings.$color-pack) {
  @each $color_name, $color_value in colors.$shades {
    .bg-#{$color_name} {
      @include background-color($color_value);

      @if (map.has-key(colors.$text-on-colors, 'shades')) {
        @include background-text-color('shades', $color_name);
      }
    }

    .text-#{$color_name} {
      @include text-color($color_value);
    }
  }

  @each $color_name, $color_color in colors.$colors {
    @each $color_type, $color_value in $color_color {
      @if ($color_type == 'base') {
        .bg-#{$color_name} {
          @include background-color($color_value);

          @if (map.has-key(colors.$text-on-colors, $color_name)) {
            @include background-text-color($color_name, $color_type);
          }
        }

        .text-#{$color_name} {
          @include text-color($color_value);
        }
      }
      @else if ($color_type != 'shades') {
        .bg-#{$color_name}-#{$color_type} {
          @include background-color($color_value);

          @if (map.has-key(colors.$text-on-colors, $color_name)) {
            @include background-text-color($color_name, $color_type);
          }
        }

        .text-#{$color_name}-#{$color_type} {
          @include text-color($color_value);
        }
      }
    }
  }
}


<template>
  <v-sheet
    class="d-flex bg-surface-variant"
  >
    <v-sheet class="ma-2 pa-2">
      I'm a single element in a flexbox container!
    </v-sheet>
  </v-sheet>
</template>


const DataOptions = {
  page: 'number',
  itemsPerPage: 'number',
  sortBy: 'string[]',
  sortDesc: 'boolean[]',
  groupBy: 'string[]',
  groupDesc: 'boolean[]',
  multiSort: 'boolean',
  mustSort: 'boolean',
}

const DataPagination = {
  page: 'number',
  itemsPerPage: 'number',
  pageStart: 'number',
  pageStop: 'number',
  pageCount: 'number',
  itemsLength: 'number',
}

const DataProps = [
  { name: 'items', source: 'v-data' },
  { name: 'options', source: 'v-data', type: 'DataOptions', example: DataOptions },
  { name: 'sortBy', source: 'v-data' },
  { name: 'sortDesc', source: 'v-data' },
  {
    name: 'customSort',
    source: 'v-data',
    default: 'gh:sortItems',
    example: '(items: any[], sortBy: string[], sortDesc: boolean[], locale: string, customSorters?: Record<string, compareFn>) => any[]',
  },
  { name: 'mustSort', source: 'v-data' },
  { name: 'multiSort', source: 'v-data' },
  { name: 'page', source: 'v-data' },
  { name: 'itemsPerPage', source: 'v-data' },
  { name: 'groupBy', source: 'v-data' },
  { name: 'groupDesc', source: 'v-data' },
  { name: 'checkboxColor', source: 'v-data', type: 'string' },
  {
    name: 'customGroup',
    source: 'v-data',
    default: 'gh:groupItems',
    example: '(items: any[], groupBy: string[], groupDesc: boolean[]) => Record<string, any[]>',
  },
  { name: 'locale', source: 'v-data' },
  { name: 'disableSort', source: 'v-data' },
  { name: 'disablePagination', source: 'v-data' },
  { name: 'disableFiltering', source: 'v-data' },
  { name: 'search', source: 'v-data' },
  { name: 'customFilter', source: 'v-data', default: 'gh:searchItems', example: '(items: any[], search: string) => any[]' },
  { name: 'serverItemsLength', source: 'v-data' },
]

const DataDefaultScopedSlotProps = {
  items: 'any[]',
  originalItemsLength: 'number',
  pagination: DataPagination,
  options: DataOptions,
  groupedItems: 'Array<{ name: string, items: any[] }>',
  updateOptions: '(obj: any) => void',
  sort: '(value: string) => void',
  sortArray: '(sortBy: string[]) => void',
  group: '(value: string) => void',
}

const DataEvents = [
  { name: 'current-items', source: 'v-data', value: 'any[]' },
  { name: 'page-count', source: 'v-data', value: 'number' },
  { name: 'pagination', source: 'v-data', value: DataPagination },
  { name: 'update:options', source: 'v-data', value: DataOptions },
  { name: 'update:page', source: 'v-data', value: 'number' },
  { name: 'update:items-per-page', source: 'v-data', value: 'number' },
  { name: 'update:sort-by', source: 'v-data', value: 'string | string[]' },
  { name: 'update:sort-desc', source: 'v-data', value: 'boolean | boolean[]' },
  { name: 'update:group-by', source: 'v-data', value: 'string | string[]' },
  { name: 'update:group-desc', source: 'v-data', value: 'boolean | boolean[]' },
  { name: 'update:multi-sort', source: 'v-data', value: 'boolean' },
  { name: 'update:must-sort', source: 'v-data', value: 'boolean' },
]

module.exports = {
  'v-data': {
    props: DataProps,
    events: DataEvents,
    slots: [{
      name: 'default',
      props: DataDefaultScopedSlotProps,
    }],
  },
  DataOptions,
  DataPagination,
  DataProps,
  DataDefaultScopedSlotProps,
  DataEvents,
}


<template>
  <v-container class="pa-4 text-center">
    <v-row
      class="fill-height"
      align="center"
      justify="center"
    >
      <template v-for="(item, i) in items" :key="i">
        <v-col
          cols="12"
          md="4"
        >
          <v-hover v-slot="{ isHovering, props }">
            <v-card
              :elevation="isHovering ? 12 : 2"
              :class="{ 'on-hover': isHovering }"
              v-bind="props"
            >
              <v-img
                :src="item.img"
                height="225px"
                cover
              >
                <v-card-title class="text-h6 text-white d-flex flex-column">

                    {{ item.title }}

                      {{ item.text }}

                      {{ item.subtext }}

                </v-card-title>

                  <v-btn
                    v-for="(icon, index) in icons"
                    :key="index"
                    variant="text"
                    :class="{ 'show-btns': isHovering }"
                    :color="transparent"
                    :icon="icon"
                  ></v-btn>

              </v-img>
            </v-card>
          </v-hover>
        </v-col>
      </template>
    </v-row>
  </v-container>
</template>


keep keep keep keep add keep keep keep keep

DOM.addClass(node, @config.class + value)

_   = require('lodash')
DOM = require('./dom')

class Format
  @types:
    LINE: 'line'

  @EMBED_TEXT: '!' # No reason we picked ! besides it being one character (so delta cannot split it up)

  @FORMATS:
    bold:
      tag: 'B'
      prepare: 'bold'

    italic:
      tag: 'I'
      prepare: 'italic'

    underline:
      tag: 'U'
      prepare: 'underline'

    strike:
      tag: 'S'
      prepare: 'strikeThrough'

    color:
      style: 'color'
      default: '#000'
      prepare: 'foreColor'

    background:
      style: 'backgroundColor'
      default: '#fff'
      prepare: 'backColor'

    font:
      style: 'fontFamily'
      default: "'Helvetica', 'Arial', sans-serif"
      prepare: 'fontName'

    size:
      style: 'fontSize'
      default: '13px'
      prepare: 'fontSize'

    link:
      tag: 'A'
      attribute: 'href'

    image:
      tag: 'IMG'
      attribute: 'src'

    align:
      type: Format.types.LINE
      style: 'textAlign'
      default: 'left'

  constructor: (@document, @config) ->

  add: (node, value) ->
    return this.remove(node) unless value
    return node if this.value(node) == value
    if _.isString(@config.tag)
      formatNode = @document.createElement(@config.tag)
      if DOM.VOID_TAGS[formatNode.tagName]?
        node.parentNode.insertBefore(formatNode, node) if node.parentNode?
        DOM.removeNode(node)
        node = formatNode
      else
        node = DOM.wrap(formatNode, node)
    if _.isString(@config.style)
      node.style[@config.style] = value if value != @config.default
    if _.isString(@config.attribute)
      node.setAttribute(@config.attribute, value)
    return node

  isType: (type) ->
    return type == @config.type

  match: (node) ->
    return false unless DOM.isElement(node)
    return false if _.isString(@config.tag) and node.tagName != @config.tag
    return false if _.isString(@config.style) and (!node.style[@config.style] or node.style[@config.style] == @config.default)
    return false if _.isString(@config.attribute) and !node.hasAttribute(@config.attribute)
    return true

  prepare: (value) ->
    if _.isString(@config.prepare)
      @document.execCommand(@config.prepare, false, value)
    else if _.isFunction(@config.prepare)
      @config.prepare(@document, value)

  remove: (node) ->
    return unless this.match(node)
    if _.isString(@config.style)
      node.style[@config.style] = ''    # IE10 requires setting to '', other browsers can take null
      node.removeAttribute('style') unless node.getAttribute('style')  # Some browsers leave empty style attribute
    if _.isString(@config.attribute)
      node.removeAttribute(@config.attribute)
    if _.isString(@config.tag)
      node = DOM.switchTag(node, DOM.DEFAULT_INLNE_TAG)
      DOM.setText(node, Format.EMBED_TEXT) if DOM.EMBED_TAGS[@config.tag]?
    return node

  value: (node) ->
    return undefined unless this.match(node)
    return node.getAttribute(@config.attribute) or undefined if _.isString(@config.attribute)
    return node.style[@config.style] or undefined if _.isString(@config.style) and node.style[@config.style] != @config.default
    return true if _.isString(@config.tag) and node.tagName == @config.tag
    # TODO class regex
    return undefined

module.exports = Format


inputHeight = 16px
inputMargin = 3px
inputPadding = 4px
blackColor = #454545
blueColor = #06c
grayColor = #ccc
lightGrayColor = #ddd
fontNamePickerWidth = 98px
fontSizePickerWidth = 75px
pickerPadding = 8px
pickerWidth = 50px
toolbarPadding = 8px

@font-face
  font-family: 'quill-snow'
  font-style: normal
  font-weight: normal
  src: url('assets/quill-snow.eot') format('embedded-opentype'), url('assets/quill-snow.woff') format('woff')

.sc-toolbar-container
  box-sizing: border-box
  color: blackColor
  font-family: 'Helvetica', 'Arial', sans-serif
  padding: toolbarPadding (15px - pickerPadding) (toolbarPadding - 3px)
  user-select: none
  -webkit-user-select: none
  -moz-user-select: none
  -ms-user-select: none
  .sc-format-group
    box-sizing: content-box
    display: inline-block
    float: left
    line-height: inputHeight
    margin-right: 15px
  .sc-format-button
    background-position: center center
    background-repeat: no-repeat
    box-sizing: content-box
    cursor: pointer
    font-family: 'quill-snow'
    font-size: 12px
    display: inline-block
    float: left
    height: inputHeight
    margin-left: inputMargin
    margin-right: inputMargin
    padding: inputPadding
    text-align: center
    width: 16px
  .sc-format-separator
    background-color: lightGrayColor
    box-sizing: content-box
    display: inline-block
    float: left
    height: inputHeight
    margin-bottom: inputPadding
    margin-top: inputPadding
    width: 1px
// Clearfloat
.sc-toolbar-container:after
  clear: both
  content: ""
  display: table

.sc-picker
  box-sizing: content-box
  font-size: 13px
  display: inline-block
  font-weight: bold
  position: relative
  .sc-picker-label
    box-sizing: content-box
    cursor: pointer
    padding: inputPadding pickerPadding
  .sc-picker-label:after
    content: "\e80"
    font-family: 'quill-snow'
    font-size: 12px
    margin-left: 8px
  .sc-picker-label.sc-active, .sc-picker-label:hover
    color: blueColor
  .sc-picker-options
    background-color: #fff
    box-sizing: content-box
    display: none
    padding: inputPadding pickerPadding
    position: absolute
    .sc-picker-item
      box-sizing: content-box
      cursor: pointer
      padding-bottom: inputPadding
      padding-top: inputPadding
    .sc-picker-item.selected, .sc-picker-item:hover
      color: blueColor
.sc-picker.sc-expanded
  .sc-picker-label
    border: 1px solid grayColor
    color: grayColor
    margin: -1px
  .sc-picker-options
    border: 1px solid grayColor
    box-shadow: rgba(0,0,0,0.2) 0 2px 8px
    display: block
    margin: -1px

.sc-picker.sc-color-picker
  float: left
  .sc-picker-label
    background-position: center center
  .sc-picker-label:after
    content: normal
  .sc-picker-options
    height: 106px
    margin-left: inputMargin
    margin-top: 24px
    padding: 6px
    position: absolute
    width: 140px
    .sc-picker-item
      float: left
      height: 16px
      margin: 2px
      padding: 0px
      width: 16px
    .sc-picker-item.selected, .sc-picker-item:hover
      border: 1px solid #000
      margin: 1px
.sc-picker.sc-color-picker.sc-expanded
  .sc-picker-label
    margin-left: inputMargin - 1px
    margin-right: inputMargin - 1px
  .sc-picker-options
    margin-left: inputMargin - 1px

.sc-toolbar-container
  .sc-format-button:hover, .sc-format-button.sc-active, .sc-format-button.sc-on
    color: blueColor
  .sc-format-button.sc-bold:before
    content: "\e801"
  .sc-format-button.sc-italic:before
    content: "\e802"
  .sc-format-button.sc-underline:before
    content: "\e803"
  .sc-format-button.sc-strike:before
    content: "\e804"
  .sc-format-button.sc-link:before
    content: "\e805"
  .sc-format-button.sc-image:before
    content: "\e806"
  .sc-format-button.sc-list:before
    content: "\e807"
  .sc-format-button.sc-bullet:before
    content: "\e808"
  .sc-format-button.sc-indent:before
    content: "\e809"
  .sc-format-button.sc-outdent:before
    content: "\e80A"
  .sc-format-button.sc-left:before
    content: "\e80B"
  .sc-format-button.sc-right:before
    content: "\e80C"
  .sc-format-button.sc-center:before
    content: "\e80D"
  .sc-format-button.sc-justify:before
    content: "\e80E"
  .sc-format-button.sc-superscript:before
    content: "\e80F"
  .sc-format-button.sc-subscript:before
    content: "\e810"
  .sc-format-button.sc-authorship:before
    content: "\e811"

  .sc-font
    width: fontNamePickerWidth
    .sc-picker-options
      width: fontNamePickerWidth - 2*pickerPadding
  .sc-size
    width: fontSizePickerWidth
    .sc-picker-options
      width: fontSizePickerWidth - 2*pickerPadding

  .sc-color, .sc-background
    .sc-picker-options
      .sc-picker-item.sc-primary-color
        margin-bottom: 8px
      .sc-picker-item.sc-primary-color.sc-selected, .sc-picker-item.sc-primary-color:hover
        margin-bottom: 7px

  for $format in color background
    .sc-picker.sc-{$format} .sc-picker-label
      background-image: url('assets/' + $format + '.png')
    .sc-picker.sc-{$format} .sc-picker-label.sc-active, .sc-picker.sc-{$format} .sc-picker-label:hover, .sc-picker.sc-{$format}.sc-expanded .sc-picker-label
      background-image: url('assets/' + $format + '-active.png')


build
node_modules
coverage
tests/webdriver/fuzzer_output
tests/webdriver/logs
*.log
TODO


Copyright (c) 2013, salesforce.com
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


return false if _.isString(@config.tag) and node.tagName != @config.tag

return false if _.isString(@config.style) and (!node.style[@config.style] or node.style[@config.style] == @config.default)

return false if _.isString(@config.attribute) and !node.hasAttribute(@config.attribute)

keep keep keep keep keep replace replace replace keep keep keep

return true if c.indexOf(@config.class) == 0


_      = require('lodash')
DOM    = require('../dom')
Line   = require('../line')
Tandem = require('tandem-core')

class Keyboard
  @hotkeys:
    BOLD:       { key: 'B',          metaKey: true }
    INDENT:     { key: DOM.KEYS.TAB, shiftKey: false }
    ITALIC:     { key: 'I',          metaKey: true }
    OUTDENT:    { key: DOM.KEYS.TAB, shiftKey: true }
    UNDERLINE:  { key: 'U',          metaKey: true }

  constructor: (@quill, options) ->
    @hotkeys = {}
    this._initListeners()
    this._initHotkeys()
    this._initDeletes()

  addHotkey: (hotkey, callback) ->
    hotkey = if _.isObject(hotkey) then _.clone(hotkey) else { key: hotkey }
    hotkey.callback = callback
    @hotkeys[hotkey.key] ?= []
    @hotkeys[hotkey.key].push(hotkey)

  toggleFormat: (range, format) ->
    delta = @quill.getContents(range)
    hasFormat = _.all(delta.ops, (op) ->
      return op.attributes[format]
    )
    @quill.formatText(range, format, !hasFormat, { source: 'user' })

  _initDeletes: ->
    _.each([DOM.KEYS.DELETE, DOM.KEYS.BACKSPACE], (key) =>
      this.addHotkey(key, =>
        # Prevent deleting if editor is already blank (or just empty newline)
        return @quill.getLength() > 1
      )
    )

  _initHotkeys: ->
    this.addHotkey(Keyboard.hotkeys.INDENT, (range) =>
      this._onTab(range, false)
      return false
    )
    this.addHotkey(Keyboard.hotkeys.OUTDENT, (range) =>
      # TODO implement when we implement multiline tabs
      return false
    )
    _.each(['bold', 'italic', 'underline'], (format) =>
      this.addHotkey(Keyboard.hotkeys[format.toUpperCase()], (range) =>
        this.toggleFormat(range, format)
        return false
      )
    )

  _initListeners: ->
    DOM.addEventListener(@quill.root, 'keydown', (event) =>
      prevent = false
      if @hotkeys[event.key]?
        range = @quill.getSelection()
        _.each(@hotkeys[event.key], (hotkey) =>
          return if hotkey.metaKey? and (event.metaKey != hotkey.metaKey and event.ctrlKey != hotkey.metaKey)
          return if hotkey.shiftKey? and event.shiftKey != hotkey.shiftKey
          prevent = hotkey.callback(range) == false or prevent
        )
      return !prevent
    )

  _onTab: (range, shift = false) ->
    # TODO implement multiline tab behavior
    # Behavior according to Google Docs + Word
    # When tab on one line, regardless if shift is down, delete selection and insert a tab
    # When tab on multiple lines, indent each line if possible, outdent if shift is down
    delta = Tandem.Delta.makeDelta({
      startLength: @quill.getLength()
      ops: [
        { start: 0, end: range.start }
        { value: "\t" }
        { start: range.end, end: @quill.getLength() }
      ]
    })
    @quill.updateContents(delta)
    @quill.setSelection(range.start + 1, range.start + 1)

module.exports = Keyboard


describe('Toolbar', ->
  beforeEach( ->
    @editorContainer = $('#editor-container').html('

          0123456789

    ').get(0)
    @toolbarContainer = $('#toolbar-container').get(0)
    @toolbarContainer.innerHTML = @toolbarContainer.innerHTML   # Remove child listeners
    @quill = new Quill(@editorContainer.firstChild)
    @toolbar = @quill.addModule('toolbar', { container: @toolbarContainer })
    @button = @toolbarContainer.querySelector('.sc-bold')
    @select = @toolbarContainer.querySelector('.sc-size')
  )

  describe('format', ->
    it('button add', ->
      range = new Quill.Lib.Range(2, 4)
      @quill.setSelection(range)
      Quill.DOM.triggerEvent(@button, 'click')
      expect(@quill.getContents(range)).toEqualDelta(Tandem.Delta.makeInsertDelta(0, 0, '23', { bold: true }))
    )

    it('button remove', ->
      range = new Quill.Lib.Range(0, 2)
      @quill.setSelection(range)
      Quill.DOM.triggerEvent(@button, 'click')
      expect(@quill.getContents(range)).toEqualDelta(Tandem.Delta.makeInsertDelta(0, 0, '01'))
    )

    it('dropdown add', ->
      range = new Quill.Lib.Range(2, 4)
      @quill.setSelection(range)
      Quill.DOM.selectOption(@select, '18px')
      expect(@quill.getContents(range)).toEqualDelta(Tandem.Delta.makeInsertDelta(0, 0, '23', { size: '18px' }))
    )

    it('dropdown remove', ->
      range = new Quill.Lib.Range(6, 8)
      @quill.setSelection(range)
      Quill.DOM.resetSelect(@select)
      expect(@quill.getContents(range)).toEqualDelta(Tandem.Delta.makeInsertDelta(0, 0, '67'))
    )
  )

  describe('updateActive()', ->
    it('button', ->
      @quill.setSelection(1, 1)
      expect(Quill.DOM.hasClass(@button, 'sc-active')).toBe(true)
      @quill.setSelection(null)
      expect(Quill.DOM.hasClass(@button, 'sc-active')).toBe(false)
    )

    it('dropdown', ->
      @quill.setSelection(7, 7)
      expect(Quill.DOM.getSelectValue(@select)).toEqual('18px')
      @quill.setSelection(9, 9)
      expect(Quill.DOM.getSelectValue(@select)).toEqual('32px')
      @quill.setSelection(3, 3)
      expect(Quill.DOM.getSelectValue(@select)).toEqual('13px')
    )

    it('dropdown blank', ->
      @quill.setSelection(5, 7)
      expect(Quill.DOM.getSelectValue(@select)).toEqual('')
    )
  )

  describe('_getActive()', ->
    tests =
      'cursor in middle of format':
        range: [1, 1], expected: { bold: true }
      'cursor at beginning of format':
        range: [4, 4], expected: {}
      'cursor at end of format':
        range: [2, 2], expected: { bold: true }
      'neighboring formats':
        range: [2, 4], expected: {}
      'overlapping formats':
        range: [1, 3], expected: {}
      'select format':
        range: [7, 7], expected: { size: '18px' }
      'overlapping select formats':
        range: [5, 7], expected: { size: ['18px'] }

    _.each(tests, (test, name) ->
      it(name, ->
        formats = @toolbar._getActive(new Quill.Lib.Range(test.range[0], test.range[1]))
        expect(formats).toEqual(test.expected)
      )
    )
  )

  describe('_interesctFormats()', ->
    tests =
      'preserve common format':
        initial:  [{ bold: true }, { bold: true }]
        expected: { bold: true }
      'remove uncommon format':
        initial:  [{ bold: true }, { italic: true }]
        expected: {}
      'common select format':
        initial:  [{ size: '18px' }, { size: '18px' }]
        expected: { size: '18px' }
      'combine select format':
        initial:  [{ size: '18px' }, { size: '10px' }, { size: '32px' }]
        expected: { size: ['18px', '10px', '32px'] }
      'preserve select format':
        initial:  [{ bold: true }, { size: '18px' }, { italic: true }]
        expected: { size: ['18px'] }
      'combination of all cases':
        initial:  [{ bold: true, size: '10px' }, { bold: true, italic: true }, { bold: true, size: '18px' }]
        expected: { bold: true, size: ['10px', '18px'] }

    _.each(tests, (test, name) ->
      it(name, ->
        formats = @toolbar._intersectFormats(test.initial)
        expect(formats).toEqual(test.expected)
      )
    )
  )
)


_       = require('lodash')
DOM     = require('../dom')
Tooltip = require('./tooltip')

class LinkTooltip extends Tooltip
  @DEFAULTS:
    styles:
      '.link-tooltip-container':
        'padding': '5px 10px'
      '.link-tooltip-container input.input':
        'width': '170px'
      '.link-tooltip-container input.input, .link-tooltip-container a.done, .link-tooltip-container.editing a.url, .link-tooltip-container.editing a.change':
        'display': 'none'
      '.link-tooltip-container.editing input.input, .link-tooltip-container.editing a.done':
        'display': 'inline-block'
    template:
     'Visit URL:

      <input class="input" type="text">
       -
      Change
      Done'

  constructor: (@quill, @options) ->
    @options.styles = _.defaults(@options.styles, Tooltip.DEFAULTS.styles)
    @options = _.defaults(@options, Tooltip.DEFAULTS)
    super(@quill, @options)
    DOM.addClass(@container, 'link-tooltip-container')
    @textbox = @container.querySelector('.input')
    @link = @container.querySelector('.url')
    this.initListeners()

  initListeners: ->
    @quill.on(@quill.constructor.events.SELECTION_CHANGE, (range) =>
      return unless range? and range.isCollapsed()
      anchor = this._findAnchor(range)
      if anchor
        this.setMode(anchor.href, false)
        this.show(anchor)
      else
        @range = null   # Prevent restoring selection to last saved
        this.hide()
    )
    DOM.addEventListener(@container.querySelector('.done'), 'click', _.bind(this.saveLink, this))
    DOM.addEventListener(@container.querySelector('.change'), 'click', =>
      this.setMode(@link.href, true)
    )
    this.initTextbox(@textbox, this.saveLink, this.hide)
    @quill.onModuleLoad('toolbar', (toolbar) =>
      toolbar.initFormat('link', 'click', _.bind(this._onToolbar, this))
    )

  saveLink: ->
    url = this._normalizeURL(@textbox.value)
    @quill.formatText(@range, 'link', url) if @range?
    this.setMode(url, false)

  setMode: (url, edit = false) ->
    if edit
      @textbox.value = url
      @textbox.focus()
      _.defer( =>
        @textbox.setSelectionRange(url.length, url.length)
      )
    else
      @link.href = url
      DOM.setText(@link, url)
    DOM.toggleClass(@container, 'editing', edit)

  _findAnchor: (range) ->
    [leaf, offset] = @quill.editor.doc.findLeafAt(Math.max(0, range.start - 1))
    node = leaf.node if leaf?
    while node?
      if node.tagName == 'A'
        this.setMode(node.href, false)
        this.show(node)
        return node
      else
        node = node.parentNode
    return null

  _onToolbar: (range, value) ->
    return unless range and !range.isCollapsed()
    if value
      this.setMode(this._suggestURL(range), true)
      nativeRange = @quill.editor.selection._getNativeRange()
      this.show(nativeRange)
    else
      @quill.formatText(range, 'link', false, 'user')

  _normalizeURL: (url) ->
    url = 'http://' + url unless /^https?:\/\//.test(url)
    return url

  _suggestURL: (range) ->
    text = @quill.getText(range)
    return this._normalizeURL(text)

module.exports = LinkTooltip


node.style[@config.style] = ''    # IE10 requires setting to '', other browsers can take null

node.removeAttribute('style') unless node.getAttribute('style')  # Some browsers leave empty style attribute

node = DOM.switchTag(node, DOM.DEFAULT_INLNE_TAG)

DOM.setText(node, Format.EMBED_TEXT) if DOM.EMBED_TAGS[@config.tag]?

keep keep keep keep add keep keep keep keep keep

DOM.removeClass(node, c) if c.indexOf(@config.class) == 0

node.removeAttribute('class') unless node.getAttribute('class')  # Some browsers leave empty style attribute


# IE feature detection
supportsRGBA = true
scriptElem = document.getElementsByTagName('script')[0]
try
  scriptElem.style.color = 'rgba(128,128,128,0.5)'
catch e
  supportsRGBA = false
finally
  scriptElem.style.color = ""

getColor = (id, lighten) ->
  alpha = if lighten then '0.4' else '1.0'
  if id == 1 or id == 'quill-1'
    return if supportsRGBA then "rgba(0,153,255,#{alpha})" else "rgb(0,153,255)"
  else
    return if supportsRGBA then "rgba(255,153,51,#{alpha})" else "rgb(255,153,51)"

listenEditor = (source, target) ->
  source.on(Quill.events.TEXT_CHANGE, (delta, origin) ->
    return if origin == 'api'
    target.updateContents(delta)
    sourceDelta = source.getContents()
    targetDelta = target.getContents()
    decomposeDelta = targetDelta.decompose(sourceDelta)
    isEqual = _.all(decomposeDelta.ops, (op) ->
      return false if op.delta?
      return true if (_.keys(op.attributes).length == 0)
      sourceOp = sourceDelta.getOpsAt(op.start, op.end - op.start)
      return true if sourceOp.length == 1 and sourceOp[0].delta == "\n"
      return false
    )
    console.assert(decomposeDelta.startLength == decomposeDelta.endLength and isEqual, "Editor diversion!", source, target, sourceDelta, targetDelta) if console?
  ).on(Quill.events.SELECTION_CHANGE, (range) ->
    return unless range?
    # target.getModule('multi-cursor').moveCursor(source.id, range.end)
  )

editors = []
for num in [1, 2]
  $wrapper = $(".editor-wrapper.#{if num == 1 then 'first' else 'last'}")
  $container = $('.editor-container', $wrapper)
  editor = new Quill($container.get(0), {
    modules:
      # 'multi-cursor': true
      'toolbar': { container: $('.toolbar-container', $wrapper).get(0) }
      'link-tooltip': true
      'image-tooltip': true
    theme: 'snow'
  })
  # authorship = editor.addModule('authorship', {
  #   authorId: editor.id
  #   color: getColor(num, true)
  #   button: $('.sc-authorship', $wrapper).get(0)
  # })
  editors.push(editor)

listenEditor(editors[0], editors[1])
listenEditor(editors[1], editors[0])
# editors[0].getModule('authorship').addAuthor(editors[1].id, getColor(editors[1].id, true))
# editors[1].getModule('authorship').addAuthor(editors[0].id, getColor(editors[0].id, true))
# editors[0].getModule('multi-cursor').setCursor(editors[1].id, 0, editors[1].id, getColor(editors[1].id))
# editors[1].getModule('multi-cursor').setCursor(editors[0].id, 0, editors[0].id, getColor(editors[0].id))


describe('Quill', ->
  beforeEach( ->
    @container = $('#editor-container').html('

0123

5678

    ').get(0)
    @quill = new Quill(@container.firstChild)
  )

  describe('constructor', ->
    it('string container', ->
      @container.innerHTML = '

'
      quill = new Quill('#target')
      expect(quill.editor.iframeContainer).toEqual(@container.firstChild)
    )

    it('invalid container', ->
      expect( =>
        @quill = new Quill('.none')
      ).toThrow(new Error('Invalid Quill container'))
    )
  )

  describe('styles', ->
    it('constructor overwrite', (done) ->
      @container.innerHTML = '
0123
'
      @quill = new Quill(@container.firstChild, {
        styles: { '.editor-container div': { 'line-height': '25px' } }
      })
      @quill.on(Quill.events.RENDER_UPDATE, (css) =>
        if css.indexOf('.editor-container div') > -1
          expect(@quill.root.firstChild.offsetHeight).toEqual(25)
          done()
      )
    )

    it('addStyles()', (done) ->
      @quill.addStyles({ '.editor-container div': { 'line-height': '25px' } })
      @quill.on(Quill.events.RENDER_UPDATE, (css) =>
        if css.indexOf('.editor-container div') > -1
          expect(@quill.root.firstChild.offsetHeight).toEqual(25)
          done()
      )
    )
  )

  describe('modules', ->
    it('addContainer()', ->
      @quill.addContainer('test-container', true)
      expect(@quill.root.parentNode.querySelector('.test-container')).toEqual(@quill.root.parentNode.firstChild)
    )

    it('addModule()', ->
      obj =
        before: ->
        after: ->
      spyOn(obj, 'before')
      spyOn(obj, 'after')
      expect(@quill.getModule('toolbar')).toBe(undefined)
      @quill.onModuleLoad('toolbar', obj.before)
      @quill.addModule('toolbar', { container: '#toolbar-container' })
      expect(@quill.getModule('toolbar')).not.toBe(null)
      expect(obj.before).toHaveBeenCalled()
      @quill.onModuleLoad('toolbar', obj.after)
      expect(obj.after).toHaveBeenCalled()
    )

    it('addModule() nonexistent', ->
      expect( =>
        @quill.addModule('nonexistent')
      ).toThrow(new Error("Cannot load nonexistent module. Are you sure you included it?"))
    )
  )

  describe('manipulation', ->
    it('deleteText()', ->
      @quill.deleteText(2, 3)
      expect(@quill.root).toEqualHTML('
013

5678
', true)
    )

    # it('formatLines()', ->
    #   @quill.formatLines(4, 2, 'align', 'right')
    #   expect(@quill.root).toEqualHTML('
    #
0123

    #
5678

    #   ', true)
    # )

    it('formatText()', ->
      @quill.formatText(2, 4, 'bold', true)
      expect(@quill.root).toEqualHTML('
0123

5678
', true)
      @quill.formatText(2, 4, 'bold', false)
      expect(@quill.root).toEqualHTML('
0123

5678
', true)
      expect(@quill.getContents(0, 4)).toEqualDelta(Tandem.Delta.makeInsertDelta(0, 0, '0123'))
    )

    # TODO add test for formatText(range, 'size', '13px')   where 13px is the default

    # it('insertEmbed()', ->
    #   @quill.insertEmbed(4, 'image', 'http://quilljs.com/images/icon.png')
    #   expect(@quill.root).toEqualHTML('
0123

5678
', true)
    # )

    it('insertText()', ->
      @quill.insertText(2, 'A')
      expect(@quill.root).toEqualHTML('
01A23

5678
', true)
    )

    it('setContents() with delta', ->
      @quill.setContents({
        startLength: 0
        endLength: 1
        ops: [{ value: 'A', attributes: { bold: true } }]
      })
      expect(@quill.root).toEqualHTML('
A
', true)
    )

    it('setContents() with ops', ->
      @quill.setContents([{ value: 'A', attributes: { bold: true } }])
      expect(@quill.root).toEqualHTML('
A
', true)
    )

    it('setHTML()', ->
      @quill.setHTML('
A
')
      expect(@quill.root).toEqualHTML('
A
', true)
    )

    it('updateContents()', ->
      @quill.updateContents(Tandem.Delta.makeInsertDelta(10, 2, 'A'))
      expect(@quill.root).toEqualHTML('
01A23

5678
', true)
    )
  )

  describe('retrievals', ->
    it('getContents() all', ->
      expect(@quill.getContents()).toEqualDelta(Tandem.Delta.makeInsertDelta(0, 0, '0123\n5678\n'))
    )

    it('getContents() partial', ->
      expect(@quill.getContents(2, 7)).toEqualDelta(Tandem.Delta.makeInsertDelta(0, 0, '23\n56'))
    )

    it('getHTML()', ->
      expect(@quill.getHTML()).toEqualHTML('
0123

5678
', true)
    )

    it('getLength()', ->
      expect(@quill.getLength()).toEqual(10)
    )

    it('getText()', ->
      expect(@quill.getText()).toEqual('0123\n5678\n')
    )
  )

  describe('selection', ->
    it('get/set range', ->
      @quill.setSelection(1, 2)
      range = @quill.getSelection()
      expect(range).not.toBe(null)
      expect(range.start).toEqual(1)
      expect(range.end).toEqual(2)
    )

    it('get/set index range', ->
      @quill.setSelection(new Quill.Lib.Range(2, 3))
      range = @quill.getSelection()
      expect(range).not.toBe(null)
      expect(range.start).toEqual(2)
      expect(range.end).toEqual(3)
    )

    it('get/set null', ->
      @quill.setSelection(1, 2)
      expect(range).not.toBe(null)
      @quill.setSelection(null)
      range = @quill.getSelection()
      expect(range).toBe(null)
    )
  )

  describe('_buildParams()', ->
    tests =
      'index range string formats'  : [1, 3, 'bold', true]
      'index range object formats'  : [1, 3, { bold: true }]
      'object range string formats' : [new Quill.Lib.Range(1, 3), 'bold', true]
      'object range object formats' : [new Quill.Lib.Range(1, 3), { bold: true }]

    _.each(tests, (args, name) ->
      it(name, ->
        [start, end, formats, source] = @quill._buildParams(args...)
        expect(start).toEqual(1)
        expect(end).toEqual(3)
        expect(formats).toEqual({ bold: true })
      )
    )

    it('source override', ->
      [start, end, formats, source] = @quill._buildParams(1, 2, {}, 'silent')
      expect(source).toEqual('silent')
    )
  )
)


_     = require('lodash')
DOM   = require('../dom')
Utils = require('../utils')

class Toolbar
  @DEFAULTS:
    container: null

  @formats:
    BUTTON: { 'bold', 'image', 'italic', 'link', 'strike', 'underline' }
    SELECT: { 'align', 'background', 'color', 'font', 'size' }
    TOOLTIP: { 'image', 'link' }

  constructor: (@quill, @options) ->
    throw new Error('container required for toolbar', @options) unless @options.container?
    @container = if _.isString(@options.container) then document.querySelector(@options.container) else @options.container
    @activeFormats = {}
    _.each(@quill.options.formats, (format) =>
      return if Toolbar.formats.TOOLTIP[format]?
      eventName = if Toolbar.formats.SELECT[format]? then 'change' else 'click'
      this.initFormat(format, eventName, (range, value) =>
        if range.isCollapsed()
          @quill.prepareFormat(format, value)
        else
          @quill.formatText(range, format, value, 'user')
        this.setActive(format, value)
      )
    )
    @quill.on(@quill.constructor.events.SELECTION_CHANGE, _.bind(this.updateActive, this))
    DOM.addClass(@container, 'sc-toolbar-container')

  initFormat: (format, eventName, callback) ->
    input = @container.querySelector(".sc-#{format}")
    return unless input?
    triggering = false
    DOM.addEventListener(input, eventName, =>
      return if triggering
      triggering = true
      value = if eventName == 'change' then DOM.getSelectValue(input) else !DOM.hasClass(input, 'sc-active')
      @quill.focus()
      range = @quill.getSelection()
      callback(range, value) if range?
      triggering = false
      return true
    )

  setActive: (format, value) ->
    if value
      @activeFormats[format] = value
    else
      delete @activeFormats[format]
    input = @container.querySelector(".sc-#{format}")
    return unless input?
    if input.tagName == 'SELECT'
      if value
        value = '' if _.isArray(value)
        DOM.selectOption(input, value, false)
      else
        DOM.resetSelect(input, false)
    else
      DOM.toggleClass(input, 'sc-active', value)

  updateActive: (range) ->
    activeFormats = this._getActive(range)
    _.each(_.keys(@activeFormats), (name) =>
      if activeFormats[name]?
        this.setActive(name, activeFormats[name]) if activeFormats[name] != @activeFormats[name]
      else
        this.setActive(name, false)
    )
    _.each(activeFormats, (value, name) =>
      this.setActive(name, value) unless @activeFormats[name]?
    )

  _getActive: (range) ->
    if range?
      if range.isCollapsed()
        start = Math.max(0, range.start - 1)
        contents = @quill.getContents(start, range.end)
      else
        contents = @quill.getContents(range)
      formatsArr = _.map(contents.ops, 'attributes')
      return this._intersectFormats(formatsArr)
    else
      return {}

  _intersectFormats: (formatsArr) ->
    return _.reduce(formatsArr.slice(1), (activeFormats, formats) ->
      activeKeys = _.keys(activeFormats)
      formatKeys = _.keys(formats)
      intersection = _.intersection(activeKeys, formatKeys)
      missing = _.difference(activeKeys, formatKeys)
      added = _.difference(formatKeys, activeKeys)
      _.each(intersection, (name) ->
        if Toolbar.formats.SELECT[name]?
          if _.isArray(activeFormats[name])
            activeFormats[name].push(formats[name]) if _.indexOf(activeFormats[name], formats[name]) < 0
          else if activeFormats[name] != formats[name]
            activeFormats[name] = [activeFormats[name], formats[name]]
      )
      _.each(missing, (name) ->
        if Toolbar.formats.BUTTON[name]?
          delete activeFormats[name]
        else if Toolbar.formats.SELECT[name]? and !_.isArray(activeFormats[name])
          activeFormats[name] = [activeFormats[name]]
      )
      _.each(added, (name) ->
        activeFormats[name] = [formats[name]] if Toolbar.formats.SELECT[name]?
      )
      return activeFormats
    , formatsArr[0] or {})

module.exports = Toolbar


return undefined unless this.match(node)

return node.getAttribute(@config.attribute) or undefined if _.isString(@config.attribute)

return node.style[@config.style] or undefined if _.isString(@config.style) and node.style[@config.style] != @config.default

return true if _.isString(@config.tag) and node.tagName == @config.tag

keep keep keep keep keep replace replace replace replace keep keep keep keep

else if _.isString(@config.style) and node.style[@config.style] != @config.default

return c.slice(@config.class.length) if c.indexOf(@config.class) == 0

else if _.isString(@config.tag) and node.tagName == @config.tag


describe('Normalizer', ->
  beforeEach( ->
    @container = $('#editor-container').html('').get(0)
  )

  describe('handleBreaks()', ->
    tests =
      'Break in middle of line':
        initial:  '
One
Two
'
        expected: '
One

Two
'
      'Break preceding line':
        initial:  '

One
'
        expected: '

One
'
      'Break after line':
        initial:  '
One

'
        expected: '
One

'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        lineNode = Quill.Normalizer.handleBreaks(@container.firstChild)
        expect(@container).toEqualHTML(test.expected)
        expect(lineNode).toEqual(@container.firstChild)
      )
    )
  )

  describe('normalizeLine()', ->
    tests =
      'pull text node':
        initial:  '

AB
C

'
        expected: '
AB

C

'
      'inline with text':
        initial:  'WhatTest'
        expected: '
WhatTest
'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        lineNode = Quill.Normalizer.normalizeLine(@container.firstChild)
        expect(@container).toEqualHTML(test.expected)
        expect(lineNode).toEqual(@container.firstChild)
      )
    )
  )

  describe('normalizeNode()', ->
    it('should whitelist style and tag', ->
      @container.innerHTML = 'Test'
      Quill.Normalizer.normalizeNode(@container.firstChild)
      expect(@container).toEqualHTML('Test')
    )
  )

  describe('optimizeLine()', ->
    tests =
      'unwrap non-format node':
        initial:  'Test'
        expected: 'Test'
      'unwrap non-format node':
        initial:  'Test'
        expected: 'Test'
      'preserve only span':
        initial:  'Test'
        expected: 'Test'
      'remove unneeded break':
        initial:  'Test
'
        expected: 'Test'
      'preserve span with attributes':
        initial:  'Test'
        expected: 'Test'
      'unwrap zero length nodes':
        initial:  ''
        expected: ''
      'unwrap eventually':
        initial:  '<s></s>'
        expected: ''
      'unwrap break on empty line':
        initial:  '
'
        expected: '
'
      'preserve break on empty line':
        initial:  '
'
        expected: '
'
      'unwrap eventually with break':
        initial:  '<s>
</s>'
        expected: '
'
      'merge similar nodes':
        initial:  'AB'
        expected: 'AB'
      'merge similar span nodes':
        initial:  'AB'
        expected: 'AB'
      'merge multiple criteria similar nodes':
        initial:  'AB'
        expected: 'AB'
      'merge similar nodes after unwrap':
        initial:  'AB'
        expected: 'AB'
      'merge recursive':
        initial:  '<s>A</s><s>B</s>'
        expected: '<s>AB</s>'
      'preserve close but not same nodes':
        initial:  'AB'
        expected: 'AB'
      'preserve similar images':
        initial:  ''
        expected: ''
      'wrap orphaned text node':
        initial:  '<s>0</s><s>1</s>'
        expected: '<s>01</s>'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = "
#{test.initial}
"
        Quill.Normalizer.optimizeLine(@container.firstChild)
        expect(@container.firstChild).toEqualHTML(test.expected)
      )
    )
  )

  describe('pullBlocks()', ->
    tests =
      'No children':
        initial:  '

'
        expected: '

'
      'Inner block':
        initial:  '

Test

Another

'
        expected: '
Test

Another

'
      'Inner deep block':
        initial:  '

Test

'
        expected: '
Test
'
      'Inner deep recursive':
        initial:  '

Test
Test

'
        expected: '
Test

Test

'
      'Continuous inlines':
        initial:  '
A
B
Inner

'
        expected: '
A
B

Inner

'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        firstChild = @container.firstChild
        Quill.Normalizer.pullBlocks(firstChild)
        expect(@container).toEqualHTML(test.expected)
        expect(firstChild).toEqual(@container.firstChild)
      )
    )
  )

  describe('stripWhitespace()', ->
    tests =
      'newlines':
        initial:
         '

            Test

'
        expected: '
Test

'
      'preceding and trailing spaces':
        initial:  '

  '
        expected: '

'
      'inner spaces':
        initial:  '

'
        expected: '

'

    _.each(tests, (test, name) ->
      it(name, ->
        strippedHTML = Quill.Normalizer.stripWhitespace(test.initial)
        expect(strippedHTML).toEqual(test.expected)
      )
    )
  )

  describe('whitelistStyles()', ->
    tests =
      'no styles':
        initial:  '

'
        expected: '

'
      'no removal':
        initial:  '

'
        expected: '

'
      'removal':
        initial:  '

'
        expected: '

'
      'complete removal':
        initial:  '

'
        expected: '

'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        Quill.Normalizer.whitelistStyles(@container.firstChild)
        expect(@container).toEqualHTML(test.expected)
      )
    )
  )

  describe('whitelistTags()', ->
    tests =
      'not element':
        initial:  'Test'
        expected: 'Test'
      'no switch needed':
        initial:  'Bold'
        expected: 'Bold'
      'alias':
        initial:  'Bold'
        expected: 'Bold'
      'switch inline':
        initial:  'A'
        expected:  'A'
      'switch block':
        initial:  '
Test
'
        expected: '
Test
'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        Quill.Normalizer.whitelistTags(@container.firstChild)
        expect(@container).toEqualHTML(test.expected)
      )
    )
  )

  describe('wrapInline()', ->
    tests =
      'Wrap newline':
        initial:  ['
']
        expected: ['

']
      'Wrap span':
        initial:  ['One']
        expected: ['
One
']
      'Wrap many spans':
        initial: [
          'One'
          'Two'
        ]
        expected: [
          '
OneTwo
'
        ]
      'Wrap break and span':
        initial:  ['
One']
        expected: ['

One
']

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial.join('')
        Quill.Normalizer.wrapInline(@container.firstChild)
        expect(@container).toEqualHTML(test.expected)
      )
    )
  )

  describe('wrapText()', ->
    tests =
      'inner text':
        initial:  'InnerTest'
        expected: 'InnerTest'
      'multiple inner':
        initial:  'TestTestTest'
        expected: 'TestTestTest'
      'text node':
        initial: 'Test'
        expected: 'Test'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        Quill.Normalizer.wrapText(@container)
        expect(@container).toEqualHTML(test.expected)
      )
    )

    it('should wrap multiple text nodes', ->
      @container.appendChild(@container.ownerDocument.createTextNode('A'))
      @container.appendChild(@container.ownerDocument.createTextNode('B'))
      Quill.Normalizer.wrapText(@container)
      expect(@container).toEqualHTML('AB')
    )
  )
)


_          = require('lodash')
DOM        = require('./dom')
Utils      = require('./utils')
Normalizer = require('./normalizer')

DEFAULT_STYLES =
  'html': { 'height': '100%', 'width': '100%' }
  'body':
    'box-sizing'  : 'border-box'
    'cursor'      : 'text'
    'font-family' : "'Helvetica', 'Arial', sans-serif"
    'font-size'   : '13px'
    'height'      : '100%'
    'line-height' : '1.42'
    'margin'      : '0px'
    'overflow-x'  : 'hidden'
    'overflow-y'  : 'auto'
    'padding'     : '12px 15px'
  '.editor-container':
    'height'      : '100%'
    'outline'     : 'none'
    'position'    : 'relative'
    'tab-size'    : '4'
    'white-space' : 'pre-wrap'
  '.editor-container a'          : { 'text-decoration': 'underline' }
  '.editor-container b'          : { 'font-weight': 'bold' }
  '.editor-container i'          : { 'font-style': 'italic' }
  '.editor-container s'          : { 'text-decoration': 'line-through' }
  '.editor-container u'          : { 'text-decoration': 'underline' }
  '.editor-container blockquote' : { 'margin': '0 0 0 2em', 'padding': '0' }
  '.editor-container ol'         : { 'margin': '0 0 0 2em', 'padding': '0', 'list-style-type': 'decimal' }
  '.editor-container ul'         : { 'margin': '0 0 0 2em', 'padding': '0', 'list-style-type': 'disc' }

LIST_STYLES = ['decimal', 'lower-alpha', 'lower-roman']
rule = '.editor-container ol > li'
_.each([1..9], (i) ->
  rule += ' > ol'
  DEFAULT_STYLES[rule] = { 'list-style-type': LIST_STYLES[i%3] }
  rule += ' > li'
)
DEFAULT_STYLES[DOM.DEFAULT_BREAK_TAG] = { 'display': 'none' } if Utils.isIE(10)

class Renderer
  @objToCss: (obj) ->
    return _.map(obj, (value, key) ->
      innerStr = _.map(value, (innerValue, innerKey) ->
        return "#{innerKey}: #{innerValue};"
      ).join(' ')
      return "#{key} { #{innerStr} }"
    ).join("\n")

  @buildFrame: (container) ->
    iframe = container.ownerDocument.createElement('iframe')
    iframe.frameBorder = '0'
    iframe.height = iframe.width = '100%'
    container.appendChild(iframe)
    iframeDoc = iframe.contentWindow.document
    iframeDoc.open()
    iframeDoc.write('<!DOCTYPE html>')
    iframeDoc.close()
    root = iframeDoc.createElement('div')
    iframeDoc.body.appendChild(root)
    return [root, iframe]

  constructor: (@container, @emitter, @options = {}) ->
    @container.innerHTML = ''
    [@root, @iframe] = Renderer.buildFrame(@container)
    @root.id = @options.id
    DOM.addClass(@root, 'editor-container')
    DOM.addEventListener(@container, 'focus', =>
      @root.focus()
    )
    this.addStyles(DEFAULT_STYLES)
    # Ensure user specified styles are added after modules'
    _.defer(_.bind(this.addStyles, this, @options.styles)) if @options.styles?

  addContainer: (className, before = false) ->
    refNode = if before then @root else null
    container = @root.ownerDocument.createElement('div')
    DOM.addClass(container, className)
    @root.parentNode.insertBefore(container, refNode)
    return container

  addStyles: (css) ->
    style = @root.ownerDocument.createElement('style')
    style.type = 'text/css'
    css = Renderer.objToCss(css) unless _.isString(css)
    if style.styleSheet?
      style.styleSheet.cssText = css
    else
      style.appendChild(@root.ownerDocument.createTextNode(css))
    # Firefox needs defer
    _.defer( =>
      @root.ownerDocument.querySelector('head').appendChild(style)
      @emitter.emit(@emitter.constructor.events.RENDER_UPDATE, css) if @emitter?
    )

module.exports = Renderer


_            = require('lodash')
ColorPicker  = require('../../lib/color-picker')
DefaultTheme = require('../default')
DOM          = require('../../dom')
Picker       = require('../../lib/picker')

class SnowTheme extends DefaultTheme
  @COLORS: [
    "#000000", "#e60000", "#ff9900", "#ffff00", "#008A00", "#0066cc", "#9933ff"
    "#ffffff", "#facccc", "#ffebcc", "#ffffcc", "#cce8cc", "#cce0f5", "#ebd6ff"
    "#bbbbbb", "#f06666", "#ffc266", "#ffff66", "#66b966", "#66a3e0", "#c285ff"
    "#888888", "#a10000", "#b26b00", "#b2b200", "#006100", "#0047b2", "#6b24b2"
    "#444444", "#5c0000", "#663d00", "#666600", "#003700", "#002966", "#3d1466"
  ]

  @OPTIONS:
    'multi-cursor':
      template:
       '

        '

  @STYLES:
    '.snow .image-tooltip-container a':
      'border': '1px solid #06c'
    '.snow .image-tooltip-container a.insert':
      'background-color': '#06c'
      'color': '#fff'
    '.snow .cursor-name':
      'border-radius': '4px'
      'font-size': '11px'
      'font-family': 'Arial'
      'margin-left': '-50%'
      'padding': '4px 10px'
    '.snow .cursor-triangle':
      'border-left': '4px solid transparent'
      'border-right': '4px solid transparent'
      'height': '0px'
      'margin-left': '-3px'
      'width': '0px'
    '.snow .cursor.left .cursor-name':
      'margin-left': '-8px'
    '.snow .cursor.right .cursor-flag':
      'right': 'auto'
    '.snow .cursor.right .cursor-name':
      'margin-left': '-100%'
      'margin-right': '-8px'
    '.snow .cursor-triangle.bottom':
      'border-top': '4px solid transparent'
      'display': 'block'
      'margin-bottom': '-1px'
    '.snow .cursor-triangle.top':
      'border-bottom': '4px solid transparent'
      'display': 'none'
      'margin-top': '-1px'
    '.snow .cursor.top .cursor-triangle.bottom':
      'display': 'none'
    '.snow .cursor.top .cursor-triangle.top':
      'display': 'block'
    '.snow a':
      'color': '#06c'
    '.snow .tooltip':
      'border': '1px solid #ccc'
      'box-shadow': '0px 0px 5px #ddd'
      'color': '#222'
    '.snow .tooltip a':
      'color': '#06c'
    '.snow .tooltip .input':
      'border': '1px solid #ccc'
      'margin': '0px'
      'padding': '5px'
    '.snow .image-tooltip-container .preview':
      'border-color': '#ccc'
      'color': '#ccc'
    '.snow .link-tooltip-container a, .snow .link-tooltip-container span':
      'display': 'inline-block'
      'line-height': '25px'

  constructor: (@quill) ->
    super
    @quill.addStyles(SnowTheme.STYLES)
    @pickers = []
    @quill.on(@quill.constructor.events.SELECTION_CHANGE, =>
      _.each(@pickers, (picker) ->
        picker.close()
      )
    )
    DOM.addClass(@quill.root.ownerDocument.body, 'snow')
    @quill.onModuleLoad('multi-cursor', _.bind(this.extendMultiCursor, this))
    @quill.onModuleLoad('toolbar', _.bind(this.extendToolbar, this))

  extendMultiCursor: (module) ->
    module.on(module.constructor.events.CURSOR_ADDED, (cursor) ->
      bottomTriangle = cursor.elem.querySelector('.cursor-triangle.bottom')
      topTriangle = cursor.elem.querySelector('.cursor-triangle.top')
      bottomTriangle.style.borderTopColor = topTriangle.style.borderBottomColor = cursor.color
    )

  extendToolbar: (module) ->
    _.each(module.container.querySelectorAll('.sc-font, .sc-size'), (select) =>
      picker = new Picker(select)
      @pickers.push(picker)
    )
    _.each(['color', 'background'], (css) =>
      select = module.container.querySelector(".sc-#{css}")
      return unless select?
      picker = new ColorPicker(select)
      @pickers.push(picker)
      DOM.addClass(picker.container.querySelector('.sc-picker-label'), 'sc-format-button')
      _.each(picker.container.querySelectorAll('.sc-picker-item'), (item, i) ->
        DOM.addClass(item, 'sc-primary-color') if i < 7
      )
    )

module.exports = SnowTheme


@quill.addFormat('author', { class: /^author-(.*)$/ })

@quill.on(@quill.constructor.events.PRE_EVENT, (eventName, delta, origin) =>

if eventName == @quill.constructor.events.TEXT_CHANGE and origin == 'user'

keep keep keep keep replace keep keep keep

@quill.addFormat({ class: /^author-(.*)$/ })


#mocha
#toolbar-container
  button.sc-bold Bold
  button.sc-link Link
  select.sc-size
    option(value='10px') Small
    option(value='13px', selected=true) Normal
    option(value='18px') Large
    option(value='32px') Huge
#editor-container(style="border: 1px solid gray; min-height: 200px;")


_          = require('lodash')
DOM        = require('./dom')
Format     = require('./format')
Line       = require('./line')
LinkedList = require('./lib/linked-list')
Normalizer = require('./normalizer')
Utils      = require('./utils')
Tandem     = require('tandem-core')

class Document
  constructor: (@root, options = {}) ->
    @formats = {}
    _.each(options.formats, _.bind(this.addFormat, this))
    this.setHTML(@root.innerHTML)

  addFormat: (name, config) ->
    config = Format.FORMATS[name] unless _.isObject(config)
    console.warn('Overwriting format', name, @formats[name]) if @formats[name]?
    @formats[name] = new Format(@root.ownerDocument, config)

  appendLine: (lineNode) ->
    return this.insertLineBefore(lineNode, null)

  findLeafAt: (index) ->
    [line, offset] = this.findLineAt(index)
    return if line? then line.findLeafAt(offset) else [null, offset]

  findLine: (node) ->
    while node? and node.parentNode != @root
      node = node.parentNode
    line = if node? then @lineMap[node.id] else null
    return if line?.node == node then line else null

  findLineAt: (index) ->
    return [null, index] unless @lines.length > 0
    length = this.toDelta().endLength     # TODO optimize
    return [@lines.last, @lines.last.length] if index == length
    return [null, index - length] if index > length
    curLine = @lines.first
    while curLine?
      return [curLine, index] if index < curLine.length
      index -= curLine.length
      curLine = curLine.next
    return [null, index]    # Should never occur unless length calculation is off

  insertLineBefore: (newLineNode, refLine) ->
    line = new Line(this, newLineNode)
    if refLine?
      @root.insertBefore(newLineNode, refLine.node) unless DOM.isElement(newLineNode.parentNode)  # Would prefer newLineNode.parentNode? but IE will have non-null object
      @lines.insertAfter(refLine.prev, line)
    else
      @root.appendChild(newLineNode) unless DOM.isElement(newLineNode.parentNode)
      @lines.append(line)
    @lineMap[line.id] = line
    return line

  mergeLines: (line, lineToMerge) ->
    if lineToMerge.length > 1
      DOM.removeNode(line.leaves.last.node) if line.length == 1
      _.each(DOM.getChildNodes(lineToMerge.node), (child) ->
        line.node.appendChild(child) if child.tagName != DOM.DEFAULT_BREAK_TAG
      )
    this.removeLine(lineToMerge)
    line.rebuild()

  optimizeLines: ->
    # TODO optimize algorithm (track which lines get dirty and only Normalize.optimizeLine those)
    _.each(@lines.toArray(), (line, i) ->
      line.optimize()
      return true    # line.optimize() might return false, prevent early break
    )

  rebuild: ->
    lines = @lines.toArray()
    lineNode = @root.firstChild
    _.each(lines, (line, index) =>
      while line.node != lineNode
        if line.node.parentNode == @root
          # New line inserted
          lineNode = Normalizer.normalizeLine(lineNode)
          newLine = this.insertLineBefore(lineNode, line)
          lineNode = lineNode.nextSibling
        else
          # Existing line removed
          return this.removeLine(line)
      if line.outerHTML != lineNode.outerHTML
        # Existing line changed
        line.node = Normalizer.normalizeLine(line.node)
        line.rebuild()
      lineNode = line.node.nextSibling
    )
    # New lines appended
    while lineNode?
      lineNode = Normalizer.normalizeLine(lineNode)
      this.appendLine(lineNode)
      lineNode = lineNode.nextSibling

  removeLine: (line) ->
    DOM.removeNode(line.node) if line.node.parentNode == @root
    delete @lineMap[line.id]
    @lines.remove(line)

  setHTML: (html) ->
    @root.innerHTML = Normalizer.stripWhitespace(html)
    @lines = new LinkedList()
    @lineMap = {}
    this.rebuild()

  splitLine: (line, offset) ->
    offset = Math.min(offset, line.length - 1)
    [lineNode1, lineNode2] = Utils.splitNode(line.node, offset, true)
    line.node = lineNode1
    line.rebuild()
    newLine = this.insertLineBefore(lineNode2, line.next)
    return newLine

  toDelta: ->
    lines = @lines.toArray()
    ops = _.flatten(_.map(lines, (line) ->
      return _.clone(line.delta.ops)
    ), true)
    return new Tandem.Delta(0, ops)

module.exports = Document


_     = require('lodash')
DOM   = require('./dom')
Utils = require('./utils')

Normalizer =
  ALIASES: {
    'STRONG' : 'B'
    'EM'     : 'I'
    'DEL'    : 'S'
    'STRIKE' : 'S'
  }

  ATTRIBUTES: {
    'color': 'color'
    'face' : 'fontFamily'
  }

  STYLES: {
    'background-color'
    'color'
    'font-family'
    'font-size'
    'text-align'
  }

  TAGS: {
    'DIV'
    'BR'
    'SPAN'
    'B'
    'I'
    'S'
    'U'
    'A'
    'IMG'
  }

  # Make sure descendant break tags are not causing multiple lines to be rendered
  handleBreaks: (lineNode) ->
    breaks = _.map(lineNode.querySelectorAll(DOM.DEFAULT_BREAK_TAG))
    _.each(breaks, (br) =>
      Utils.splitAncestors(br.nextSibling, lineNode.parentNode) if br.nextSibling?
    )
    return lineNode

  normalizeLine: (lineNode) ->
    lineNode = Normalizer.wrapInline(lineNode)
    lineNode = Normalizer.handleBreaks(lineNode)
    Normalizer.pullBlocks(lineNode)
    Normalizer.wrapText(lineNode)
    return lineNode

  normalizeNode: (node) ->
    _.each(Normalizer.ATTRIBUTES, (style, attribute) ->
      if node.hasAttribute(attribute)
        node.style[style] = node.getAttribute(attribute)
        node.removeAttribute(attribute)
    )
    Normalizer.whitelistStyles(node)
    return Normalizer.whitelistTags(node)

  # Removes unnecessary tags but does not modify line contents
  optimizeLine: (lineNode) ->
    lineNodeLength = Utils.getNodeLength(lineNode)
    nodes = DOM.getDescendants(lineNode)
    while nodes.length > 0
      node = nodes.pop()
      continue unless node?.parentNode?
      continue if DOM.EMBED_TAGS[node.tagName]?
      if node.tagName == DOM.DEFAULT_BREAK_TAG
        # Remove unneeded BRs
        DOM.removeNode(node) unless lineNodeLength == 0
      else if Utils.getNodeLength(node) == 0
        nodes.push(node.nextSibling)
        DOM.unwrap(node)
      else
        attributes = DOM.getAttributes(node)
        if node.tagName == DOM.DEFAULT_INLNE_TAG
          # Remove unneeded SPANs
          if _.keys(attributes).length == 0 and (node.parentNode != lineNode or DOM.isElement(node.firstChild))
            nodes.push(node.nextSibling)
            DOM.unwrap(node)
            continue
        if node.previousSibling? and node.tagName == node.previousSibling.tagName
          # Merge similar nodes
          neighborAttributes = DOM.getAttributes(node.previousSibling)
          if _.isEqual(attributes, neighborAttributes)
            nodes.push(node.firstChild)
            DOM.moveChildren(node.previousSibling, node)
            DOM.normalize(node.previousSibling)
            DOM.removeNode(node)
    Normalizer.wrapText(lineNode)

  # Make sure descendants are all inline elements
  pullBlocks: (lineNode) ->
    curNode = lineNode.firstChild
    return unless curNode?
    if DOM.BLOCK_TAGS[curNode.tagName]?
      if curNode.nextSibling?
        Utils.splitAncestors(curNode.nextSibling, lineNode.parentNode)
      DOM.unwrap(curNode)
      Normalizer.pullBlocks(lineNode)
    curNode = curNode.nextSibling
    while curNode?
      if DOM.BLOCK_TAGS[curNode.tagName]?
        lineNode = Utils.splitAncestors(curNode, lineNode.parentNode)
        break
      curNode = curNode.nextSibling

  stripWhitespace: (html) ->
    # Remove leading and tailing whitespace
    html = html.replace(/^\s+/, '').replace(/\s+$/, '')
    # Remove whitespace between tags, requires   for legitmate spaces
    html = html.replace(/\>\s+\</g, '><')
    return html

  whitelistStyles: (node) ->
    original = DOM.getStyles(node)
    styles = _.omit(original, (value, key) ->
      return !Normalizer.STYLES[key]?
    )
    if _.keys(styles).length < _.keys(original).length
      if _.keys(styles).length > 0
        DOM.setStyles(node, styles)
      else
        node.removeAttribute('style')

  whitelistTags: (node) ->
    return unless DOM.isElement(node)
    node = DOM.switchTag(node, Normalizer.ALIASES[node.tagName]) if Normalizer.ALIASES[node.tagName]?
    if !Normalizer.TAGS[node.tagName]?
      tagName = if DOM.BLOCK_TAGS[node.tagName]? then DOM.DEFAULT_BLOCK_TAG else DOM.DEFAULT_INLNE_TAG
      node = DOM.switchTag(node, tagName)
    return node

  # Wrap inline nodes with block tags
  wrapInline: (lineNode) ->
    return lineNode if DOM.BLOCK_TAGS[lineNode.tagName]?
    blockNode = lineNode.ownerDocument.createElement(DOM.DEFAULT_BLOCK_TAG)
    lineNode.parentNode.insertBefore(blockNode, lineNode)
    while lineNode? and !DOM.BLOCK_TAGS[lineNode.tagName]?
      nextNode = lineNode.nextSibling
      blockNode.appendChild(lineNode)
      lineNode = nextNode
    return blockNode

  wrapText: (lineNode) ->
    texts = DOM.getTextNodes(lineNode)
    _.each(texts, (textNode) =>
      if textNode.previousSibling? or textNode.nextSibling? or textNode.parentNode == lineNode
        DOM.wrap(lineNode.ownerDocument.createElement(DOM.DEFAULT_INLNE_TAG), textNode)
    )

module.exports = Normalizer


format: new Quill.Format(document, Quill.Format.FORMATS.align)

format: new Quill.Format(document, { class: 'author-' })